Unlike a controller, which is paired with a newly created scope when created, a directive is not given a scope of its own by default. Instead, it simply uses the scope that is available, based on its location in the DOM. I consider this a poor default, because most directives are written as reusable components with their own encapsulated state. But the default does make it easy to get started with directives, as we saw in the previous chapter.

Now that we're familiar with custom directives, it's time to do the right thing. In this chapter we will learn how to manage directive state in an encapsulated way, permitting the safe reuse of custom directives.

Isolate scope

The term isolate scope is often cited as an example of Angular's difficult vocabulary. But as is often the case with computer science (as well as computer pseudo-science), the concept behind the fancy name is pretty easy to grasp.

Isolate scope just means giving the directive a scope of its own that does not inherit from the existing scope.

Before we get started, we need to take care of the usual setup. First, let's add Angular and Bootstrap to the page.

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.js"></script>

We must define an application module.

angular.module('app', []);

Then, we must load our root module by passing its name to ng-app.

<body ng-app="app">
  <!-- Other examples to be inserted here. -->
</body>

Now that Angular and our root module are properly configured, let's define a controller that exposes three string properties, each set to the name of the component instance that we expect to display it in the view. The first string is intended to identify the controller itself, and the other two strings are for two separate instances of a directive that we will define next.

angular.module('app')
  .controller('MessageController', function($scope) {
    $scope.message  = "the controller";
    $scope.message1  = "directive 1";
    $scope.message2  = "directive 2";
  });

Below is our directive. It also sets a message property on the scope instance that is passed to its link function.

angular.module('app') .directive('message', function() { return { template: "<p>hello, from {{message}}</p>", link: function(scope, element, attrs) { scope.message = scope.$eval(attrs.message); } }; });

The usage of scope.$eval to access a scope property passed as a string argument to a directive is explained in the previous chapter. What we are particularly interested in here is what happens to the scope.message property referenced by the controller and the two instances of the directive. We'll add inputs bound to the three scope properties set in the controller.

Before you look at the example usage and its output below, glance once more over the controller and directive shown above. Do you see any potential for conflict?

<div ng-controller="MessageController"> <h4>hello, from {{message}}</h4> <input class="form-control" ng-model="message"> <input class="form-control" ng-model="message1"> <input class="form-control" ng-model="message2"> <br> <div class="well" message="message1"></div> <div class="well" message="message2"></div> </div>

By the time the second usage of the directive has finished its work, the controller scope's message property has been set to "directive 2". If you type something into the top input, which is bound to message, you will see the change updated in the output of both directive instances. In this example, the controller scope is shared across the three instances, which leads to trouble. We do not want either directive instance to modify the scope shared by MessageController and the other directive instance. So, how do we give each directive instance its own, private, isolated scope? Can you guess the solution?

It is to declare a scope property on the directive's configuration object. The simplest option is to set this property to true, which gives the directive instances new scopes of their own. These new scopes inherit from the current scope, just as if the directive instances were controller instances.

angular.module('app') .directive('message', function() { return { template: "<p>hello, from {{message}}</p>", link: function(scope, element, attrs) { scope.message = scope.$eval(attrs.message); }, scope: true }; });

That fixes the initial state of each element. Are we done? No. Try typing in each of the inputs.

If we want updates to the controller scope properties message1 and message2 to be reflected in the directives, we have more to do. Because the link function is only run when a directive is first rendered, using $eval to set a directive property will not create a permanent two-way binding between the directive's output and the original model. Again, if you haven't already done so, I urge you to try it for yourself. Update "directive 1" and "directive 2" using the lower two inputs, and observed that the values are not updated in the directive output.

So, what should we add to our link function to set up the two-way binding? As it turns out, nothing. We should delete it. The imperative code that would be required in our custom link function is tedious, mundane, and predictable. Why not replace it with declarative configuration and automate the programming? That's exactly what the creators of Angular decided to do.

Isolate scope bindings

Directive scope bindings are declared using somewhat cryptic symbols, =, &, and @, that denote two-way binding, one-way binding, and text binding, respectively. Let's look at each binding option in turn, beginning with the useful two-way binding.

Two-way binding (=)

To solve the problem of updating the directives' output, we merely replace the imperative programming that we would have to write in a link function with a configuration object on the scope property.

angular.module('app') .directive('message', function() { return { template: "<p>hello, from {{message}}</p>", scope: { message: '=' } }; });

Notice that the value of the scope property is now an object instead of a true primitive boolean value. First of all, this has the effect of cutting off the scope inheritance that we relied on earlier. To see this in action, return to the previous example and replace scope: true with scope: {}. The link function in that example does not work correctly without access to properties on the parent scope.

In order to automate the work previously handled in the link function, the scope configuration object needs a property named for the local scope property that we wish to bind to an external scope property. That's a mouthful, but bear with me, it's pretty simple: Since our directive uses message in its template, we add message to the configuration object, with a value of =, which indicates that we want a two-way binding with whatever external scope property is passed by name to the message attribute.

Ok, I'll admit that it's not that simple. But it does work, as you can see if you use the inputs to update controller properties in the example above. Our message directive is finally complete.

One-way (evaluation) binding (&)

Sometimes your directives need a way to call functions on the enclosing scope even when provided with an isolated scope. Stated another way, they need to evaluate Angular expressions in the original context. For this, we have the one-way binding, or evaluation, option. Let's start with a controller that exposes the callback function kaboom.

angular.module('app')
  .controller('ClickController', function($scope) {
    $scope.message = "Waiting...";
    $scope.kaboom = function() {
      $scope.message = "Kaboom!";
    }
  });

The example below shows how the ClickController, above, might be used in a template along with a generic bootstrap-button directive that can be configured to call the kaboom function when its button is clicked.

<div ng-controller="ClickController"> <h4> {{message}} </h4> <bootstrap-button the-callback="kaboom()"></bootstrap-button> </div>

Let's code the bootstrap-button directive. Within its simple template, we use Angular's built-in ng-click directive to bind the click event to a callback function.

<button type="button" class="btn btn-default" ng-click="theCallback()">
  Submit
</button>

Since our generic directive can't know the name of the actual callback function that will be invoked, we randomly choose theCallback as a name for the function. Fine, but how do we bind theCallback (actually a scope property) to kaboom?

Not a problem, dear reader.

angular.module('app')
  .directive('bootstrapButton', function() {
    return {
      restrict: 'E',
      scope: {
        theCallback: '&'
      },
      templateUrl: '/views/button.html'
    }
  });

The ampersand (&) option sets up the binding for us. Try it, it works.

What about the final configuration option, @, which we introduced above as the text binding option? In order to best demonstrate its usefulness, we need to divert our attention momentarily to another famous example of Angular's difficult vocabulary.

Transclusion

Transclusion certainly could have been given an easier name. For example, users of Ruby on Rails will recognize this functionality as something similar to yield. That's certainly a lot easier to understand.

In any case, to transclude content simply means to yield rendering control back to the client of a directive. The client in this case is a template, which invokes the directive. If this metaphor doesn't work for you, you can also think of transclusion as cut what the template has put inside of the directive, and paste it here. The where of the paste it here part depends on the use of a special built-in directive, ng-transclude, that you place somewhere within your custom directive's template.

For a very simple example, let's say we have a box directive that we intend to invoke around some content, by placing it on an enclosing div element.

<div box>
  This <strong>content</strong> will be <em>wrapped</em>.
</div>

What does this box directive do? It simply wraps the enclosed content in a p element with some Bootstrap styles. There are two parts to using transclusion. First, we must add transclude: true to the directive's configuration. Second, we must use the ng-transclude directive someplace in the directive's template, in order to designate where to insert the client-supplied content.

angular.module('app')
  .directive('box', function() {
    return {
      template: '<p class="bg-primary text-center" ng-transclude></p>',
      transclude: true
    };
  });

Simple, right? Of course.

For this example, it would be even simpler to not use a directive at all, and just add the CSS styling to the enclosing element instead. Therefore, for a more realistic example, we will use transclusion to encapsulate the boilerplate for a Bootstrap panel with heading. Our panel directive will allow the client to both set the text for the title bar at the top of the panel, as well as supply the content to be placed in the body of the panel. For the body content, transclusion will be employed exactly as in the previous example. However, for the title text, we will use the remaining isolate scope binding option that we left uncovered.

Text (interpolation) binding (@)

When all you need to do is copy a string argument to your directive's scope, a simple one-way text binding (@) is the declarative way to configure it. Here is the template for our directive. Our intention for the title property is to allow text for it to be set as an attribute on the directive.

<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> {{title}} <small> Static text in the directive template </small> </h3> </div> <div class="panel-body" ng-transclude></div> </div>

In our directive's configuration, we restrict its usage to elements, and declare the binding of the title attribute to a title property on its scope.

angular.module('app')
  .directive('panel', function() {
    return {
      restrict: 'E',
      templateUrl: '/views/panel.html',
      scope: {
        title: '@'
      },
      transclude: true
    };
  });

Below is an example using panel to set both the static text for the title at the same time that two-way bindings are used in the transcluded content.

<div ng-controller="MessageController"> <panel title="Static text in the client template"> <h3> <em>Hello</em>, from <strong>{{message}}</strong> </h3> <input class="form-control" ng-model="message"> </panel> </div>

The interpolation of the title using @ is only appropriate for strings that will not be modified, as this option will not set up a watch to monitor the original property for changes.

Conclusion

This chapter may be the most difficult material in this book, depending on your experience, so congratulations on finishing it. You should now be able to define custom directives that can be used without fear of side effects arising from shared state, and that is a big step toward your complete mastery of Angular directives. At this juncture we must turn our attention away from the nuances of using directives, however, and toward a vital topic in client-side web development: routing and URLs.

I hope you have been enjoying Angular Basics.
You can join the mailing list to hear about updates to the book.
Please also let me know what you liked and what I can improve.
And please share the word using the social buttons below!