When migrating from AngularJS (1.x) to Angular (2+), one of the biggest challenges is converting custom directives to Angular components. AngularJS directives are a powerful way to extend HTML functionality, but in Angular, components replace most directive use cases. If not converted properly, directives may stop working, leading to issues like:
- Bindings not working
- DOM manipulation failures
- Scope issues
- Dependency injection errors
In this guide, we’ll cover:
- Understanding AngularJS directives
- How Angular components replace directives
- Step-by-step directive migration
- Common errors and fixes
1. Understanding AngularJS Directives
Types of Directives in AngularJS
Directive Type | Example | Purpose |
---|---|---|
Element (E ) | <my-directive></my-directive> | Creates a new HTML element |
Attribute (A ) | <div my-directive></div> | Attaches behavior to an existing element |
Class (C ) | <div class="my-directive"></div> | Applies behavior based on class name |
Comment (M ) | <!-- directive: my-directive --> | Used in HTML comments (deprecated) |
In Angular (2+), components replace most directives and only structural (*ngIf
, *ngFor
) and attribute directives ([ngClass]
, [ngStyle]
) remain.
2. How Angular Components Replace Directives
AngularJS Directive Example
angular.module('myApp').directive('myDirective', function() {
return {
restrict: 'E',
template: `<p>Hello, {{ message }}</p>`,
scope: {
message: '@'
}
};
});
Converted to Angular Component
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-my-directive',
template: `<p>Hello, {{ message }}</p>`
})
export class MyDirectiveComponent {
@Input() message: string = '';
}
Why this works:
- Angular components are the replacement for most element-based directives.
- Uses
@Input()
instead of isolated scope (scope: {}
) in AngularJS. - Eliminates
$scope
and replaces it with TypeScript properties.
3. Step-by-Step Migration of Directives
Let’s break down the migration process for different types of directives.
Step 1: Migrating Element Directives (E
)
AngularJS Directive
angular.module('myApp').directive('customButton', function() {
return {
restrict: 'E',
template: `<button>{{ label }}</button>`,
scope: {
label: '@'
}
};
});
Converted to Angular Component
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-custom-button',
template: `<button>{{ label }}</button>`
})
export class CustomButtonComponent {
@Input() label: string = '';
}
Why this works?
@Input()
replaces isolated scope (scope: { label: '@' }
).restrict: 'E'
is now handled byselector: 'app-custom-button'
.
Step 2: Migrating Attribute Directives (A
)
AngularJS Attribute Directive
angular.module('myApp').directive('highlight', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.css('background-color', attrs.highlight);
}
};
});
Converted to Angular Attribute Directive
import { Directive, ElementRef, Renderer2, Input, OnInit } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
@Input() appHighlight: string = '';
constructor(private el: ElementRef, private renderer: Renderer2) {}
ngOnInit() {
this.renderer.setStyle(this.el.nativeElement, 'background-color', this.appHighlight);
}
}
Why this works?
- Uses
@Directive()
instead of.directive()
. - Replaces
link
function withngOnInit()
. - Uses
ElementRef
andRenderer2
to manipulate the DOM safely.
🔹 Usage in HTML (Same as AngularJS)
<div appHighlight="yellow">This is highlighted</div>
Step 3: Migrating Directives with Two-Way Binding (=
)
AngularJS Directive
angular.module('myApp').directive('counter', function() {
return {
restrict: 'E',
template: `<button ng-click="increment()">+</button> {{ count }}`,
scope: {
count: '='
},
link: function(scope) {
scope.increment = function() {
scope.count++;
};
}
};
});
Converted to Angular Component
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
template: `<button (click)="increment()">+</button> {{ count }}`
})
export class CounterComponent {
@Input() count: number = 0;
@Output() countChange = new EventEmitter<number>();
increment() {
this.count++;
this.countChange.emit(this.count);
}
}
Usage in HTML
<app-counter [(count)]="counterValue"></app-counter>
Why this works?
@Input()
replaces one-way binding (=
scope).@Output()
withEventEmitter
replaces$scope.$watch()
.(click)="increment()"
replacesng-click="increment()"
.
Step 4: Migrating Directives Using Controllers
AngularJS Directive with Controller
angular.module('myApp').directive('greeting', function() {
return {
restrict: 'E',
template: `<p>{{ ctrl.message }}</p>`,
controller: function() {
this.message = "Hello, World!";
},
controllerAs: 'ctrl'
};
});
Converted to Angular Component
import { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `<p>{{ message }}</p>`
})
export class GreetingComponent {
message: string = "Hello, World!";
}
Why this works?
- Removes the need for
controllerAs
. - Moves logic to the component class.
4. Common Errors and Fixes
Error | Solution |
---|---|
TypeError: Cannot read property of undefined | Use @Input() to pass values from parent components |
Bindings not working in Angular component | Ensure properties are initialized in the TypeScript class |
Event binding not working | Use @Output() and EventEmitter instead of $scope.$emit() |
DOM manipulation errors | Use Renderer2 instead of document.querySelector() |
Two-way binding ( =) not updating | Use [(ngModel)] or @Input() with @Output() |