Write AngularJS like a pro. Angularjs Icon

Follow the ultimate AngularJS roadmap.

Digging into Angulars Controller as syntax

AngularJS Controllers have recently gone under some changes (version 1.2 to be precise). What this means for scopes, Controllers and Angular development is some very subtle but powerful changes. One of those changes I believe is improved architecture, clearer scoping and smarter Controllers.

Controllers as we know them are class-like Objects that drive Model and View changes, but they all seem to revolve around this mystical $scope Object. Angular Controllers have been pushed to change the way $scope is declared, with many developers suggesting using the this keyword instead of $scope.

Pre v1.2.0 Controllers looked similar to this:

// <div></div>
app.controller('MainCtrl', function ($scope) {
  $scope.title = 'Some title';
});

Here, the concept of the Controller is separate from the $scope itself, as we have to dependency inject it. Some argued this would’ve been better:

app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

We didn’t quite get there, but we got something pretty awesome in return.

Controllers as Classes

If you instantiate a “class” in JavaScript, you might do this:

var myClass = function () {
  this.title = 'Class title';
}
var myInstance = new myClass();

We can then use the myInstance instance to access myClass methods and properties. In Angular, we get the feel of proper instantiation with the new Controller as syntax. Here’s a quick look at declaring and binding:

// we declare as usual, just using the `this` Object instead of `$scope`
app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

This is more of a class based setup, and when instantiating a Controller in the DOM we get to instantiate against a variable:

<div ng-controller="MainCtrl as main">
  // MainCtrl doesn't exist, we get the `main` instance only
</div>

To reflect this.title in the DOM, we need to ride off our instance:

<div ng-controller="MainCtrl as main">
  {{ main.title }}
</div>

Namespacing the scopes is a great move I think, it cleans up Angular massively. I’ve always disliked the “floating variables” such as {{ title }}, I much prefer hitting the instance with {{ main.title }}.

Nested scopes

Nested scopes is where we see great return from the Controller as syntax, often we’ve had to use the current scope’s $parent property to scale back up scopes to get where we need.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

Take this for example:

<div ng-controller="MainCtrl">
  {{ title }}
  <div ng-controller="AnotherCtrl">
    {{ title }}
    <div ng-controller="YetAnotherCtrl">
      {{ title }}
    </div>
  </div>
</div>

Firstly, we’re going to get interpolation issues as {{ title }} will be very confusing to use and most likely one scope will take precedence over another. We also don’t know which one that might be. Whereas if we did this things are far clearer and variables can be accessed properly across scopes:

<div ng-controller="MainCtrl as main">
  {{ main.title }}
  <div ng-controller="AnotherCtrl as another">
    {{ another.title }}
    <div ng-controller="YetAnotherCtrl as yet">
      {{ yet.title }}
    </div>
  </div>
</div>

I can also access parent scopes without doing this:

<div ng-controller="MainCtrl">
  {{ title }}
  <div ng-controller="AnotherCtrl">
    Scope title: {{ title }}
    Parent title: {{ $parent.title }}
    <div ng-controller="YetAnotherCtrl">
      {{ title }}
      Parent title: {{ $parent.title }}
      Parent parent title: {{ $parent.$parent.title }}
    </div>
  </div>
</div>

And make things more logical:

<div ng-controller="MainCtrl as main">
  {{ main.title }}
  <div ng-controller="AnotherCtrl as another">
    Scope title: {{ another.title }}
    Parent title: {{ main.title }}
    <div ng-controller="YetAnotherCtrl as yet">
      Scope title: {{ yet.title }}
      Parent title: {{ another.title }}
      Parent parent title: {{ main.title }}
    </div>
  </div>
</div>

No hacky $parent calls. If a Controller’s position in the DOM/stack were to change, the position in sequential $parent.$parent.$parent.$parent may change! Accessing the scope lexically makes perfect sense.

$watchers/$scope methods

The first time I used the Controller as syntax I was like “yeah, awesome!”, but then to use scope watchers or methods (such as $watch, $broadcast, $on etc.) we need to dependency inject $scope. Gargh, this is what we tried so hard to get away from. But then I realised this was awesome.

The way the Controller as syntax works, is by binding the Controller to the current $scope rather than it being all one $scope-like class-like Object. For me, the key is the separation between the class and special Angular features.

This means I can have my pretty class-like Controller:

app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

When I need something above and beyond generic bindings, I introduce the magnificent $scope dependency to do something special, rather than ordinary.

Those special things include all the $scope methods, let’s look at an example:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  $scope.$on('someEventFiredFromElsewhere', function (event, data) {
    // do something!
  });
});

Ironing a quirk

Interestingly enough, whilst writing this I wanted to provide a $scope.$watch() example. Doing this usually is very simple, but using the Controller as syntax doesn’t work quite as expected:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // doesn't work!
  $scope.$watch('title', function (newVal, oldVal) {});
  // doesn't work!
  $scope.$watch('this.title', function (newVal, oldVal) {});
});

Uh oh! So what do we do? Interestingly enough I was reading the other day, and you can actually pass in a function as the first argument of a $watch():

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // hmmm, a function
  $scope.$watch(function () {}, function (newVal, oldVal) {});
});

Which means we can return our this.title reference:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // nearly there...
  $scope.$watch(function () {
    return this.title; // `this` isn't the `this` above!!
  }, function (newVal, oldVal) {});
});

Let’s change some execution context using angular.bind():

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // boom
  $scope.$watch(angular.bind(this, function () {
    return this.title; // `this` IS the `this` above!!
  }), function (newVal, oldVal) {
    // now we will pickup changes to newVal and oldVal
  });
});

Declaring in $routeProvider/Directives/elsewhere

Controllers can by dynamically assigned, we don’t need to always bind them via attributes. Inside Directives, we get a controllerAs: property, this is easily assigned:

app.directive('myDirective', function () {
  return {
    restrict: 'EA',
    replace: true,
    scope: true,
    template: [].join(''),
    controllerAs: '', // woohoo, nice and easy!
    controller: function () {}, // we'll instantiate this controller "as" the above name
    link: function () {}
  };
});

The same inside $routeProvider:

app.config(function ($routeProvider) {
  $routeProvider
  .when('/', {
    templateUrl: 'views/main.html',
    controllerAs: '',
    controller: ''
  })
  .otherwise({
    redirectTo: '/'
  });
});

Testing controllerAs syntax

There’s a subtle difference when testing controllerAs, and thankfully we no longer need to dependency inject $scope. This means we also don’t need to have a reference property when testing the Controller (such as vm.prop), we can simply use the variable name we assign $controller to.

// controller
angular
  .module('myModule')
  .controller('MainCtrl', MainCtrl);

function MainCtrl() {
  this.title = 'Some title';
};

// tests
describe('MainCtrl', function() {
  var MainController;

  beforeEach(function(){
    module('myModule');

    inject(function($controller) {
      MainController = $controller('MainCtrl');
    });
  });

  it('should expose title', function() {
    expect(MainController.title).equal('Some title');
  });
});

You can alternatively use the controllerAs syntax in the $controller instantiation but you will need to inject a $scope instance into the Object that is passed into $controller. The alias (for instance scope.main) for the Controller will be added to this $scope (like it is in our actual Angular apps), however this is a less elegant solution.

// Same test becomes
describe('MainCtrl', function() {
  var scope;

  beforeEach(function(){
    module('myModule');

    inject(function($controller, $rootScope) {
      scope = $rootScope.$new();
      var localInjections = {
        $scope: scope,
      };
      $controller('MainCtrl as main', localInjections);
    });
  });

  it('should expose title', function() {
    expect(scope.main.title).equal('Some title');
  });
});

Thank you for reading!

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover