I gave a talk on AngularJS directives recently and a good portion of that talk focused on defining the types of scopes that those directives can have. As something that the talk’s attendees could refer back to, I decided to put together this short blog post. It captures the spirit (if not the exact words) of what I presented that day. So without further ado…
scopes: an overview
One way to think about
scope objects on an AngularJS directive is to think of it as a little sandbox for the directive’s model data. The directive is able to participate in the application’s
$digest cycle, but we’re also able to carve off pieces that we can safely modify without “polluting” or “damaging” the parent scope. (Except when we want to.) This safety is the main reason that I’ve advocated so strongly for the liberal use of directives in AngularJS application design. When defining a directive, the value (or lack thereof) assigned to the
scope property of the directive definition object will determine what type of scope is created. At a high level, these scopes include: parent, new, and isolate.
This one’s easy. If we leave off the
scope property from the directive definition object, then it (the directive) does not create a new scope, and instead inherits one from its parent (e.g., the controller). This implicitly creates a two-way binding for all properties on the parent scope; there is no inheritance, we’re simply dealing directly with the same scope as our parent.
This one’s only slightly trickier. If we set
ddc-foo directives in this fiddle:
Where the AngularJS scope directives really shine is in the isolate scopes. These are those safe little sandboxes I alluded to earlier. By declaring the
scope property on the directive definition object with an object, we are creating an entirely new scope that is isolated from the parent with no inheritance from it. While this doesn’t necessarily mean that we can manipulate data with impunity, it does offer us some protection against accidentally mangling data from our parent scopes. We still have access to our parent scope (through the
$parent property) if we really need it, but we’re no longer working with it incidentally.
The other important thing to know about isolate scopes is that properties are declared with one of three special symbols to indicate the type of binding. Those symbols are:
@for one-way bindings
&for expression bindings
@ (one-way) binding uses interpolated literals to initialize a value on the directive’s isolate scope that is a copy of that value from the parent scope. The “copying” takes place during the directive’s linking phase and once complete, we can update that value indefinitely without overwriting the value on the parent scope. Observe:
= (two-way) binding allows us to keep values synchronized between the directive’s isolate scope and the parent scope. During the linking phase, a reference is created between the two scopes such that updates to one will reflect in the other (and vice versa). In some ways, this is what we get with the parent scope option (above) except that we are making explicit bindings to a specific subset of the properties on the parent scope. Observe:
& (expression) binding allows us to call functions from the parent scope within the context of the isolate scope. This allows us to create generic directives effectively have an interface that needs to be implemented with methods from that parent scope. Observe:
To sum up
AngularJS directives provide three different scopes to use for interacting with the data from parents scopes. We can omit the
scope property when defining our directive and simply use the parent scope. We can set
scope:true to create a new scope that prototypically inherits from the parent. And lastly, we can create an isolate scope to create a “sandboxed” context for the directive’s data. While all three have their uses, we should prefer the isolate scope for the flexibility and safety that it gives us.