Good Practices
#Banning ng-controller
When you start building apps with Angular using the ng-controller
[1] directive seems pretty straight forward. Just like in every other MVC-style application, controllers are the mediators between the model and view.
By adding ng-controller
to a DOM element, Angular will create a new child scope with the element as its root. Child scopes prototypally inherit methods and properties from their parent. Hence, there is no Shadow DOM[2] or other isolation. Deep scope hierachies will lead to semi-global data, which make it hard to maintain and reason about your code. Knowing from where data originates will be difficult.
Instead of polluting your templates with ng-controller
directives, rather make use of the component pattern. This pattern focuses on directives to achive seperation of concerns and high reusablity of code. The next chapter will give a thorough explaination of the component pattern.
#No $scope
Soup
With the introduction of the controllerAs
syntax in Angular 1.2 we finally can get rid of injecting $scope
in each and every controller. In fact, today it is a best practice to use the controllerAS
syntax throughout your app.[3]
#Controllers
As a result controllers can be written as a class
. Or, if you are still writing ES5, can be written with the constructor pattern.[4] In either case, writing controllers as “classes” allows us to only inject the $scope
service if it is really necessary. This lets us write regular JavaScript (e.g. use this
) and controllers are much more lightweight. The following code examples show how controllers should be initialised:
// ES5
function SomeController () {
this.title = 'Some Controller Tittle';
}
angular
.module('app', [])
.controller('SomeController', SomeController);
// ES2015
class AnotherController () {
constructor() {
this.title = 'Some Controller Tittle';
}
}
angular
.module('app', [])
.controller('AnotherController', AnotherController);
#Directives
Before Angular 1.3 the controllerAs
syntax only worked well with controllers that are used with a ng-controller
directive. Using the syntax in your own directive was a real pain because the bindings didn’t work like you would have expected. To illustrate the problem consider the following directive:
function HelloDirectiveConstructor () {
// Directive definition object
return {
restrict: 'E',
template: 'Hello {{ vm.to }}!',
scope: {
to: '='
},
controllerAs: 'vm',
controller: HelloDirectiveController
};
}
function HelloDirectiveController ( $scope ) {
this.to = $scope.to;
}
angular
.module('app', [])
.directive('sayHello', HelloDirectiveConstructor);
As you might already see, even though we used controllerAs
syntax and wrote the directive as a class-like object, using $scope
was still necessary. In addition, we messed up the two-way binding. If we want to update this.to
whenever the to
attribute binding changes a $watch
is required.
Fortunately the directive definition object was extended in Angular 1.3 with a new property called bindToController
. When set to true
in a directive that has an isolated scope and uses controllerAs
, all properties are bound to the controller rather than the $scope
.[5]
In Angular 1.4 the API was improved further. Instead of specifying bound properties in the scope
definition, you can now put all binding directly into bindToController
. Furthermore, starting with Angular 1.4 using bindToController
is also allowed on directives that introduce a new scope.[6]
The HelloDirectiveConstructor
should be refactored like this:
function HelloDirectiveConstructor () {
return {
restrict: 'E',
template: 'Hello {{ vm.to }}!',
scope: {},
controllerAs: 'vm',
controller: HelloDirectiveController,
bindToController: {
to: '='
}
};
}
#The “ViewModel” Convention
Because of John Papa’s infamous styleguide[7] (and rightfully so) the Angular community started to use a consitent naming convention for aliasing controllers: vm
also known as the “ViewModel”. The reason to use a dedicated variable instead of the this
keyword is alawys the same (in JavaScript). This is contextual[8] and functions inside your controllers will create a new context, hence this
inside a function will not be the same as the this
of your controller. The only expection for this is of courese if you .bind
your function.[9]
Using vm
will also make it very clear what members are exported and can be accessed from outside the controller. You should always list exported variables and methods at the top of a controller and place the logic below:
function SomeController () {
let vm = this;
// Exports
vm.title = 'Some Controller';
vm.list = [];
vm.someFunction = someFunction;
// Here come the controller logic
function someFunction () { ... }
}
So make yourself a favor and always use vm
to reference your controller’s context. Because it commonly used by Angular developers it will be easier for other people to reason about your code.
#Use controllers for directive logic
Looking at $compile
[10] you have a lot of options to define the logic for your directive. Namely the compile
, link
or controller
methods. If you want to generate a dynamic template even using the template
property together with a function may have its merits.
But most of the time creating a controller will suffice and since Angular 1.3 binding properties to a directive’s controller is very convenient (see above). Only in a few cases you have to use other options. An example for this is the ngModelController
, which registers itself in the preLink
phase with a parent ngFormController
.[11]
An important benefit of putting all logic in a controller is testability. It is very easy to isolate and thus thoroughly test a controller because you have full control over the injected arguments. In order to test compile
or link
methods you either have to setup the whole directive or need a hold of the full directive definition object. The later can be done by injecting the directive as a dependency in your test suite.
Testing the compile
or link
of the <say-hello>
with Jasmine[12] controller can be done like this:
define('<say-hello>', () => {
var ddo;
beforeEach(module('app'));
beforeEach(inject( ( 'sayHelloDirective' ) => {
ddo = sayHelloDirective[0]; // Array of "sayHello` directives
});
it('should have a linkt function', () => {
expect(ddo.link).toEqual(jasmine.any(Function));
});
});
#Slim and focused Controllers
In order to keep your code DRY[13] you should defer application logic into services. This way you have reusable pieces and your controllers are slim, focused and maintanable.[14]
A common case for this is fetching data from the server. Instead of using the $http
services inside a controller the implementation details should be hidden away in a seperate service. This is especially true if the JSON response needs some additional transformations before you can use it as model.
Recall the MarvelApiService
from the TypeScript section. When you request data from the Marvel API you will get a lot of additional information besides the “real” data you requested. Using the whole JSON request as model may not be what you want and if you do not have a dedicated service, which does the transformations for you, parsing the JSON has to be done in every single controller you request data from the API.
The following code snippets shows how a controller should not be build and what a good alternative could look like:
// Bad
class MarvelController ( $http, MARVEL_API_URL ) {
var vm = this;
// Exports
vm.character_list = [];
vm.fetchCharacterList = fetchCharacterList;
function fetchCharacterList () {
let url = MARVEL_API_URL.base + MARVEL_API_URL.characters
.replace(/:id$/, '');
return $http.get(url)
.then( (reponse) => {
// Not kidding, that's the property
// with the list of characters.
vm.character_list = reponse.data.data.results;
})
.catch( (err) => {
// Handle error
});
}
}
// Good
class MarvelController ( MarvelApiService ) {
var vm = this;
// Exports
vm.character_list = [];
vm.fetchCharacterList = fetchCharacterList;
function fetchCharacterList () {
return MarvelApiService.getCharacters()
.then( (list) => {
vm.character_list = list;
});
// Error is handled by the service
}
}
As you can see, the controller not only has to deal with the server response, but also using the correct URL and handling any error that may occurs during the API call. Whereas in the second example only the MarvelApiService
has to be injected. The controller does not care about the exact URL of the endpoint. Also, any errors that occur will be handled by the service.
In addition, if you move complex logic into a service it is much easier to adjust your code to any (third-party) API changes. The alternative would be to change every controller that uses the API. A very error-prone task if you have a large application with many calls to that API.