Angular is a very powerful framework, sometimes too powerful causing some developers to make some architecture mistakes. The two-way data binding and the power of directives are awesome, but you need to think about what are you doing and try to use some best practices to avoid common pitfalls during the development process.
Controllers are class-like objects to “control” the model and update the view, and as you know everything is based around the magic and mystic $scope
property.
A good practice is to avoid binding everything to $scope
, because too many bindings crowd the watch list of the $digest
loop. To avoid that, Angular give us the controllerAs
property.
Writing controllers as classes
A class in Javascript (at least in ES5 for now) is something like this:
var aClass = function () {
this.name = 'Class name';
};
var instance = new aClass();
With this you can use the instance
variable to access methods and properties.
Using the controllerAs
property we write our Controllers in the same way, using this
instead of $scope
angular.module('myApp')
.controller('MyCtrl', function () {
this.name = 'Controller Name';
});
Now this can be instantiated in the template with something like the following:
<div ng-controller="MyCtrl as vm">
<h1 ng-bind="vm.name"></h1>
</div>
To access the properties and methods of the controller you use the vm
instance.
With this you are namespacing the scopes, making the code cleaner and readable. Think about nested scopes.
<div ng-controller="BaseCtrl">
{{ name }}
<div ng-controller="SectionCtrl">
{{ name }}
<div ng-controller="FinalCtrl">
{{ name }}
</div>
</div>
</div>
Here you can see that each controller is accessing the name
property, but the question is: which one? That code looks very confusing and is probably that one controller takes precedence over another, but you don’t know which one.
Using the controllerAs
syntax this will be much cleaner:
<div ng-controller="BaseCtrl as base">
Base scope: {{ base.name }}
<div ng-controller="SectionCtrl as section">
Section scope: {{ section.name }}
Base scope: {{base.name}}
<div ng-controller="FinalCtrl as final">
{{ final.name }}
</div>
</div>
</div>
As we can see in the above code, using controllerAs
syntax allows us access to parent scopes without the hassle of scope collision and without using $parent
to access it.
How to set watchers
One question that comes to mind when you use this kind of syntax is how to use a $watch
call because you need to inject $scope
. We fight to remove the use of $scope
, and now we need to inject it anyway.
Well, we can keep using controllerAs
and keep binding methods and properties to the this
object that is binded to the current $scope
. At the same time, we can keep the separation of concerns using $scope
only for special cases, like $watch
, $on
, or $broadcast
.
Keep in mind that using controllerAs
the syntax for $watch
method changes a little bit. Normally you would do something like the following:
app.controller('Ctrl', function ($scope) {
$scope.name = 'name';
$scope.$watch('name', function (newVal, oldVal) {
});
});
But that doesn’t work now, because $watch
is looking for the watched property inside the $scope
, and you don’t directly bind that property to $scope
. Instead watched property is binded to this
. The correct way to do it now is as shown in the following example:
app.controller('Ctrl', function ($scope) {
this.name = 'name';
$scope.$watch(function () {
return this.title
}.bind(this), function (newVal, oldVal) {
});
});
Alternative is using angular.bind
:
app.controller('Ctrl', function ($scope) {
this.name = 'name';
$scope.$watch(angular.bind(function () {
return this.title
}), function (newVal, oldVal) {
});
});
How can I declare controllerAs
without using the DOM attributes?
In the case of directives, you have the controllerAs
property inside the directive signature:
app.directive('Directive', function () {
return {
restrict: 'EA',
templateUrl: 'template.html',
scope: true,
controller: function () {},
controllerAs: 'vm'
}
});
Or for controllers in the $routeProvider
:
app.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'main.html',
controllerAs: 'main',
controller: 'MainCtrl'
})
});