Migrate a legacy AngularJS directive to .component()
Convert an isolated-scope directive to AngularJS 1.5+ component syntax with one-way bindings and lifecycle hooks.
27 Feb 2026, 03:12 UTC

Problem and takeaway
Legacy AngularJS directives often rely on $scope and two-way '=' bindings. That makes data flow hard to trace because any controller or child directive can mutate shared objects, and debugging requires inspecting scope hierarchies in the browser. If you are on AngularJS 1.5 or later, including the final 1.8.x LTS line, you can register a component instead. A component defaults to controller-as syntax and isolated scope, uses explicit one-way bindings, and exposes lifecycle hooks such as $onInit and $onChanges. Isolated scope means the component does not prototypically inherit the parent scope, so bindings must be declared explicitly. Controller-as syntax means the controller instance is exposed as $ctrl in the template rather than attaching properties to $scope. The digest cycle is AngularJS's mechanism for checking bindings and updating the DOM; it still runs with components.
Takeaway: use .component() for new UI widgets and when refactoring isolated-scope directives, but remember AngularJS is end of life. Expect only critical security patches, so treat this as a stepping stone, not a long-term platform investment.
Desired outcome
A legacy directive that binds user data through scope is replaced by a component with explicit bindings, a controller, and an inline template. The component renders the same UI, initializes after bindings are ready, and responds to input changes through $onChanges.
Prerequisites
- AngularJS 1.5.0 or later. Confirm the version in package.json, bower.json, or the script tag; .component() does not exist in 1.4.x or earlier.
- An existing AngularJS module to register the component in.
- Write access to the project's JavaScript source files.
- A browser with developer tools for runtime inspection.
Procedure
1. Locate the directive to replace
Find a directive with restrict: 'E', an isolated scope, and a template. Example legacy directive:
angular.module('app').directive('userCard', function() {
return {
restrict: 'E',
scope: {
user: '=',
title: '@',
onSave: '&'
},
template: '{{title}}
{{user.name}}Save',
link: function(scope) {
scope.$watch('user', function(newUser) {
if (newUser) scope.displayName = newUser.name;
});
}
};
});
2. Register the component
In the same module, call .component() with a name, bindings, controller, and template. Use one-way bindings where possible: < for objects, @ for strings, and & for callbacks.
angular.module('app').component('userCard', {
bindings: {
user: '<',
title: '@?',
onSave: '&'
},
template: '
{{$ctrl.title}}
{{$ctrl.user && $ctrl.user.name}}
Save
',
controller: function() {
var ctrl = this;
ctrl.$onInit = function() {
ctrl.displayName = ctrl.user && ctrl.user.name;
};
ctrl.$onChanges = function(changes) {
if (changes.user && changes.user.currentValue) {
ctrl.displayName = changes.user.currentValue.name;
}
};
}
});
In HTML, write the element as <user-card user="$ctrl.currentUser" title="'Profile'" on-save="$ctrl.saveUser()"></user-card>. AngularJS normalizes the kebab-case element name to camelCase internally.
3. Update the parent view
Replace the directive element with the component element and pass bindings using the prefix that matches the binding type:
<user-card user="$ctrl.currentUser" title="'User profile'" on-save="$ctrl.saveUser()"></user-card>
Remove the old directive registration if nothing else references it.
4. Run the application
Run the development server from the project root, for example:
npm start
Open the view containing <user-card>. Open browser developer tools and confirm:
- No AngularJS errors in the console.
- The title and user name render.
- Clicking Save invokes the parent callback.
Expected checks
- Inspect the component instance: in the Elements panel, select <user-card>, then in the Console run
angular.element($0).controller('userCard')to verify the controller object exists. - Add a temporary
console.log('$onInit', this)inside$onInitand reload to confirm it runs after bindings are initialized. - Change
currentUserin the parent and confirm$onChangesfires for the wrapped binding. - Run the existing test suite or manual regression checks for the surrounding routes.
Limitations
$onChangesonly fires for wrapped binding changes. Mutating a property inside a shared object or array passed by reference does not trigger it.- Components still participate in the AngularJS digest cycle. Excessive watchers or complex expressions in templates can degrade rendering performance.
- AngularJS 1.8.2 is the final LTS release. .component() receives no new feature development after LTS ended.
- Mixing .component() and legacy $scope directives in the same module can create inconsistent data flow.
Recovery options
Because this procedure changes source files, keep the original directive in a commented block or a separate branch until the component passes regression checks. If rendering breaks or callbacks fail:
- Revert the module file to the previous commit.
- Restore the original directive element in the view.
- Confirm the application returns to its prior behavior before retrying the component conversion.
Practical verification
After deployment, open browser developer tools on the affected view and check that no digest iteration errors appear when the parent updates user data. If the component displays stale data, verify the parent is replacing the object reference rather than mutating it in place, since one-way < bindings detect reference changes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.