Angularjs

Binding to Directive Controllers in Angular 1.3

In version 1.2, Angular introduced a new controllerAs syntax that made scoping clearer and controllers smarter. In fact, it’s a best practice to use controllerAs throughout our Angular apps in order to prevent some common problems that developers run into fairly often.

Even if it’s nice that we are able to use that syntax in pretty much every case where a controller comes into play, it turned out that in version 1.2, there’s a little quirk when using it with directives that have an isolated scope. But before we get to the actual problem, let’s recap controllerAs in general first, to get an idea of what problems it solves and how to use it, so that we are all on the same page.

controllerAs as Namespace

Who of us did not run into the problem that, when having nested scopes created by nested controllers, scope properties that have the same name as properties on the parent scope, shadow that value of the parent scope property due to JavaScript’s prototypical inheritance model?

Or, when speaking in code, having two controllers like this:

function ControllerOne($scope) {
  $scope.foo = 'Pascal';
}

function ControllerTwo($scope) {
  $scope.foo = 'Christoph';
}

app.controller('ControllerOne', ControllerOne);
app.controller('ControllerTwo', ControllerTwo);

And a DOM structure like this:

<div ng-controller="ControllerOne">
  {{foo}}
  <div ng-controller="ControllerTwo">
    {{foo}}
  </div>
</div>

The {% raw %} {{foo}} {% endraw %} expression in ControllerTwo scope will shadow the {% raw %} {{foo}} {% endraw %} expression in ControllerOne scope, which results in string Christoph being displayed in the inner scope and Pascal being displayed in the outer scope.

We could always get around this problem by using a scope’s $parent property to reference its parent scope when accessing scope properties like this:

<div ng-controller="ControllerOne">
  {{foo}}
  <div ng-controller="ControllerTwo">
    {{$parent.foo}}
  </div>
</div>

However, it turns out that using $parent is actually a bad practice, since we start coupling our expression code to the underlying DOM structure, which makes our code less maintainable. Just imagine you have not only two nested scopes, but four or five. That would bring you into $parent.$parent.$parent.$parent hell, right?

That’s one of the reasons why you might have heard that we should always have a dot in our expressions that access scope properties. In other words, this could easily be fixed with doing the following:

function ControllerOne($scope) {
  $scope.modelOne = {
    foo: 'Pascal'
  };
}

function ControllerTwo($scope) {
  $scope.modelTwo = {
    foo: 'Christoph'
  };
}

And in our template, we update our expressions accordingly:

<div ng-controller="ControllerOne">
  {{modelOne.foo}}
  <div ng-controller="ControllerTwo">
    {{modelOne.foo}}
  </div>
</div>

And here comes controllerAs into play. This syntax allows us to introduce a new namespace bound to our controller without the need to put scope properties in an additional object literal. In fact, we don’t even need to request $scope in our controller anymore, since the scope is bound the controller’s this reference when using controllerAs.

Let’s see what that looks like in code. First we remove the $scope service and assign our values to this:

function ControllerOne() {
  this.foo = 'Pascal';
}

function ControllerTwo() {
  this.foo = 'Christoph';
}

Next, we update ngController directive expression with the controllerAs syntax, and use the new namespaces in our scopes:

<div ng-controller="ControllerOne as ctrl1">
  {{ctrl1.foo}}
  <div ng-controller="ControllerTwo as ctrl2">
    {{ctrl2.foo}}
  </div>
</div>

It gets even better. We are able to use that syntax whenever a controller is used. For example if we configure an application state with Angular’s $routeProvider we can use controllerAs there too, in order to make our template code more readable.

$routeProvider.when('/', {
  templateUrl: 'stateTemplate.html',
  controllerAs: 'ctrl',
  controller: 'StateController'
});

And as you probably know, directives can also have controllers and yes, we can use controllerAs there too.

app.controller('SomeController', function () {
  this.foo = 'bar';
});

app.directive('someDirective', function () {
  return {
    restrict: 'A',
    controller: 'SomeController',
    controllerAs: 'ctrl',
    template: '{{ctrl.foo}}'
  };
});

Great. Now we know what the controllerAs syntax it is all about, but we haven’t talked about the little drawback that it comes with in 1.2. Let’s move on with that one.

The problem with controllerAs in Directives

We said that, when using controllerAs, the controllers’ scope is bound to the controllers’ this object, so in other words - this represents our scope. But how does that work when building a directive with isolated scope?

We know we can create an isolated scope by adding an object literal to our directive definition object that defines how each scope property is bound to our directive. To refresh our memory, here’s what we can do:

app.directive('someDirective', function () {
  return {
    scope: {
      oneWay: '@',
      twoWay: '=',
      expr: '&'
    }
  };
});

This is a directive with an isolated scope that defines how its scope properties are bound. Alright, let’s say we have directive with an isolated scope, a controller and a template that uses the controller properties accordingly:

app.directive('someDirective', function () {
  return {
    scope: {},
    controller: function () {
      this.name = 'Pascal'
    },
    controllerAs: 'ctrl',
    template: '<div>{{ctrl.name}}</div>'
  };
});

Easy. That works and we knew that already. Now to the tricky part: what if name should be two-way bound?

app.directive('someDirective', function () {
  return {
    scope: {
      name: '='
    },
    // ...
  };
});

Changes to isolated scope properties from the outside world are not reflected back to the controllers’ this object. What we need to do to make this work in 1.2, is to use the $scope service to re-assign our scope values explicitly, whenever a change happens on a particular property. And of course, we mustn’t forget to bind our watch callback to the controllers’ this:

app.directive('someDirective', function () {
  return {
    scope: {
      name: '='
    },
    controller: function ($scope) {
      this.name = 'Pascal';

      $scope.$watch('name', function (newValue) {
        this.name = newValue;
      }.bind(this));
    },
    // ...
  };
});

Here we go… the $scope service we initially got rid off is now back. If you now think this is crazy, especially when considering that this is just one scope property and in a real world directive you usually have more than one, then my friend, I agree with you.

Luckily, this is no longer a problem in Angular 1.3!

Binding to controllers with bindToController

Angular 1.3 introduces a new property to the directive definition object called bindToController, which does exactly what it says. When set to true in a directive with isolated scope that uses controllerAs, the component’s properties are bound to the controller rather than to the scope.

That means, Angular makes sure that, when the controller is instantiated, the initial values of the isolated scope bindings are available on this, and future changes are also automatically available.

Let’s apply bindToController to our directive and see how the code becomes cleaner.

app.directive('someDirective', function () {
  return {
    scope: {
      name: '='
    },
    controller: function () {
      this.name = 'Pascal';
    },
    controllerAs: 'ctrl',
    bindToController: true,
    template: '<div>{{ctrl.name}}</div>'
  };
});

As we can see, we don’t need $scope anymore (yay!) and there’s also no link nor compile function in this directive definition. At the same time we keep taking advantage of controllerAs.

Improvements in 1.4

In version 1.4, bindToController gets even more powerful. When having an isolated scope with properties to be bound to a controller, we always define those properties on the scope definition and bindToController is set to true. In 1.4 however, we can move all our property binding definitions to bindToController and make it an object literal.

Here’s an example with a component directive that uses bindToController. Instead of defining the scope properties on scope, we declaratively define what properties are bound to the component’s controller:

app.directive('someDirective', function () {
  return {
    scope: {},
    bindToController: {
      someObject: '=',
      someString: '@',
      someExpr: '&'
    }
    controller: function () {
      this.name = 'Pascal';
    },
    controllerAs: 'ctrl',
    template: '<div>{{ctrl.name}}</div>'
  };
});

In addition to that, bindToController is no longer exclusive to isolated scope directives! Whenever we build a directive that introduces a new scope, we can take advantage of bindToController. So the following code also works:

app.directive('someDirective', function () {
  return {
    scope: true
    bindToController: {
      someObject: '=',
      someString: '@',
      someExpr: '&'
    },
    ...
  };
});
Written by  Author

Pascal Precht