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,
, on the scope. When AngularJS processes the
directive in the view, it starts listening for change events on that input element and on the scope's
property. If the value of
changes, the input will update, and if the input changes,
updates. Let's look at another example:
Even though both inputs bind to a
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
. 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
) request new scopes. After compilation is done, AngularJS will have created a scope tree that mirrors the DOM tree - the
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
property on it.
Sidebar: AngularJS Scope Inheritance
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?
directive creates a child scope, and it's that child scope that
is binding to. Before we modify the input, no
property exists on the child scope, so the prototype chain is consulted, and the parent's
is used. However, when we update the input,
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
instead to bind to
and we never end up creating
locally in the child. Although this code works, it's highly suspect. Because it uses
, if we make a simple change to our view, by changing
(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
refers to an object on the parent,
is never set on the child scope. It's a best practice to never bind directly to primitives to avoid this problem.
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
, 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
, 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,
sets up a two-way binding between the parent's
and the child's
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.
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.