Angular 1.5 is set to introduce the .component()
helper method, which is much simpler than the .directive()
definition and advocates best practices and common default behaviours. Using .component()
will allow developers to write in an Angular 2 style as well, which will in turn make upgrading to Angular 2 an easier feat.
Let’s compare the differences in syntax and the super neat abstraction that .component()
gives us over using the .directive()
method.
Please note: Angular 1.5 is still in beta
phase, keep an eye out for it’s release!
.directive() to .component()
The syntax change is very simple:
// before
module.directive(name, fn);
// after
module.component(name, options);
The name
argument is what we want to define our Component as, the options
argument is a definition Object passed into the component, rather than a function that we know so well in versions 1.4 and below.
I’ve prebuilt a simple counter
component for the purposes of this exercise in Angular 1.4.x
which we’ll refactor into a version v1.5.0
build to use .component()
.
.directive('counter', function counter() {
return {
scope: {},
bindToController: {
count: '='
},
controller: function () {
function increment() {
this.count++;
}
function decrement() {
this.count--;
}
this.increment = increment;
this.decrement = decrement;
},
controllerAs: 'counter',
template: [
'<div class="todo">',
'<input type="text" ng-model="counter.count">',
'<button type="button" ng-click="counter.decrement();">-</button>',
'<button type="button" ng-click="counter.increment();">+</button>',
'</div>'
].join('')
};
});
A live embed of the 1.4.x
Directive:
We’ll continue building this alongside how we’d build the Angular 1.4 version to compare differences.
Function to Object, method name change
Let’s start from the top and refactor the function
argument to become an Object
, and change the name from .directive()
to .component()
:
// before
.directive('counter', function counter() {
return {
};
});
// after
.component('counter', {
});
Nice and simple. Essentially the return {};
statement inside the .directive()
becomes the Object definition inside .component()
– easy!
“scope” and “bindToController”, to “bindings”
In a .directive()
, the scope
property allows us to define whether we want to isolate the $scope
or inherit it, this has now become a sensible default to (usually) always make our Directives have isolate scope. So repeating ourselves each time just creates excess boilerplate. With the introduction of bindToController
, we can explicitly define which properties we want to pass into our isolate scope and bind directly to the Controller.
With the bindings
property on .component()
we can remove this boilerplate and simply define what we want to pass down to the component, under the assumption that the component will have isolate scope.
// before
.directive('counter', function counter() {
return {
scope: {},
bindToController: {
count: '='
}
};
});
// after
.component('counter', {
bindings: {
count: '='
}
});
Controller and controllerAs changes
Nothing has changed in the way we declare controller
, however it’s now a little smarter.
If we’re using a controller local to the component, we’ll do this:
// 1.4
{
...
controller: function () {}
...
}
If we’re using another Controller defined elsewhere, we’ll do this:
// 1.4
{
...
controller: 'SomeCtrl'
...
}
If we want to define controllerAs
at this stage, we’ll need to create a new property and define the instance alias:
// 1.4
{
...
controller: 'SomeCtrl',
controllerAs: 'something'
...
}
This then allows us to use something.prop
inside our template
to talk to the instance of the Controller.
Now, there are some changes in .component()
that make sensible assumptions and automatically create a controllerAs
property under the hood for us, and automatically assign a name based on three possibilities:
// inside angular.js
controllerAs: identifierForController(options.controller) || options.controllerAs || name,
Possibility one uses this aptly named identifierForController
function that looks like so:
// inside angular.js
var CNTRL_REG = /^(S+)(s+ass+(w+))?$/;
function identifierForController(controller, ident) {
if (ident && isString(ident)) return ident;
if (isString(controller)) {
var match = CNTRL_REG.exec(controller);
if (match) return match[3];
}
}
This allows us to do the following inside .component()
:
// 1.5
{
...
controller: 'SomeCtrl as something'
...
}
This saves adding the controllerAs
property… however…
We can add the controllerAs
property to maintain backwards compatibility or keep it if that’s within your style for writing Directives/Components.
The third option, and better yet, completely removes all need to think about controllerAs
, and Angular automatically uses the name of the Component you’ve created to instantiate the Controller with that alias. For instance:
.component('test', {
controller: function () {
this.testing = 123;
}
});
The would-be controllerAs
definition automatically defaults to test
, so we can use test.testing
in our template
which would give us the value of 123
.
Based on this information, we add our controller
, and refactor our Directive into a Component by dropping the controllerAs
property:
// before
.directive('counter', function counter() {
return {
scope: {},
bindToController: {
count: '='
},
controller: function () {
function increment() {
this.count++;
}
function decrement() {
this.count--;
}
this.increment = increment;
this.decrement = decrement;
},
controllerAs: 'counter'
};
});
// after
.component('counter', {
bindings: {
count: '='
},
controller: function () {
function increment() {
this.count++;
}
function decrement() {
this.count--;
}
this.increment = increment;
this.decrement = decrement;
}
});
Things are becoming much simpler to use and define with this change.
Template
There’s a subtle difference in the template
property worth noting. Let’s add the template
property to finish off our rework and then take a look.
.component('counter', {
bindings: {
count: '='
},
controller: function () {
function increment() {
this.count++;
}
function decrement() {
this.count--;
}
this.increment = increment;
this.decrement = decrement;
},
template: [
'<div class="todo">',
'<input type="text" ng-model="counter.count">',
'<button type="button" ng-click="counter.decrement();">-</button>',
'<button type="button" ng-click="counter.increment();">+</button>',
'</div>'
].join('')
});
The template
property can be defined as a function that is now injected with $element
and $attrs
locals. If the template
property is a function then it needs to return an String representing the HTML to compile:
{
...
template: function ($element, $attrs) {
// access to $element and $attrs
return [
'<div class="todo">',
'<input type="text" ng-model="counter.count">',
'<button type="button" ng-click="counter.decrement();">-</button>',
'<button type="button" ng-click="counter.increment();">+</button>',
'</div>'
].join('')
}
...
}
Let’s checkout the live working example with Angular version v1.5.0-build.4376+sha.aff74ec
:
That’s it for our Directive to Component refactor, however there are a few other changes worth exploring before we finish.
Assumed transclusion
Components now assume that we’ll want some transclusion, so this is enabled by default within these lines of the Angular source:
// angular.js
{
...
transclude: options.transclude === undefined ? true : options.transclude
...
}
Obviously to disable transclusion, set transclude: false
and you’re good to go.
Disabling isolate scope
Components are created with isolate scope by default. To disable isolate scope inside .component()
, we can simply add the following property:
.component('counter', {
isolate: false
});
The corresponding section of code in the Angular source uses a ternary operator to automatically assign an empty Object to the scope
property, and if we disable isolate scope inside a normal .directive()
and want to inherit, we’ll do scope: true
. Here’s the source code:
{
...
scope: options.isolate === false ? true : {}
...
}
Sourcecode for comparison
Throughout the article I’ve referred to some Angular source code snippets to cross reference against. You can find the source code here or check it out below, it’s a really nice abstraction:
component: function(name, options) {
function factory($injector) {
function makeInjectable(fn) {
if (angular.isFunction(fn)) {
return function(tElement, tAttrs) {
return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
};
} else {
return fn;
}
}
var template = (!options.template && !options.templateUrl ? '' : options.template);
return {
controller: options.controller || function() {},
controllerAs: identifierForController(options.controller) || options.controllerAs || name,
template: makeInjectable(template),
templateUrl: makeInjectable(options.templateUrl),
transclude: options.transclude === undefined ? true : options.transclude,
scope: options.isolate === false ? true : {},
bindToController: options.bindings || {},
restrict: options.restrict || 'E'
};
}
if (options.$canActivate) {
factory.$canActivate = options.$canActivate;
}
if (options.$routeConfig) {
factory.$routeConfig = options.$routeConfig;
}
factory.$inject = ['$injector'];
return moduleInstance.directive(name, factory);
}
Again, please note that Angular 1.5 isn’t released just yet, so this article uses an API that may be subject to slight change.
Upgrading to Angular 2
Writing components in this style will allow you to upgrade your Components using .component()
into Angular 2 very easily, it’d look something like this in ECMAScript 5 and new template syntax:
var Counter = ng
.Component({
selector: 'counter',
template: [
'<div class="todo">',
'<input type="text" [(ng-model)]="count">',
'<button type="button" (click)="decrement();">-</button>',
'<button type="button" (click)="increment();">+</button>',
'</div>'
].join('')
})
.Class({
constructor: function () {
this.count = 0;
},
increment: function () {
this.count++;
},
decrement: function () {
this.count--;
}
});
Source: Todd Motto