Nested Scopes in AngularJS

If you haven’t yet watched, Miško Hevery’s talk on AngularJS Best Practices is well worth the watch. It’s jammed packed with excellent insights on how to use AngularJS effectively.

This post is a deep-dive into a specific point about nested scopes and your data model. This point begins at 29:19 and a couple of phrases are used as guidelines (paraphrasing) “The scope is not the model, the scope refers to the model” and “Whenever you have ng-model there’s gotta be a dot in there somewhere. If you don’t have a dot, you’re doing it wrong.”

He uses a live example to illustrate the point. Below is that example. To see the behavior, first manipulate the second input, and notice all three references are updated as expected. Then edit the first field, then the second field again. Now the first field is disconnected from the second field and final text output.

Now if you are familiar with the way prototypal inheritance works in JavaScript, and you also know that scopes use prototypal inheritance when creating child scopes, it should be pretty clear what this all means. If not, it can be pretty confusing, so let’s take a crash course on prototypal inheritance.

Prototypal Inheritance

JavaScript uses a very simple way to deal with inheritance. We won’t dig to deep into it, but let’s get a mental model of how it works.

An object can be thought of as a bag of key/value pairs. If I have the following code

var jim = {
  first_name: "Jim",
  last_name: "Hoskins",
  company: "Treehouse" 
}

Jim is an object with the key first_name mapped to the value "Jim", last_name mapped to "Hoskins" and company mapped to "Treehouse". So I can refer to jim.first_name and get the value, or assign to jim.first_name to set the value for that key.

But here’s something interesting, I can call jim.toString() even though I didn’t define toString on my object. That’s because each object has an invisible link to one other object, its prototype, or parent.

When I execute jim.toString(), it first needs to find the key/value pair for toString. It looks in the jim object, but it clearly doesn’t exist. So it automatically follows that link to the prototype to see if toString exists there, and it does. In this example, jim’s prototype is Object.prototype, and you can actually find toString directly at Object.prototype.toString.

But here’s the thing, once If I assign to jim.toString like so:

jim.toString = function () {
  return "I am Jim!";
}

I am creating a toString key directly on jim, so calling jim.toString() now no longer needs to search up the prototype to find it. This turns out to be a really useful mechanism, because it allows many objects to share, or inherit, certain properties or methods, which means each object doesn’t need its own copy of the value. This also allows us to override or overwrite any value we have inherited, without messing anything up higher up.

So reading a value will search the parents if it is not found, but writing a value always writes directly to the object, even if it is also defined higher up.

Angular Scopes and Prototypes.

AngularJS will often create child scopes inside of directives, for the same reasons we saw above: it allows a directive to have it’s own space for key/value pairs, without messing with higher level data. Child scopes inherit from parent scopes using prototypal inheritance. (There is an exception for isolated scopes);

But we often do want to transparently write to a higher level scope, and this is where the “dot rule” comes in. Let’s see how that is applied.

Do not do this

// Controller 
$scope.first_name = "Jim"

// HTML
<input ng-model="first_name">

That is bad because we can’t write to it from a child scope, because a child scope uses prototypal inheritance. Once you write to first_name from a child scope, it would become a distinct value in that child scope, and no longer refer up to the parent’s first_name.

Instead, do this

// Controller
$scope.person = {
  first_name:  "Jim"
}

// HTML
<input ng-model="person.first_name">

The difference is we are not storing the data directly on the scope. Now when ng-model wants to write, it actually does a read, then write. It reads the person property from the scope, and this will work from a child scope because a child scope will search upwards for a read. Once it finds person, it writes to person.first_name, which is no problem. It just works.

Play with the updated example here to see it in action.

Hopefully you now have a better grasp on the technical reason this type of code behaves the way it does. Quite often, bugs can be solved by simply nesting your property in another object. That little bit of indirection can be very powerful.