Migrating an application to component directives
In Angular 1, there are several built-in directives, including ngController
and ngInclude
, that developers tend to lean on when building applications. While not anti-patterns, using these features moves away from having a component-centric application.
All these directives are actually subsets of component functionality, and they can be entirely refactored out.
Note
The code, links, and a live example related to this recipe are available at http://ngcookbook.herokuapp.com/1008/.
Getting ready
Suppose your initial application is as follows:
[index.html] <div ng-app="articleApp"> <ng-include src="'/press_header.html'"></ng-include> <div ng-controller="articleCtrl as article"> <h1>{{article.title}}</h1> <p>Written by: {{article.author}}</p> </div> <script type="text/ng-template" id="/press_header.html"> <div ng-controller="headerCtrl as header"> <strong> Angular Chronicle - {{header.currentDate | date}} </strong> <hr /> </div> </script> </div> [app.js] angular.module('articleApp', []) .controller('articleCtrl', function() { this.title = 'Food Fight Erupts During Diplomatic Luncheon'; this.author = 'Jake'; }) .controller('headerCtrl', function() { this.currentDate = new Date(); });
Note
Note that this example application contains a large number of very common Angular 1 patterns; you can see the ngController
directives sprinkled throughout. Also, it uses an ngInclude
directive to incorporate a header. Keep in mind that these directives are not inappropriate for a well-formed Angular 1 application. However, you can do better, and this involves refactoring to a component-driven design.
How to do it...
Component-driven patterns don't need to be frightening in appearance. In this example (and for essentially all Angular 1 applications), you can do a component refactor while leaving the existing template largely intact.
Begin with the ngInclude
directive. Moving this to a component directive is simple—it becomes a directive with templateUrl
set to the template path:
[index.html] <div ng-app="articleApp"> <header></header> <div ng-controller="articleCtrl as article"> <h1>{{article.title}}</h1> <p>Written by: {{article.author}}</p> </div> <script type="text/ng-template" id="/press_header.html"> <div ng-controller="headerCtrl as header"> <strong> Angular Chronicle - {{header.currentDate | date}} </strong> <hr /> </div> </script> </div> [app.js] angular.module('articleApp', []) .controller('articleCtrl', function() { this.title = 'Food Fight Erupts During Diplomatic Luncheon'; this.author = 'Jake'; }) .controller('headerCtrl', function() { this.currentDate = new Date(); }) .directive('header', function() { return { templateUrl: '/press_header.html' }; });
Next, you can also refactor ngController
everywhere it appears. In this example, you find two extremely common appearances of ngController
. The first is at the head of the press_header.html
template, acting as the top-level controller for that template. Often, this results in needing a superfluous wrapper element just to house the ng-controller
attribute. The second is ngController
nested inside your primary application template, controlling some arbitrary portion of the DOM. Both of these can be refactored to component directives by reassigning ngController
to a directive controller:
[index.html] <div ng-app="articleApp"> <header></header> <article></article> </div> [app.js] angular.module('articleApp', []) .directive('header', function() { return { controller: function() { this.currentDate = new Date(); }, controllerAs: 'header', template: ` <strong> Angular Chronicle - {{header.currentDate | date}} </strong> <hr /> ` }; }) .directive('article', function() { return { controller: function() { this.title = 'Food Fight Erupts During Diplomatic Luncheon'; this.author = 'Jake'; }, controllerAs: 'article', template: ` <h1>{{article.title}}</h1> <p>Written by: {{article.author}}</p> ` }; });
Tip
Note that templates here are included in the directive for visual congruity. For large applications, it is preferred that you use templateUrl
and locate the template markup in its own file.
How it works...
Generally speaking, an application can be represented by a hierarchy of nested MVC components. ngInclude
and ngController
act as subsets of a component functionality, and so it makes sense that you are able to expand them into full component directives.
In the preceding example, the ultimate application structure is comprised of only components. Each component is delegated its own template, controller, and model (by virtue of the controller object itself). Sticklers will dispute whether or not Angular belongs to true MVC style, but in the context of component refactoring, this is irrelevant. Here, you have defined a structure that is completely modular, reusable, testable, abstractable, and easily maintainable. This is the style of Angular 2, and the value of this should be immediately apparent.
There's more...
An alert developer will notice that no attention is paid to scope inheritance. This is a difficult problem to approach, mostly because many of the patterns in Angular 1 are designed for a mishmash between a scope and controllerAs
. Angular 2 is built around strict input and output between nested components; however, in Angular 1, scope is inherited by default, and nested directives, by default, have access to their encompassing controller objects.
Thus, to truly emulate an Angular 2 style, one must configure their application to explicitly pass data and methods to children, similar to the controllerAs
encapsulation recipe. However, this does not preclude direct data access to ancestral component directive controllers; it merely wags a finger at it since it adds additional dependencies.
See also
- Componentizing directives using controllerAs encapsulation shows you a superior method of organizing Angular 1 directives
- Implementing a basic component in AngularJS 1.5 details how to write an Angular 1 component
- Normalizing service types gives instruction on how to align your Angular 1 service types for Angular 2 compatibility