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:
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:
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:
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:
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:
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:
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:
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:
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.