ndsena
Last Updated: February 25, 2016
·
828
· alextercete

Testable Controller Activate in AngularJS

I've adopted John Papa's AngularJS style guide recently, and it's been working great for me so far. One of its rules I don't like is the Controller Activation Promises:

function AvengersViewModel(dataService) {
    var viewModel = this;

    viewModel.avengers = [];
    viewModel.save = save;

    activate();

    function activate() {
        return dataService.getAvengers().then(function (data) {
            viewModel.avengers = data;
            return viewModel.avengers;
        });
    }

    function save() {
        // (...)
    }
}

I like the fact that the activation code is in its own function, but invoking it straight away is pretty much the same as having logic in the constructor. This means that if you want to unit test the method save you'll need to make sure that dataService.getAvengers is stubbed properly.

I wanted to expose activate and have it called by someone else rather than the AvengersViewModel:

function AvengersViewModel(dataService) {
    var viewModel = this;

    // (...)

    // Expose it
    viewModel.activate = activate;

    function activate() {
        // (...)
    }

    // (...)
}

But who could that someone be?

The naive approach

I knew about ngInit, but I also knew I was not supposed to use it. That didn't stop me from trying:

function avengers() {
    return {
        scope: {},
        template: '<div ng-init="viewModel.activate()">The avengers directive</div>',
        controller: 'AvengersViewModel',
        controllerAs: 'viewModel'
    };
}

That worked, but it didn't look right.

A better approach

I remembered seeing someone using require to make the directive's own controller available as the fourth parameter of the link function. Apparently, you can even omit the require property altogether, and you'll have access to the directive's controller instance:

function avengers() {
    return {
        scope: {},
        template: '<div>The avengers directive</div>',
        controller: 'AvengersViewModel',
        controllerAs: 'viewModel',
        link: function (scope, element, attributes, viewModel) {
            viewModel.activate();
        }
    };
}

Now I can properly unit test my view models and have the benefits of Controller Activation Promises. Awesome!