Bec d'état - Rebecca Scott

Blog | Links | Archive
About | Resume | Advisor profile | Projects | Contact


~/Controller scope in Angular JS directives

28 Jun 2015

This is probably basic level Angular JS but I haven’t seen it mentioned anywhere. I’m probably missing something fundamental about directive scope.

Say you’ve got this directive (JSFiddle):

angular
    .module('app', [])
    .directive('thing', function() {
        return {
            restrict: 'E',
            replace: true,
            template: '<div><input ng-model="vm.name"/> Name: </div>',
            controller: function() {
                this.name = '';
            },
            controllerAs: 'vm'
        };
    });

Using it once works great:

<div ng-app="app">
	<thing></thing>
</div>

But if you use the directive multiple times, it becomes clear that the directive views all share the same controller:

<div ng-app="app">
    <thing></thing>
    <thing></thing>
    <thing></thing>
    <thing></thing>
    <thing></thing>
</div>

Typing in the first textbox affects all of the other directive views, ie. they are all pointing to the same controller.

In fact, if you have different directives with the same controllerAs value, you can see that the vm instance for each directive is set to the last directive’s controller (JSFiddle):

angular
    .module('app', [])
    .directive('firstDirective', function() {
        return {
            restrict: 'E',
            replace: true,
            template: '<div>first directive: <pre></pre></div>',
            controller: function() {
                this.foo = 'Hi!';
            },
            controllerAs: 'vm'
        };
    })
    .directive('secondDirective', function(){
        return {
            restrict: 'E',
            replace: true,
            template: '<div>second directive: <pre></pre></div>',
            controller: function() {
                this.bar = 'There?';
            },
            controllerAs: 'vm'
        };
    });

<div ng-app="app">
	<first-directive></first-directive>
	<second-directive></second-directive>
</div>

If you change the name of the controllerAs alias - say to firstDirectiveVm and secondDirectiveVm - then the problem goes away, so Angular JS by default is setting vm globally each time a directive uses controllerAs: 'vm', and going down the page, meaning the last vm wins. This can obviously be a pretty tricky problem to diagnose. Besides which, this workaround of changing each directive’s controllerAs value won’t work for multiple directives of the same type.

The solution is to set scope to true in the directive declaration (JSFiddle):

angular
    .module('app', [])
    .directive('thing', function() {
        return {
            restrict: 'E',
            replace: true,
            template: '<div><input ng-model="vm.name"/> Name: </div>',
            controller: function() {
                this.name = '';
            },
            controllerAs: 'vm',
            scope: true
        };
    });

A lot more can happen in that scope value than setting it to true. See the Angular JS docs for isolating directive scope for examples. Unfortunately, ‘scope’ seems to be an overloaded term in Angular JS world. This kind of ‘scope’ is talking about the scope of the element and attributes provided by the directive, in a way distinct from $scope, which is what I’m trying to avoid by using controllerAs in the first place.

It seems strange to me that shared scope is the default, and that you need to set scope to a non-falsy value to opt out of that. I’m sure I’m missing a lot of nuance around the reasons. In any case, setting scope: true seems to be the happy path.

I just wish I hadn’t wasted a full day rewriting an entire site before figuring out what was happening.

:-(