One of the unusual conventions introduced in Angular 1 was the relationship between directives and the data they consumed. By default, directives used an inherited scope, which suited the needs of most developers just fine. While this was easy to use, it had the effect of introducing extra dependencies in the directives, and also the convention that directives often did not own the data they were consuming. Additionally, the data interpolated in the template was unclear in relation to where it was being assigned or owned.
Angular 2 utilizes components as the building blocks of the entire application. These components are class-based and are therefore in some ways at odds with the scope mechanisms of Angular 1. Transitioning to a controller-centric directive model is a large step towards compliance with the Angular 2 standards.
Suppose your application contains the following setup that involves the nested directives that share data using an isolate scope:
[index.html]
<div ng-app="articleApp">
<article></article>
</div>
[app.js]
angular.module('articleApp', [])
.directive('article', function() {
return {
controller: function($scope) {
$scope.articleData = {
person: {firstName: 'Jake'},
title: 'Lesotho Yacht Club Membership Booms'
};
},
template: `
<h1>{{articleData.title}}</h1>
<attribution author="articleData.person.firstName">
</attribution>
`
};
})
.directive('attribution', function() {
return {
scope: {author: '='},
template: `<p>Written by: {{author}}</p>`
};
});
The goal is to refactor this setup so that templates can be explicit about where the data is coming from and so that the directives have ownership of this data:
[app.js]
angular.module('articleApp', [])
.directive('article', function() {
return {
controller: function() {
this.person = {firstName: 'Jake'};
this.title = 'Lesotho Yacht Club Membership Booms';
},
controllerAs: 'articleCtrl',
template: `
<h1>{{articleCtrl.title}}</h1>
<attribution></attribution>
`
};
})
.directive('attribution', function() {
return {
template: `<p>Written by: {{articleCtrl.author}}</p>`
};
});
In this second implementation, anywhere you use the article data, you are certain of its origin. This is better, but the child directive is still referencing the parent controller, which isn't ideal since it is introducing an unneeded dependency. The attribution directive instance should be provided with the data, and it should instead interpolate from its own controller instance:
[app.js]
angular.module('articleApp', [])
.directive('article', function() {
return {
controller: function() {
this.person = {firstName: 'Jake'};
this.title = 'Lesotho Yacht Club Membership Booms';
},
controllerAs: 'articleCtrl',
template: `
<h1>{{articleCtrl.title}}</h1>
<attribution author="articleCtrl.person.firstName">
</attribution>
`
};
})
.directive('attribution', function() {
return {
controller: function() {},
controllerAs: 'attributionCtrl',
bindToController: {author: '='},
template: `<p>Written by: {{attributionCtrl.author}}</p>`
};
});
Much better! You provide the child directive with a stand-in controller and give it an alias in the attributionCtrl
template. It is implicitly bound to the controller instance via bindToController
in the same way you would accomplish a regular isolate scope; however, the binding is directly attributed to the controller object instead of the scope.
Now that you have introduced the notion of data ownership, suppose you want to modify your application data. What's more, you want different parts of your application to be able to modify it. A naïve implementation of this would be something as follows:
[app.js]
angular.module('articleApp', [])
.directive('attribution', function() {
return {
controller: function() {
this.capitalize = function() {
this.author = this.author.toUpperCase();
}
},
controllerAs: 'attributionCtrl',
bindToController: {author: '='},
template: `
<p ng-click="attributionCtrl.capitalize()">
Written by: {{attributionCtrl.author}}
</p>`
};
});
The desired behavior is for you to click on the author, and it will become capitalized. However, in this implementation, the article controller's data is modified in the attribution controller, which does not own it. It is preferable for the controller that owns the data to perform the actual modification and instead supply an interface that an outside entity—here, the attribution directive—could use:
[app.js]
angular.module('articleApp', [])
.directive('article', function() {
return {
controller: function() {
this.person = {firstName: 'Jake'};
this.title = 'Lesotho Yacht Club Membership Booms';
this.capitalize = function() {
this.person.firstName =
this.person.firstName.toUpperCase();
};
},
controllerAs: 'articleCtrl',
template: `
<h1>{{articleCtrl.title}}</h1>
<attribution author="articleCtrl.person.firstName"
upper-case-author="articleCtrl.capitalize()">
</attribution>
`
};
})
.directive('attribution', function() {
return {
controller: function() {},
controllerAs: 'attributionCtrl',
bindToController: {
author: '=',
upperCaseAuthor: '&'
},
template: `
<p ng-click="attributionCtrl.upperCaseAuthor()">
Written by: {{attributionCtrl.author}}
</p>`
};
});
Vastly superior! You are still able to namespace within the click binding, but the owning directive controller is providing a method to outside entities instead of just giving them direct data access.