Kill $scope - Replace it with controllerAs

Dave Ceddia bio photo By Dave Ceddia Comment

You’re chugging along great with your Angular app, following John Papa’s Style Guide like you’ve heard you should be. Then you run into a problem.

You found a solution on some blog but oh crap! It’s using $scope all over the place! How can you pull it into your code without polluting your well-styled masterpiece?

Here we’ll cover a couple quick things you can do to transform a $scope’d mess into a sparkling paragon of virtuous code.

$scope becomes controllerAs

Start with a controller using $scope:

angular.controller('AppCtrl', AppCtrl);

//  1: $scope is injected
function AppCtrl($scope) {
  //  2: $scope is used to pass data to/from the view
  $scope.name = "Bob";
}
<div ng-controller="AppCtrl">
  Hello {{ name }}
</div>
Transform it!
  1. [controller] Add vm = this at the top.
  2. [controller] Find/replace $scope with vm.
  3. [view] Add as someName to any ng-controllers.
  4. [view] Prepend someName. to all variables.

Here’s that example again, fixed up to use controllerAs:

angular.controller('AppCtrl', AppCtrl);

//  1: $scope is not injected
function AppCtrl() {
  // 2: The controller itself is now exposed to the view
  //    Give it a name ('vm' or 'ctrl' is common)
  var vm = this;

  //  3: Find/Replace "$scope" with "vm"
  vm.name = "Bob";
}
<!-- Add 'as app' to the controller -->
<div ng-controller="AppCtrl as app">
  <!-- Prefix variables with 'app.' -->
  Hello {{ app.name }}<br>
</div>

Notice that the view refers to the controller as “app”, while the controller refers to itself as “vm”. These names don’t affect each other.

Using $watch with controllerAs

What if you need to $watch or $broadcast from the controller? You can’t do it without $scope!

This is ok though – think of $scope as a service in this case. It’s giving you access to special behavior. You won’t use it to pass data to/from the view.

Here’s it is with $scope:

angular.controller('AppCtrl', AppCtrl);

// 1: $scope is injected
function AppCtrl($scope, nameValidator) {
  // 2: $scope passes data to view
  $scope.name = "Bob";

  // 3: $watch is setup on $scope variable
  $scope.changeCount = 0;
  $scope.$watch('name', function(newValue, oldValue) {
    $scope.changeCount++;
  });
}
<div ng-controller="AppCtrl as app">
  Hello {{ name }}<br>
  (changed {{ changeCount }} times).
</div>
Transform it!

(only step 3 is new from before)

  1. [controller] Add vm = this at the top.
  2. [controller] Find/replace $scope with vm.
  3. [controller] Prefix watched vars with the controller name from the view. (app. in this case)
  4. [view] Add as someName to any ng-controllers.
  5. [view] Prepend someName. to all variables.

Here’s the controllerAs version:

angular.controller('AppCtrl', AppCtrl);

// 1: $scope is still injected (for $watch)
function AppCtrl($scope, nameValidator) {
  var vm = this;

  // 2: Use 'vm' instead.
  vm.name = "Bob";

  // 3: 'name' becomes 'app.name'
  //    (because in the view, this controller is called 'app')
  vm.changeCount = 0;
  $scope.$watch('app.name', function(newValue, oldValue) {
    vm.changeCount++;
  });
}
<!-- Add 'as app' to the controller -->
<div ng-controller="AppCtrl as app">
  <!-- Prefix variables with 'app.' -->
  Hello {{ app.name }}<br>
  (changed {{ app.changeCount }} times).
</div>

Now you know how to mechanically transform tutorials that use $scope into cleaner code that uses controllerAs!

So $scope is vanquished now, right?

Well, not quite. $scope never truly dies. Read on…

Behind The Scenes

Here’s what the $scope hierarchy looks like normally:

Variable access using plain $scope

When you refer to users in your view, Angular looks on $scope for it. If it’s not there, it will look to the prototypical parent, which is $rootScope.

If there were any intermediate $parent scopes, it would check those before checking $rootScope.

It’s a plain old JavaScript prototypical inheritance tree: Check children first, then walk up the tree until the variable is found.

Here’s that same variable nested under a controllerAs-style controller named ctrl:

Variable access using controllerAs

You write UserCtrl as ctrl in your view and Angular inserts the controller itself onto $scope, as $scope.ctrl. What was previously just user is now ctrl.user.

This is why, when setting up a $watch, “name” became “app.name” – everything is still a descendant of $scope, but variables are now nested inside a named controller.

Clarity at last

Hopefully this cleared up some confusion around using tutorials out on the web.

As always, the best way to internalize this stuff is to practice.

Remember: There are no shortcuts, ONLY ZUUL! I mean practice. Only practice.

Choosing a framework is difficult.

Learning React? Sign up and get my React Learning Timeline PDF, and also weekly-ish articles about React, Angular, and JavaScript.

A few emails per month — unsubscribe any time.
comments powered by Disqus