Table of Contents Preface typescript angular

Good Practices

Banning ng-controller

When you start build­ing apps with An­gu­lar us­ing the ng-controller[1] di­rec­tive seems pretty straight for­ward. Just like in every other MVC-style ap­pli­ca­tion, con­trollers are the me­di­a­tors be­tween the model and view.

By adding ng-controller to a DOM el­e­ment, An­gu­lar will cre­ate a new child scope with the el­e­ment as its root. Child scopes pro­to­typ­ally in­herit meth­ods and prop­er­ties from their par­ent. Hence, there is no Shadow DOM[2] or other iso­la­tion. Deep scope hi­er­achies will lead to semi-global data, which make it hard to main­tain and rea­son about your code. Know­ing from where data orig­i­nates will be dif­fi­cult.

In­stead of pol­lut­ing your tem­plates with ng-controller di­rec­tives, rather make use of the component pattern. This pat­tern fo­cuses on di­rec­tives to achive seper­a­tion of con­cerns and high reusablity of code. The next chap­ter will give a thor­ough ex­plaina­tion of the component pattern.

No $scope Soup

With the in­tro­duc­tion of the controllerAs syn­tax in An­gu­lar 1.2 we fi­nally can get rid of in­ject­ing $scope in each and every con­troller. In fact, to­day it is a best prac­tice to use the controllerAS syn­tax through­out your app.[3]

Controllers

As a re­sult con­trollers can be writ­ten as a class. Or, if you are still writ­ing ES5, can be writ­ten with the con­struc­tor pat­tern.[4] In ei­ther case, writ­ing con­trollers as “classes” al­lows us to only in­ject the $scope ser­vice if it is re­ally nec­es­sary. This lets us write reg­u­lar JavaScript (e.g. use this) and con­trollers are much more light­weight. The fol­low­ing code ex­am­ples show how con­trollers should be ini­tialised:

// 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

Be­fore An­gu­lar 1.3 the controllerAs syn­tax only worked well with con­trollers that are used with a ng-controller di­rec­tive. Us­ing the syn­tax in your own di­rec­tive was a real pain be­cause the bind­ings did­n’t work like you would have ex­pected. To il­lus­trate the prob­lem con­sider the fol­low­ing di­rec­tive:

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 al­ready see, even though we used controllerAs syn­tax and wrote the di­rec­tive as a class-like ob­ject, us­ing $scope was still nec­es­sary. In ad­di­tion, we messed up the two-way bind­ing. If we want to up­date this.to when­ever the to at­tribute bind­ing changes a $watch is re­quired.

For­tu­nately the di­rec­tive de­f­i­n­i­tion ob­ject was ex­tended in An­gu­lar 1.3 with a new prop­erty called bindToController. When set to true in a di­rec­tive that has an iso­lated scope and uses controllerAs, all prop­er­ties are bound to the con­troller rather than the $scope.[5]

In An­gu­lar 1.4 the API was im­proved fur­ther. In­stead of spec­i­fy­ing bound prop­er­ties in the scope de­f­i­n­i­tion, you can now put all bind­ing di­rectly into bindToController. Fur­ther­more, start­ing with An­gu­lar 1.4 us­ing bindToController is also al­lowed on di­rec­tives that in­tro­duce a new scope.[6]

The HelloDirectiveConstructor should be refac­tored like this:

function HelloDirectiveConstructor () {
    return {
        restrict: 'E',
        template: 'Hello {{ vm.to }}!',
        scope: {},
        controllerAs: 'vm',
        controller: HelloDirectiveController,
        bindToController: {
            to: '='
        }
    };
}

The “ViewModel” Convention

Be­cause of John Pa­pa’s in­fa­mous styleguide[7] (and right­fully so) the An­gu­lar com­mu­nity started to use a con­si­tent nam­ing con­ven­tion for alias­ing con­trollers: vm also known as the “View­Model”. The rea­son to use a ded­i­cated vari­able in­stead of the this key­word is alawys the same (in JavaScript). This is con­tex­tual[8] and func­tions in­side your con­trollers will cre­ate a new con­text, hence this in­side a func­tion will not be the same as the this of your con­troller. The only ex­pec­tion for this is of courese if you .bind your func­tion.[9]

Us­ing vm will also make it very clear what mem­bers are ex­ported and can be ac­cessed from out­side the con­troller. You should al­ways list ex­ported vari­ables and meth­ods at the top of a con­troller and place the logic be­low:

function SomeController () {
    let vm = this;

    // Exports
    vm.title = 'Some Controller';
    vm.list = [];

    vm.someFunction = someFunction;

    // Here come the controller logic
    function someFunction () { ... }
}

So make your­self a fa­vor and al­ways use vm to ref­er­ence your con­troller’s con­text. Be­cause it com­monly used by An­gu­lar de­vel­op­ers it will be eas­ier for other peo­ple to rea­son about your code.

Use controllers for directive logic

Look­ing at $compile[10] you have a lot of op­tions to de­fine the logic for your di­rec­tive. Namely the compile, link or controller meth­ods. If you want to gen­er­ate a dy­namic tem­plate even us­ing the template prop­erty to­gether with a func­tion may have its mer­its.

But most of the time cre­at­ing a con­troller will suf­fice and since An­gu­lar 1.3 bind­ing prop­er­ties to a di­rec­tive’s con­troller is very con­ve­nient (see above). Only in a few cases you have to use other op­tions. An ex­am­ple for this is the ngModelController, which reg­is­ters it­self in the preLink phase with a par­ent ngFormController.[11]

An im­por­tant ben­e­fit of putting all logic in a con­troller is testa­bil­ity. It is very easy to iso­late and thus thor­oughly test a con­troller be­cause you have full con­trol over the in­jected ar­gu­ments. In or­der to test compile or link meth­ods you ei­ther have to setup the whole di­rec­tive or need a hold of the full di­rec­tive de­f­i­n­i­tion ob­ject. The later can be done by in­ject­ing the di­rec­tive as a de­pen­dency in your test suite.

Test­ing the compile or link of the <say-hello> with Jas­mine[12] con­troller 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 or­der to keep your code DRY[13] you should de­fer ap­pli­ca­tion logic into ser­vices. This way you have reusable pieces and your con­trollers are slim, fo­cused and main­tan­able.[14]

A com­mon case for this is fetch­ing data from the server. In­stead of us­ing the $http ser­vices in­side a con­troller the im­ple­men­ta­tion de­tails should be hid­den away in a seper­ate ser­vice. This is es­pe­cially true if the JSON re­sponse needs some ad­di­tional trans­for­ma­tions be­fore you can use it as model.

Re­call the MarvelApiService from the Type­Script sec­tion. When you re­quest data from the Mar­vel API you will get a lot of ad­di­tional in­for­ma­tion be­sides the “real” data you re­quested. Us­ing the whole JSON re­quest as model may not be what you want and if you do not have a ded­i­cated ser­vice, which does the trans­for­ma­tions for you, pars­ing the JSON has to be done in every sin­gle con­troller you re­quest data from the API.

The fol­low­ing code snip­pets shows how a con­troller 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 con­troller not only has to deal with the server re­sponse, but also us­ing the cor­rect URL and han­dling any er­ror that may oc­curs dur­ing the API call. Whereas in the sec­ond ex­am­ple only the MarvelApiService has to be in­jected. The con­troller does not care about the ex­act URL of the end­point. Also, any er­rors that oc­cur will be han­dled by the ser­vice.

In ad­di­tion, if you move com­plex logic into a ser­vice it is much eas­ier to ad­just your code to any (third-party) API changes. The al­ter­na­tive would be to change every con­troller that uses the API. A very er­ror-prone task if you have a large ap­pli­ca­tion with many calls to that API.


  1. Angular Docs: ng-controller

  2. MDN - Shadow DOM

  3. Pascal Precht - Exploring Angular 1.3: Binding to Directive Controllers

  4. Addy Osmani - Learning JavaScript Design Patterns

  5. Todd Motto - No $scope soup, bindToController in AngularJS

  6. Angular Commit: bindToController

  7. John Papa - Angular Style Guide

  8. MDN - this

  9. MDN - Function.prototype.bind()

  10. Angular Documentation - $compile

  11. Preparing for Angular 2

  12. Jasmine - Behaviour-Driven JavaScript

  13. Wikipedia - Don't repeat yourself

  14. John Papa - Angular Styleguide: Defer Controller Logic to Services