AngularJS: "Controller as" or "$scope"?

I just finished reading a blog post by John Papa. He talks about the trend of using Controller as someName instead of injecting $scope into your controller. I wanted to expound on his point, but first let's demonstrate this technique for those of you who haven't heard of it yet. It's a pretty simple feature, but I think it has even more useful implications than what John covered.

Traditionally you're probably used to doing something like this:

<div ng-controller="MainController">  
  {{ someObj.someProp }}
</div>  
app.controller('MainController', function ($scope) {  
  $scope.someObj = {
    someProp: 'Some value.'
  };
});

With the new Controller as technique you can now do something like this:

<div ng-controller="MainController as main">  
  {{ main.someProp }}
</div>  
app.controller('MainController', function () {  
  this.someProp = 'Some value.'
});

John Papa points out in his post that the only real difference between the two is preference. He talks about the Controller as technique as "syntactic sugar" that you can use if you want to. I mostly agree with him, but I also think this solves some of the more complicated problems I've run into with Angular before.

One such problem has to do with the way scopes inherit prototypically. In JavaScript when one object inherits from another prototypically, you are able to access all the properties and methods from the parent object.

var obj1 = {  
  someProp: 'obj1 property!',
  someMethod: function () {
    alert('obj1 method!');
  }
};
var obj2 = Object.create(obj1);  
obj2.someProp = 'obj2 property!';  

You might think from the above code that someProp was "changed" from "obj1 property!" to "obj2 property!" when obj2 was instantiated. However, you can't change properties on an object's prototype like that. All that code did was create a property called someProp on obj2 that masks the value of the underlying someProp on obj1. If you run delete obj2.someProp then someProp won't be gone, it will revert to showing the value of obj1.someProp. This is the way nested scopes work. Setting a property on a child scope does not change the property with the same name on the parent scope; it merely hides it.

There's a good reason why my example of the classic $scope technique assigns someProp to a new object called someObj. If my parent scope has a property called foo and I want to change it on my child scope, then foo must be a property on an object on the parent scope. Since objects are passed by reference, changing a property on an object attached to the parent scope actually does modify that object's property; it's only the property representing the object itself that we would end up masking if we set it on our child scope.

Here's an example of what happens when you nest controllers and use scalar values on the $scope.

<div ng-controller="ParentController">  
  ParentController: <input type="text" ng-model="foo" />
  <div ng-controller="ChildController">
    ChildController: <input type="text" ng-model="foo" />
  </div>
</div>  
app  
  .controller('ParentController', function ($scope) {
    $scope.foo = "bar";
  })
  .controller('ChildController', function ($scope) { /*empty*/ });

See the Pen Demo: Scope Inheritance Issue by Alex Ford (@Chevex) on CodePen.

Initially the child scope has no property called foo. Instead it's reading from the inherited foo property from the parent scope. This is why the child input updates when you change the parent input. However, once you modify the child input, it uses its value and updates foo on the child scope. Because of the way prototypal inheritance works, the child foo property is merely masking the parent foo property. In other words, foo on the parent scope remains unchanged while foo on the child scope has been changed to a new value. Once you've modified the child input, modifying the parent input does nothing to the child one anymore because the child scope now has its own foo property with a value.

The problem with scope inheritance is one I see newbies run into constantly on Stack. Traditionally, the fix is to do what I did in my first example and attach your scalar values to objects on the scope.

<div ng-controller="ParentController">  
  ParentController: <input type="text" ng-model="obj.foo" />
  <div ng-controller="ChildController">
    ChildController: <input type="text" ng-model="obj.foo" />
  </div>
</div>  
app  
  .controller('ParentController', function ($scope) {
    $scope.obj = {
      foo: "bar"
    };
  })
  .controller('ChildController', function ($scope) { /*empty*/ });

See the Pen Demo: Scope Inheritance "Fix" by Alex Ford (@Chevex) on CodePen.

You can see that the inputs properly update each other now. It just sucks that we have to use such a strange setup just to avoid this issue. Now with the addition of the new Controller as technique we don't have to worry anymore! We can simply refer to the controller we wish to refer to and stop worrying about the subtleties of scope inheritance.

<div ng-controller="ParentController as parent">  
  ParentController: <input type="text" ng-model="parent.foo" />
  parent.foo: {{ parent.foo }}
  <div ng-controller="ChildController as child">
    ChildController: <input type="text" ng-model="parent.foo" />
    parent.foo: {{ parent.foo }}
  </div>
</div>  
app  
  .controller('ParentController', function () {
    this.foo = "bar";
  })
  .controller('ChildController', function () { /*empty*/ });

See the Pen Demo: No more coming up with meaningless manual namespaces. by Alex Ford (@Chevex) on CodePen.

Not only does this help bypass this annoying inheritance issue, I think it makes the markup even cleaner than when we used $scope. You can clearly see, even in the DOM managed by the child controller, that we're explicitly binding things to a value on the parent controller. We no longer have to do any of this $scope.$parent nonsense or create complicated services and inject them all over. Now you can just simply refer to the controller and the value that you intended to refer to in the first place :D

It's important to keep in mind what Angular is actually doing with this new syntax. It's not some magical global variable that you can refer to from anywhere; it's just a variable that refers to that controller's execution context and that variable is attached to $scope behind the scenes.

Essentially, this:

app.controller('MyController', function () {  
  this.someValue = "Hello!";
});

Is no different from this:

app.controller('MyController', function ($scope) {  
  $scope.myController = this;
  this.someValue = "Hello!";
}

It's just that the "as" syntax implicitly creates a namespace on the controller's scope, whereas you must create a namespace manually in the latter example. Don't believe me? Here's the proof :)

See the Pen Demo: What's actually happening? by Alex Ford (@Chevex) on CodePen.

As you can see, this and $scope.myController are the same object in the above example. That gives you a big clue to what Angular is doing behind the scenes. It merely attached the controller variable to the $scope, implicitly giving us a very readable and logical namespace for the values we'd like to expose. What that also means is that if we overrode the child scope with a property called myController, it would still mask the myController property implicitly defined on the parent scope. So as long as you aren't setting variables on your child scopes to the same value that you used in your "controller as someName" on the parent scope, the whole inheritance issue is kind of hidden from you and helps prevent you from making mistakes as easily.

Chev

Read more posts by this author.

comments powered by Disqus