The first step down the path of limiting, or modularizing, your scope is setting its value to true. In this case, a new inherited scope is actually generated, much like the behavior when nesting controllers. This type of scoping for a directive is by far the easiest to understand and operate within, as you still have read access to all of the parent scope's values, but it's less likely that you'll accidentally change the data in the rest of the application. For this reason, I usually recommend setting scope to true from the beginning, unless you have a strong reason to do otherwise.
To help clarify the difference of this type of scoping, let's revisit our previous example. This time, the scope section of our definition object will be scope: true, but otherwise all of the code will remain the same. When we first load the page, this directive will be indistinguishable from our previously false-scoped directive; both
#appTitle and #directiveTitle will still say Hello World because our directive's scope is inherited from the parent.
We discussed this inheritance briefly earlier, but as a reminder, this is the prototypical inheritance innate to JavaScript. As such, any value not explicitly declared on our scope will be read from the parent, or parent's parent, all the way up to $rootScope. The caveat, of course, is that setting, or writing, values does not work that same way.
As soon as we set a value, it's stored locally and we lose all connection to any ancestral property of the same name. Again, let's see how our directive will flow given our newly updated scope parameter, as shown in the following figure:
$parentScope
As you can see, what this new scope object means for us is that once we click on our button and fire setDirectiveTitle, #directiveTitle will change to bob, however
#appTitle will still say Hello World. This type of scoping has all of the advantages and disadvantages of prototypical inheritance. We get to keep read access to the rest of the application's data, and even the updates to it, as demonstrated by our firing of setAppTitle, without having to worry about accidentally overwriting anything. On the other hand, for values that you do want to change, it does require keeping track of which values are local and which are inherited, as well as using inherited methods to change any ancestral values.
Chapter 5
[ 41 ]
Scope = {}
For times when you want to have complete control over which properties and methods are interconnected within your new directive scope, an object hash is usually your best solution. This type of scoping is commonly referred to as an isolate scope, because of the lack of connectedness with the other scopes within the application. An empty object signifies that you want the new scope to be completely isolated from its parents, so nothing is inherited or carried over. If absolutely
necessary, you can still access the parent or root scopes by using the $parent and
$root properties, respectively, however this goes directly against our goals of modularity and thus should only be used when there's no better option.
While there are a few instances where we want our directive to be entirely isolated, more commonly we'll want to maintain access to a few explicitly specified properties and methods from the ancestral scope tree. To do this, Angular provides three symbols for notating what type of access you want to acquire: @, =, and &, which are prepended to the attribute names that you want to derive a value from. As such, we can create an isolate scope that looks like this:
scope : {
'myReadonlyVariable' : '@myStringAttr', 'myTwowayVariable' : '=myParentProperty', 'myInternalFunction' : '&myParentFunction' }
@ – read-only Access
Using the @ symbol to retrieve a value from your attributes means that the attribute value will be interpolated and whatever is returned will be stored within the scope property that you specify. To help explain, let's expand our previous scoping example with a few extra details as shown in the following code snippet:
<div ng-init="title = 'Hello World'">
<h2 id="appTitle">{{title}}</h2>
<button id="newAppTitle" ng-click="setAppTitle('App 2.0')">Upgrade me!</button>
<div my-scoped-directive msd-title="I'm a directive, within the app {{title}}">
<h4 id="directiveTitle">{{title}}</h4>
<button id="newDirTitle" ng-click="setDirectiveTitle('bob')">Bob it!</button>
</div>
</div>
…
Keeping it Clean with Scope $scope.setDirectiveTitle = function (title) { $scope.title = title;
};
} };
});
First, as a quick reminder, within our JavaScript, all attribute names are normalized to be camelCased, which is why we refer to the HTML attribute msd-title as msdTitle within our scoping object. Secondly, take note of how our attribute string is evaluated. The {{title}} value in our attribute will be evaluated within the parent scope, not in the new internal scope we're creating. Thus, in this example,
#appTitle will still be Hello World, but #directiveTitle will now read I'm a directive, within the app Hello World.
Thirdly, it is important to realize that even though we've requested only read-only access for this attribute, it will still be dynamically updated when the parent scope changes. If a user clicks on our #setAppTitle button, the #appTitle will be updated to App 2.0 and #directiveTitle will mirror that change by now reading I'm a directive, within the app App 2.0.
Of course, as you can probably ascertain by the "read-only" nature of this access, the reverse is not true. If our user now clicks on our #setDirTitle button, #appTitle will remain unchanged, #directiveTitle will now read bob, and, most importantly, we've now severed the connection between the two values. The following figure provides insight into the state of our data at each point of the process:
$parentScope
title : ‘I'm a directive, within the app Hello World’
}
{
title : ‘I'm a directive, within the app App 2.0’
}
Chapter 5
[ 43 ]
From this point forward, any updates to the parent scope's title property will be ignored by our directive, as we've overridden our original linked value with a new static value. If we you need both read and write access to a property, you will need to instead utilize the following method for requesting property access.
= – two-way binding
For occasions where you want full access to a specific property on the parent scope, Angular provides the = symbol for use within our isolate scope. Let's extend our original example again to see how this can be useful:
<div ng-init="title = 'Hello World'; subtitle = 'I am an app'">
<h2 id="appTitle">{{title}}</h2>
<h3 id="appSub">{{subtitle}}</h3>
<button id="newAppTitle" ng-click="setAppTitle('App 2.0')">Upgrade me!</button>
<div my-scoped-directive msd-title="I'm a directive, within the app {{title}}" msd-subtitle="subtitle">
<h4 id="directiveTitle">{{title}}</h4>
<button id="newDirTitle" ng-click="setDirectiveTitle('bob')">Bob it!</button>
<button id="newDirSub" ng-click="setDirectiveSubtitle('Time to submerge')">Empty the ballasts!</button>
</div>
</div>
directive('myScopedDirective', function() { return {
scope : {
'title' : '@msdTitle', 'subtitle' : '=msdSubtitle' },
link : function ($scope, $element, $attrs) { $scope.setDirectiveTitle = function (title) { $scope.title = title;
};
$scope.setDirectiveSubtitle = function (subtitle) { $scope.subtitle = subtitle;
};
} };
});
Keeping it Clean with Scope
[ 44 ]
Now that's we've updated our code, the data model now proceeds through the following flow:
title : ‘I’m a directive, within the app Hello World’, subtitle : ‘I am an app’
}
{
title : ‘Hello World’, subtitle : ‘Time to submerge’
}
{
title : ‘I’m a directive, within the app Hello World’, subtitle : ‘Time to submerge’
}
Within this scenario, our subtitle property works in exactly the same way as it would if we had set the entire scope property to false. When our app is first initialized, both subtitles will read I am an app. If we fire the
setDirectiveSubtitle method, however, both values will again change, this time reading Time to submerge. Given that this method of binding is identical to a false scope property, we might be inclined to wonder why this option is even provided.
The difference here is that we only have this two-way binding for properties that we explicitly specify, which helps us ensure modularity. Because of the way we've bound our property, we don't care about the name of our parent property. It could be lesser-title-the-third for all we care, and as long as that property is passed into our msd-subtitle attribute, our directive will continue to function in exactly the same way, including updating the parent scope's property when our button is clicked.
And, of course, the reverse is also true. When developing our parent application, we don't need to worry about our property names conflicting with those used in our directive and suddenly being overwritten. This type of binding plays a huge role in making sure a directive can be plugged into any application and have both the directive and the application function as their respective developers intended.
& – method binding
Sometimes, however, it's not simply properties that you want to be able to maintain access to. Sometimes you need to be able to call a method on the parent scope. For this, the symbol of choice is &, and let's once again return to our example to see how this symbol is used, as shown in the following code snippet:
<div ng-init="title = 'Hello World'; subtitle = 'I am an app'">
<h2 id="appTitle">{{title}}</h2>
<h3 id="appSubtitle">{{subtitle}}</h3>
Chapter 5
[ 45 ]
<button id="upgradeApp" ng-click="setAppTitle('App 2.0', 'Still an app')">Upgrade me!</button>
<div my-scoped-directive msd-update-title="setAppTitle(title, 'Updated by a directive')">
<h4 id="directiveTitle">{{title}}</h4>
<button id="bobApp" ng-click="updateTitle({title : 'bob'})">Bob it!</button>
</div>
</div>
//Parent scope:
$scope.setAppTitle = function (title, subtitle) { $scope.title = title;
$scope.subtitle = subtitle;
}
…
directive('myScopedDirective', function() { return {
scope : {
'updateTitle' : '&msdUpdateTitle' },
Method binding is by far the most complicated of all scope bindings, so don't worry if it doesn't all make sense instantly. In most cases if you need to share methods between parent and child directives, I'd recommend using controllers (see our next chapter) whenever possible, however this is helpful if for some reason your method has to be scoped. Once more, the following diagram demonstrates the flow of the data model as our user interacts with our elements:
$parentScope subtitle : ‘Still an app’
}
Keeping it Clean with Scope
[ 46 ]
In this example, we've extended our parent scope's setAppTitle method to take two arguments, allowing us to update both the title and subtitle at the same time. We've also requested access to that same function and passed it in via the msd-update-title attribute. Note, however, that we don't just ask for it by name. Instead, the attribute value actually calls the function with a set of variable and/or hard-coded parameters.
Then, within our new scope, Angular creates a wrapper function that takes a map of the variable names and values (the {title : 'bob'} parameter in our example) and calls our original method within the context of the parent scope, passing in all mapped values and other hard-coded parameters.
What this means is that when we first click on the #upgradeApp button, our
#appTitle will become App 2.0 and our #appSubtitle will read Still an app. If, however, we click on the #bobApp button within our directive, our wrapper function, bound to updateTitle within our directive's scope will be called, the title variable within our msd-update-title value will be mapped to bob, and finally setAppTitle('bob','Updated by a directive') will be called within the parent scope. It's important to emphasize that the entire function is called within the parent scope, so even though we're passing in a title value and calling it from the directive, it's the parent scope's title property that we're actually updating.
Again, binding a method like this is one of the most complex binding options Angular offers for use in an isolate scope, however it also allows you as the developer once again to remain fully modular, having to worry about the internal logic within the setAppTitle method, or even what it's called, and yet still be able to trigger it as you see fit. This type of access can be especially helpful when you want to be able to trigger a data refresh from an external source, but don't want your directive to know anything about how to go about that refresh or any data processing that needs to happen before or after the new content is received.
Chapter 5
[ 47 ]
Summary
If you've made it this far, I trust you're convinced of the value of modularity and are interested in the ways Angular directives can make that easier for you. Scoping, particularly isolate scopes, is one of the most fundamental pieces of enabling that modularity. While the default scope value of false can certainly be convenient, and is often useful during early development and debugging, I strongly recommend not using it within your final production code unless you know it's absolutely necessary, and certainly not when you're developing a directive intended to be packaged and made available for use in other applications. The new scope provided by a value of true does provide a helpful compromise, though is still often more access than you need for a packaged directive. Hopefully by now, you see the value of an isolate scope, even though it does require some additional effort up front to consider all of the properties and methods you might need. If not, hold on, it'll demonstrate its usefulness even more in the coming chapters. In the next chapter, we'll be talking about controllers, and the ways they can be shared across multiple directives to extend the functionality of each.