Managing Directive Scopes in AngularJS

The relationship between a directive and its scope means that some care is required if you want to create a directive that can be reused throughout an application. By default, the link function is passed the scope of the controller that manages the view that contains the element to which the directive has been applied. I know that last sentence sounds like a tongue-twister nursery rhyme—but if you read it again, you’ll make sense of the relationship between some of the major components of an AngularJS application. A simple example will help give context, and Listing 16-16 shows the content of the directiveScopes.html file that I added to the angularjs directory.

Listing 16-16. The Contents of the directiveScopes.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Scopes</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template:

“<div class=’panel-body’>Name: <input ng-model=name /></div>”,

}

})

.controller(“scopeCtrl”, function ($scope) {

// do nothing – no behaviours required

});

</script>

</head>

<body>

<div ng-controller=”scopeCtrl” class=”panel panel-default”>

<div class=”panel-body” scope-demo></div>

<div class=”panel-body” scope-demo></div>

</div>

</body>

</html>

This is such a simple directive that I don’t even need to define a link function—just a template that contains an input element to which the ng-model directive has been applied. The ng-model directive creates a two-way binding for a scope property called name, and I have applied the directive to two separate div elements in the body section of the document.

Even though there are two instances of the directive, they are both updating the same name property on the scopeCtrl controller. You can see the effect this creates by loading the directivesScopes.html file into the browser and entering some characters into either input element. The two-way data bindings ensure that both input elements are in sync, as shown in Figure 16-4.

This behavior can be useful, and it is a nice demonstration of how the scope can be used to keep elements coordinated and capture or display the same data. But you will often want to reuse a directive to capture or display different data, and that’s where the management of scopes comes in.

It can be hard to get your head around the different ways that directives and scopes can be set up, so I am going to diagram each of the different configurations that I created in this part of the chapter. In Figure 16-5, you can see the effect created by Listing 16-16, shown before and after the input elements have been edited.

There is no scope data in this example when the application first starts, but the ng-model directive that I included in the directive template means that AngularJS will dynamically create a name property when the contents of either input element are changed. Since there is only one scope in this example—well, aside from the root scope, which I am not directly using in this chapter—both directives bind to the same property, and that’s why they are synchronized.

Tip In this chapter, I only describe the scopes used by the controller and the directives I create. In fact, there can be a lot more scopes because directives can use other directives in their templates or even explicitly create scopes. I am focused on controller/directive scopes in this chapter, but the same rules and behaviors apply throughout the scope hierarchy.

1. Creating Multiple Controllers

The simplest but least elegant way to reuse directives is to create a separate controller for each instance of the directive so that each gets its own scope. This is an inelegant technique, but it can be useful when you don’t control the source code for the directive you are using and so can’t change the way that the directive works. In Listing 16-17, you can see how I have added an additional controller to the directiveScopes.html file.

Listing 16-17. Adding a Second Controller to the directiveScopes.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Scopes</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template:

“<div class=’panel-body’>Name: <input ng-model=name /></div>”,

}

})

.controller(“scopeCtrl”, function ($scope) {

// do nothing – no behaviours required

})

.controller(“secondCtrl”, function($scope) {

// do nothing – no behaviours required

});

</script>

</head>

<body>

<div class=”panel panel-default”>

<div ng-controller=”scopeCtrl” class=”panel-body” scope-demox/div>

<div ng-controller=”secondCtrl” class=”panel-body” scope-demo></div>

</div>

</body>

</html>

The effect of using two controllers is that there are two scopes, each of which has its own name property, and this allows the input elements to operate independently. Figure 16-6 shows how the scopes and data in this example are arranged.

There are two controllers, each of which contains no data when the application starts. Editing the input elements dynamically creates a name property in the scope of the controller that contains the directive instance the input element is managed by, but these properties are completely separate from one another.

2. Giving Each Directive Instance Its Own Scope

You don’t have to create controllers to give directives their own scopes. A more elegant approach is to ask AngularJS to create a scope for each instance of the directive by setting the scope definition object property to true, as shown in Listing 16-18.

Listing 16-18. Creating a New Scope for Each Instance of a Directive in the directiveScopes.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Scopes</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template:

“<div class=’panel-body’>Name: <input ng-model=name /></div>”,

scope: true

}

})

.controller(“scopeCtrl”, function ($scope) {

// do nothing – no behaviours required

});

</script>

</head>

<body ng-controller=”scopeCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body” scope-demo></div>

<div class=”panel-body” scope-demo></div>

</div>

</body>

</html>

Setting the scope property true allows me to reuse the directive within the same controller, and that means I am able to remove the second controller and simplify the application. The simplification isn’t enormously significant in a simple example like this one, but large projects are complex enough without having to create endless controllers just to stop your directives from sharing data values.

The scopes that are created when the scope property is set to true are part of the regular scope hierarchy that I described in Chapter 13. This means the rules I described about inheritance of objects and properties apply, giving you flexibility about how you set up the data used—and potentially shared—by instances of a custom directive. As a quick demonstration, I have expanded my example in Listing 16-19 to show the most commonly used permutations.

Listing 16-19. Expanding the Example Directive in the directiveScopes.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Scopes</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”scopeTemplate”>

<div class=”panel-body”>

<p>Name: <input ng-model=”data.name” /></p>

<p>City: <input ng-model=”city” /></p>

<p>Country: <input ng-model=”country” /></p>

</div>

</script>

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template: function() {

return angular.element(

document.querySelector(“#scopeTemplate”)).html();

},

scope: true

}

})

.controller(“scopeCtrl”, function ($scope) {

$scope.data = { name: “Adam” };

$scope.city = “London”;

});

</script>

</head>

<body ng-controller=”scopeCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body” scope-demo></div>

<div class=”panel-body” scope-demo></div>

</div>

</body>

</html>

I have reached the limits of using a string as the template, so I used a script element to define the markup I require and select its contents through a template function, as I described in the “Using a Function as a Template” section earlier in the chapter. The template contains three input elements, each of which is bound through the ng-model directive to a data value in the scope. Figure 16-7 shows the arrangement of scopes and data in the example.

The disposition of the data in this example is a step up in complexity, so I have described what happens to each of the three data values in Table 16-4, just to provide some additional detail.

3. Creating Isolated Scopes

In the previous example, you saw how creating a separate scope for each instance of the directive allowed me to remove the redundant controllers and mix together the different ways that objects and properties are inherited from one level to the next in the scope hierarchy (which I described in Chapter 13).

The advantage of this approach is that it is simple and consistent with the rest of AngularJS, but the disadvantage is that the behavior of your directive is at the mercy of the controller in which it is applied because the default rules for scope inheritance are always used. It is easy to get into a situation where one controller defines a scope property called count as 3 and another defines count as Dracula. You may not want to inherit the value at all, and you may end up modifying the controller scope in an unexpected way if your changes are made to properties defined on scope objects, something that is likely to cause problems if your directive is being applied by other developers.

The solution to this problem is to create an isolated scope, which is where AngularJS creates a separate scope for each instance of the directive but the scope doesn’t inherit from the controller scope. This is useful if you are creating a directive that you intend to reuse in a range of different situations and don’t want any interference caused by the data objects and properties defined on by the controller or elsewhere in the scope hierarchy. An isolated scope is created when the scope definition property is set to an object. The most basic kind of isolated scope is represented by an object with no properties, as shown in Listing 16-20.

Listing 16-20. Creating an Isolated Scope in the directiveScopes.html File

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template: function() {

return angular.element(

document.querySelector(“#scopeTemplate”)).html();

},

scope: {}

}

})

.controller(“scopeCtrl”, function ($scope) {

$scope.data = { name: “Adam” };

$scope.city = “London”;

});

</script>

You can see the effect of the isolated scope if you load the directiveScopes.html file into the browser—although this is one of the dullest examples to test, since all six input elements are empty. This is the consequence of the isolated scope; because there is no inheritance from the controller’s scope, there are no values defined for any of the properties specified by the ng-model directive. AngularJS will dynamically create these properties if you edit the input elements, but the properties will only be part of the isolated scope of the directive that the modified input element is associated with. Figure 16-8 shows the disposition of the scopes in Listing 16-20 so that you can compare an isolated scope with previous examples.

Each instances of the directive has its own scope but does not inherit any data values from the controller scope. Because there is no inheritance, changes to properties that are defined via objects are not propagated to the controller scope. In short, an isolated scope is cut off from the rest of the scope hierarchy.

3.1. Binding via an Attribute Value

Isolated scopes are an important building block when you are creating a directive that you intend to reuse in different situations because it avoids unexpected interactions between the controller scope and the directive. But totally isolating a directive makes it hard to get data in and out, so AngularJS provides a mechanism by which you can break through the isolation to created expected interactions between the controller scope and the directive.

Isolated scopes allow you to bind to data values in the controller scope using attributes applied to the element alongside the directive. It makes more sense when you see a demonstration, and in Listing 16-21 you can see how I have created a one-way binding between a data value in the controller scope and the directive’s local scope.

Listing 16-21. Creating a One-Way Binding for an Isolated Scope in the directiveScopes.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Scopes</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”scopeTemplate”>

<div class=”panel-body”>

<p>Data Value: {{local}}</p>

</div>

</script>

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template: function() {

return angular.element(

document.querySelector(“#scopeTemplate”)).html();

},

scope: {

local: “@nameprop”

}

}

})

.controller(“scopeCtrl”, function ($scope) {

$scope.data = { name: “Adam” };

});

</script>

</head>

<body ng-controller=”scopeCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

Direct Binding: <input ng-model=”data.name” />

</div>

<div class=”panel-body” scope-demo nameprop=”{{data.name}}”></div>

</div>

</body>

</html>

There are three changes in this example, and together they created a binding between the controller and directive scopes. The first change is in the scope definition object, where I set up a one-way mapping between an attribute and a property in the directive scope, as follows:

scope: {

local: “@nameprop”

}

I have defined a property called local on the object assigned to the scope definition object, and this tells AngularJS that I want to define a new property in the directive scope by that name. The value of the local property is prefixed with an @ character, which specifies that the value for the local property should be obtained as a one-way binding from an attribute called nameprop.

The second change is to define the nameprop attribute on the elements to which I apply my custom directive, as follows:

<div class=”panel-body” scope-demo nameprop=”{{data.name}}“></div>

I specify the value for the local property in the directive scope by providing an AngularJS expression in the nameprop attribute. In this case, I selected the data.name property, but any expression can be used. The final change is to update the template so that it displays the value of the local property:

<script type=”text/ng-template” id=”scopeTemplate”>

<div class=”panel-body”>

<p>Data Value: {{local}}</p>

</div>

</script>

I have used an inline binding expression to display the value of the local property. I added an input element to the view that modifies the data.name property in the controller scope, and you can see the result in Figure 16-9.

It is worth reiterating what is happening in this example because it is an important concept in advanced directive development and one that causes a lot of confusion. I have used an isolated scope so that my directive doesn’t inherit the data in the controller’s scope and end up working with data that it wasn’t expecting—something that can happen because there is no way to selectively control how a regular nonisolated scope inherits data values from its parent.

But my directive does need to access a data value in the controller’s scope, so I told AngularJS to create a one-way binding between an expression I specified as an attribute value and a property on the local scope. Figure 16-10 shows how the scopes and data in this example are arranged.

As the diagram shows, there are two data bindings. The first binds the data.name property in the controller scope to the local property in the isolated scope, as specified by the attribute value. The second binds the local property in the isolated scope to the inline binding expression in the directive template. AngularJS keeps everything organized so that changes to the data.name property update the value of the local property.

This gives me the selective control over the scope inheritance that I need and, as a bonus, allows that selection to be configured when the directive is applied, which is key to allowing a single directive to be reused in different ways without needing any code or markup changes. You can see a demonstration of this reuse in Listing 16-22.

Listing 16-22. Reusing a Directive with a One-Way Data Binding in the directiveScopes.html

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Scopes</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”scopeTemplate”>

<div class=”panel-body”>

<p>Data Value: {{local}}</p>

</div>

</script>

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template: function() {

return angular.element(

document.querySelector(“#scopeTemplate”)).html();

},

scope: {

local: “@nameprop”

}

}

})

.controller(“scopeCtrl”, function ($scope) {

$scope.data = { name: “Adam” };

});

</script>

</head>

<body ng-controller=”scopeCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

Direct Binding: <input ng-model=”data.name” />

</div>

<div class=”panel-body” scope-demo nameprop=”{{data.name}}”></div>

<div class=”panel-body” scope-demo nameprop=”{{data.name + Freeman’}}”></div>

</div>

</body>

</html>

I created a second instance of my custom directive and set the nameprop attribute to bind to an expression based on the data.name property. What’s important in this example is what I did not do, which is to make changes to the directive. I used the same (admittedly simple) functionality to display two different data values just by changing the expression specified in the attribute on the element to which the directive is applied. This is a powerful technique and one that is invaluable for creating complex directives.

3.2. Creating a Two-Way Binding

The process for creating two-way bindings in isolated scope is similar to that for a one-way binding, as shown by Listing 16-23.

Listing 16-23. Creating a Two-Way Binding in the directiveScopes.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Scopes</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”scopeTemplate”>

<div class=”panel-body”>

<p>Data Value: <input ng-model=”local” /></p>

</div>

</script>

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template: function() {

return angular.element(

document.querySelector(“#scopeTemplate”)).html();

},

scope: {

local: “=nameprop”

}

}

})

.controller(“scopeCtrl”, function ($scope) {

$scope.data = { name: “Adam” };

});

</script>

</head>

<body ng-controller=”scopeCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

Direct Binding: <input ng-model=”data.name” />

</div>

<div class=”panel-body” scope-demo nameprop=”data.name”></div>

</div>

</body>

</html>

To create a two-way binding, I replace the @ character with the = character when I create the isolated scope so that this definition from the previous example:

scope: { local: “@nameprop” }

becomes this:

scope: { local: ” =nameprop” }

This isn’t the only change, however. When using a one-way binding, I provided a binding expression complete with the {{ and }} characters, but AngularJS needs to know which property to update with changes in a two-way binding, so I have to set the attribute value to a property name, like this:

<div class=”panel-body” scope-demo nameprop=“data.name”></div>

These changes create the two-way binding and allow me to update my directive template so that I can include content that modifies the data value. For this simple example, that just means an input element that uses the ng-model directive, like this:

<div class=”panel-body” scope-demo nameprop=”data.name”></div>

The effect this example creates is that the flow of updates goes in both directions between the scopes—updates to the data.name property in the controller scope update the local property in the isolated scope and changes to the local property cause data.name to be updated, as shown in Figure 16-11. It is impossible to capture this relationship in a figure, and I recommend you load the directiveScopes.html file into the browser to see firsthand how the contents of the input elements are synchronized.

Tip The arrangement for the scopes in this example is just the same as in Figure 16-10, except that the data bindings are two-way.

3.3. Evaluating Expressions

The final isolated scope feature is the ability to specify expressions as attributes and have them evaluated in the controller’s scope. This is another feature that is easier to understand with an example, such as the one shown in Listing 16-24.

Listing 16-24. Evaluating an Expression in the Controller Scope in the directiveScopes.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Directive Scopes</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script type=”text/ng-template” id=”scopeTemplate”>

<div class=”panel-body”>

<p>Name: {{local}}, City: {{cityFn()}}</p>

</div>

</script>

<script type=”text/javascript”>

angular.module(“exampleApp”, [])

.directive(“scopeDemo”, function () {

return {

template: function () {

return angular.element(

document.querySelector(“#scopeTemplate”)).html();

},

scope: {

local: “=nameprop”,

cityFn: “&city”

}

}

})

.controller(“scopeCtrl”, function ($scope) {

$scope.data = {

name: “Adam”,

defaultCity: “London”

};

$scope.getCity = function (name) {

return name == “Adam” ? $scope.data.defaultCity : “Unknown”;

}

});

</script>

</head>

<body ng-controller=”scopeCtrl”>

<div class=”panel panel-default”>

<div class=”panel-body”>

Direct Binding: <input ng-model=”data.name” />

</div>

<div class=”panel-body” scope-demo

city=”getCity(data.name)” nameprop=”data.name”>

</div>

</div>

</body>

</html>

This technique is slightly convoluted, but it is worth unwinding it because it can be useful, especially when you need to create a directive that takes advantage of the behavior and data defined by the controller in a reusable and predictable way.

The first change I made was to define a simple controller behavior that checks a first name argument and returns the name of the city associated with it; the default city is defined as a scope property. It doesn’t matter what the behavior does for this demonstration, only that the behavior and the data it depends on are defined in the controller scope, which would not be available by default in the directives isolated scope.

The name of the behavior is getCity, and to make this available to the directive, I added a new attribute to the element the directive is applied to, as follows:

<div class=”panel-body” scope-demo city=”getCity(data.name)” nameprop=”data.name”></div>

The value of the city attribute is an expression that calls the getCity behavior and passes the value of the data.name property as the argument to process. To make this expression available in the isolated scope, I have added a new property to the scope object, as follows:

scope: {

local: “=nameprop”,

cityFn: “&city”

}

The & prefix tells AngularJS that I want to bind the value of the specified attribute to a function. In this case, the attribute is city, and I want to bind it to a function called cityFn. All that remains is to call the function to evaluate the expression in the directive template, like this:

<div class=”panel-body”>

<p>Name: {{local}}, City: {{cityFn()}}</p>

</div>

Notice that I call cityFn(), with the parentheses, to evaluate the expression specified by the attribute, This is required even when the expression is itself a call to a function. You can see the effect in Figure 16-12: When the value of the data.name property is Adam, the data binding in the template displays the city name of London.

3.4. Using Isolated Scope Data to Evaluate an Expression

A variation on this previous technique allows you to pass data from the isolated scope to be evaluated as part of the expression in the controller’s scope. To do this, I modify the expression so that the argument passed to the behavior is the name of the property that has not been defined on the controller’s scope, as follows:

<div class=”panel-body” scope-demo city=”getCity(nameVal)” nameprop=”data.name”></div>

I selected nameVal as the name for the argument in this case. To pass data from the isolated scope, I have updated the binding in my template that evaluates the expression, passing in an object that provides values for the expression arguments, like this:

<div class=”panel-body”>

<p>Name: {{local}}, City: {{cityFn({nameVal: local})}}</p>

</div>

This has the effect of creating a data binding that evaluates an expression using a mix of data defined in the isolated scope and the controller scope. You have to be careful to make sure that the controller scope doesn’t define a property whose name corresponds to the argument in the expression; if it does, the value from the isolated scope will be ignored.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *