AngularJS Scopes: An Introduction

Thomas Fisher ·

At Carbon Five, we build quite a few AngularJS projects. It's a fun, powerful framework that gets you up and running quickly, but once you've got your feet wet, you run into its infamous learning curve. AngularJS has a fair share of deep concepts; taking some time to understand them can get you back on track. One of these important concepts is scopes. In an AngularJS application, the controller and view share an object called a scope; this object is at the core of its amazing two-way data binding. The controller sets properties on the scope, and the view binds to those properties. AngularJS takes responsibility for keeping the two in sync. Let's start with a simple example:
{{message}}
In this example, the controller sets a property, message, on the scope. When AngularJS processes the ng-model directive in the view, it starts listening for change events on that input element and on the scope's message property. If the value of message changes, the input will update, and if the input changes, message updates. Let's look at another example:
{{message}}
{{message}}
Even though both inputs bind to a message property, they're completely independent because they are bound to separate scopes. But where do those scopes come from?

Sidebar: Scope Creation

When your application starts, AngularJS creates the initial scope, which it calls $rootScope. It then "compiles" the document, starting at the root element. As it traverses the DOM, it encounters and processes markers that it calls directives, and some of these (such as ng-controller) request new scopes. After compilation is done, AngularJS will have created a scope tree that mirrors the DOM tree - the $rootScope is bound to the root element and child scopes are bound as the DOM nodes that request them are discovered. Separate scopes are incredibly useful. As seen in the example above, they allow for different parts of the view to cleanly separate their part of the underlying model. Additionally, child scopes can access properties of their parent, for example:
{{outer}}, {{inner}}
But how does this work? We know from the earlier example that each controller has a separate scope created for it, and, as far as we can tell, the inner controller's scope has no outer property on it.

Sidebar: AngularJS Scope Inheritance

In JavaScript, classical inheritance doesn't exist. However, each object has a special property, called its prototype. When accessing properties of an object, if the property doesn't exist, JavaScript checks if it exists on the prototype. It will continue looking up the prototype chain until it finds the property or it reaches the end. AngularJS uses object prototypes to implement child scopes. When creating a new scope, by default, it sets the new scope's prototype to its parent. This scope inheritance allows us to create pages with nested components, as seen in the example above. However, sometimes the interaction between parent and child scopes can be confusing:
{{message}}
Edit Message
At a first glance, we expect this code to work - but when we try to edit the message, we notice it's not being updated above. Why? Just like ng-controller, the ng-if directive creates a child scope, and it's that child scope that ng-model is binding to. Before we modify the input, no message property exists on the child scope, so the prototype chain is consulted, and the parent's message is used. However, when we update the input, ng-model sets the message property on the child. The end result is two separate message properties on the parent and child scope, not what we intended! We can verify this was the problem by explicitly referring to the parent scope within the child:
{{message}}
Edit Message
Here, we tell ng-model instead to bind to $parent.message and we never end up creating message locally in the child. Although this code works, it's highly suspect. Because it uses $parent, if we make a simple change to our view, by changing ng-if to ng-show (which doesn't create a new scope), our scope hierarchy would change and our code would suddenly break. A better alternative is to bind to properties of objects:
{{message.content}}
Edit Message
This example works because AngularJS now looks for message, then content. Since message refers to an object on the parent, content is never set on the child scope. It's a best practice to never bind directly to primitives to avoid this problem.

Isolating Scopes

AngularJS's default scope inheritance model can be very useful to divide up parts of a large application, but sometimes it causes problems. In particular, it's very difficult to write reusable components - how can you ensure your code works when you can't know what scope it will be included in? Luckily, AngularJS provides an escape hatch from inheritance: isolated scopes. These scopes are identical to normal ones - but they do not prototypically inherit. Isolated scopes can only be used in directives, so let's create a simple one:
{{message.content}}
Our directive uses scope: {}, which tells AngularJS to create an isolated scope. We also inline a template, which will be inserted into the DOM when we use the directive. Even though the template refers to the parent scope's message.content, it cannot access it, because it's bound to the directive's isolated scope. Isolated scopes are great for encapsulation, but without some kind of communication with a parent, their use is very limited. To address this, AngularJS provides a way to bind specify properties between the isolated scope and its parent:
{{parentMessage.content}}
This example makes it seems like both inputs share the same scope - but they don't. In our isolated scope definition, message: '=' sets up a two-way binding between the parent's parentMessage and the child's message properties. Using bindings like this is a common way to pass values to an isolated scope, or to allow it to set a value on its parent.

Summary

Scopes are core to how AngularJS works, so understanding them is very important. Learning how to use scopes effectively helps you better divide up chunks of large applications, and using isolated scopes can help you encapsulate complex components. Although we've barely scratched the surface, we now know enough to dive deeper. If you're looking to learn more, explore how AngularJS keeps the view in sync with the digest loop, or learn how to communicate directly between scopes with events.