Angular's built-in directives, such as ng-bind, ng-model, and input, have played a starring role in this book since the very first chapter, Basics. In fact, the Collections chapter focuses almost entirely on a single (quite powerful) directive, ng-repeat. It would seem that Angular's list of built-in directives would contain a solution for every situation, but of course this isn't true. While Angular was conceived as a way to enable non-programmers to build dynamic web pages, it evolved into a platform that gives web developers the power to extend and customize HTML. This means creating your own custom directives.

component

Angular version 1.5 introduced components, a simpler way to create directives that will probably prove to be the best approach if you are using version 1.5 or later. While the version of Angular running in the live examples in this book has been updated to support components, this chapter and the one that follows it were written before components arrived on the scene. Until I can add a new chapter covering components, I recommend reading Understanding Components in the official guide before continuing with this chapter.

directive

In its simplest form, creating a custom directive is very similar to creating a custom filter, which is why we learned about filters first, in the preceding chapter. The outer function that we pass to the directive function is a dependency injection wrapper and factory function, also known in the Angular documentation as a recipe. It is called at most once, or never if the directive is not used, as we learned in the chapter on Services. Inside this factory function is another function, which will be called by Angular. This inner function is where we do the actual work of the directive. It is called the link function.

The appropriately-named directive function for registering custom directives is part of the modules API, so we need to begin by creating a root module for our application.

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

We load our root module by passing its name to ng-app.

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

And with that, we're ready to create a directive. Let's start with a very simple directive named hello.

angular.module('app')
  .directive('hello', function() {
    return function(scope, element) {
      element.text("hello");
    };
  });

To do anything useful, we need to use the link function's second parameter, which is a DOM element wrapper from Angular's jqLite library or jQuery, depending on which you have included. In this example, we replace the element's previous content with the text hello.

The message is <span hello></span>.

Isn't that awesome? We just created our very own custom HTML attribute.

scope

A hardcoded string isn't very useful, so let's see if we can replace it with a dynamic value. You were probably wondering about the first parameter, scope. Let's add a controller that sets a message property on its scope.

angular.module('app')
  .controller('MessageController', function($scope) {
    $scope.message = 'hello';
  });

Now, let's use that property in a directive. We'll name the directive message as well.

angular.module('app')
  .directive('message', function() {
    return function(scope, element) {
      element.text(scope.message);
    };
  });

Obviously, this directive must be used in a DOM location where the scope belonging to a MessageController is available.

<p ng-controller="MessageController">
  The message is <span message></span>.
</p>

You may be wondering why a controller function declares a scope parameter that is prefixed with a dollar symbol ($scope), while the link function lists one without (scope). The reason is to distinguish dependency injection parameters from ordinary parameters. Unlike injected parameters, the link function parameter names are not significant except to us humans.

Arguments

As you know from using Angular's built-in directives, directives can accept arguments. Arguments are passed in as the link function's third parameter, attrs. Again, unlike injection dependencies, you can name the link function's parameters as you like, but for your own sanity, it's best to follow established convention.

welcome-message to just plain message?

angular.module('app')
  .directive('welcomeMessage', function() {
    return function(scope, element, attrs) {
      element.text(attrs.welcomeMessage);
    };
  });

The message is <em welcome-message="bonjour"></em>.

Notice that when we use more than one word for our directive name, we use the camel case style to identify it in JavaScript, but a hyphenated style for our HTML attribute. That is to say, if you want to use welcome-message to invoke your directive in your templates, you need to name it welcomeMessage when you define it in JavaScript.

You may have noticed that some of the built-in Angular directives evaluate expressions. However, this behavior is not the default.

The message is <em welcome-message="'bon' + 'jour'"></em>.

Of course, since we are working within an Angular template, we can always pass in the result of an evaluated expression.

The message is <em welcome-message="{{'bon' + 'jour'}}"></em>.

You can also interpolate an expression within a regular string string.

<p ng-init="greeting = 'bonjour'"> The message is <span welcome-message="I say {{greeting}}, you say hello"></span>. </p>

Demonstrate how to escape safe content, like an em wrapping greeting, above.

In order to evaluate expressions that are passed as ordinary strings to your directive, you will need to use $eval.

scope.$eval

Although Angular does not treat the argument to a directive attribute as an expression to be evaluated, we can easily do it ourselves with the scope's $eval method. This approach will not update the view if the model changes, but it works for basic use cases. Mention and link $watch from prev chpt, as well as scope bindings in next chpt

angular.module('app')
  .directive('welcomeMessage', function() {
    return function(scope, element, attrs) {
      var result = scope.$eval(attrs.welcomeMessage);
      element.text(result);
    };
  });

Let's stick with the simple use case of concatenating string literals.

The message is <em welcome-message="'bon' + 'jour'"></em>.

Fantastic. It works.

With scope.$eval, we can also pass in objects, giving us a way to accept multiple arguments. Later in the chapter we will look at element directives, which also accept multiple named arguments by supporting multiple attributes on the element. But by evaluating an options object, we have a great way to pass multiple arguments to an attribute directive.

angular.module('app')
  .directive('welcomeMessage', function() {
    return function(scope, element, attrs) {
      var options = scope.$eval(attrs.welcomeMessage);
      var result = options.emoticon + ' ' + options.message + ' ' + options.emoticon;
      element.text(result);
    };
  });

We are now effectively accepting two arguments, message and emoticon, and performing the concatenation in the directive.

The message is <em welcome-message="{message: 'bonjour', emoticon: '\u263a'}"></em>.

We now understand how to use custom directives together with scopes and controllers. But what if we want to use other services from within our directive?

Dependency injection

If we need access to a managed resource such as a service or filter within our directive, we merely need to declare the dependency as a parameter on the outer function. (Review the chapters Dependency Injection and Services if this concept is unfamiliar.)

For example, imagine that we need a directive that can apply a discount as well as currency formatting to a price value on the scope. The directive will need access to a custom calculateDiscount service, defined as follows.

angular.module('app')
  .value('discountRate', 0.8)
  .factory('calculateDiscount', function(discountRate) {
    return function(amount) {
      return amount * discountRate;
    };
  });

Using our enclosing recipe wrapper function, we simply inject the calculateDiscount service as well as the built-in currency filter.

angular.module('app')
  .directive('discount', function(calculateDiscount, currencyFilter) {
    return function(scope, element, attrs) {
      var price = scope.$eval(attrs.discount);
      var discountPrice = calculateDiscount(price);
      element.html(currencyFilter(discountPrice));
    };
  });

Notice that we pass the price property as an argument to the directive in the template. As explained above, this argument is available to us on the attrs parameter as a property named for our directive (discount). To keep things clear, we assign the value for this scope property (which we obtain with scope.$eval) to a local variable named price.

<table ng-init="price = 100">
  <tr>
    <td>Full price:</td>
    <td ng-bind="price | currency"></td>
  </tr>
  <tr>
    <td>Sale price:</td>
    <td discount="price"></td>
  </tr>
</table>

You could argue that a transformation as simple as this one should be a filter rather than a directive, since it does not involve adding markup or interactive behavior to the DOM. Fair enough. Let's move on to a more complex example that requires us to add markup using a template.

Templates

So far our operations on the element parameter have been quite simple. But what if we want to append a sizable chunk of new content to the DOM?

Let's approach the problem progressively. For starters, say we just want to wrap a text argument within a single strong element.

The message is <span strong-message="a strong hello!"></span>

We can always use string concatenation, of course.

angular.module('app')
  .directive('strongMessage', function() {
    return function(scope, element, attrs) {
      element.html('<strong>' + attrs.strongMessage + '</strong>');
    };
  });

Or we can build content programmatically using jqLite or jQuery.

angular.module('app')
  .directive('strongMessage', function() {
    return function(scope, element, attrs) {
      element.html('<strong>');
      element.find('strong').text(attrs.strongMessage);
    };
  });

Concatenation and programmatic DOM manipulation are fine for small stuff like a single element. What about when the content involves several nested elements? Or a dynamically-sized list of elements? As any experienced front-end developer will tell you, a templating library makes the job much easier. Now, where to get one of those... Hmmmm... Wait, this is Angular, don't we have a templating library?

Of course we do! At the same time that Angular is rendering our application template, it can render nested templates supplied by our directives.

To add a template to our directive, we need to switch from returning only the link function to returning a configuration object that contains both the link function and a template. This is a big change, so let me repeat it: Our recipe wrapper will now return an object rather than a function. The function that we used to return will be nested as the link property on this object.

Here is an example that uses a template to create an arbitrary number of li elements.

angular.module('app') .directive('wordList', function() { return { link: function(scope, element, attrs) { scope.words = attrs.wordList.split(" "); }, template: "<li ng-repeat='word in words'>\ {{word}}\ </li>" }; });

Angular will handle invoking the template with the proper scope object and appending the output to the directive's element. This particular directive's output should be appended to a list element, so let's invoke the directive on a ul element.

<ul word-list="we love templates"></ul>

I think this example begins to demonstrate the power and elegance of custom directives.

templateUrl

Even in the simple example above, which uses two backslashes (\) to format a three-line template string, inline templates become quickly awkward in JavaScript source code. Angular supports external templates, either as special, hidden DOM elements or as completely separate web resources.

<li ng-repeat="word in words"> {{word}} </li>

We have extracted our template code to a web resource located at the relative URL /views/word-list.html. In the configuration object for our directive, we provide a templateUrl property with this path, rather than a template property containing the template itself as text.

angular.module('app')
  .directive('wordList', function() {
    return {
      link: function(scope, element, attrs) {
        scope.words = attrs.wordList.split(" ");
      },
      templateUrl: '/views/word-list.html'
    };
  });

Usage is exactly the same.

<ul word-list="external templates rock"></ul>

External templates are generally much easier to read and edit than inline ones.

Replace

The directive in our last example must be used on a list, but what if we don't trust its clients to use it correctly? One solution is to replace the element upon which the directive is invoked, rather than append to it. Let's add an enclosing ul element to our template. We also need an additional configuration property, replace, which we will set to true. (The default value for replace, obviously, is false.)

First, let's add the ul element to our template.

<ul> <li ng-bind="word" ng-repeat="word in words"></li> </ul>

The only significant change to our directive example is the addition of the replace property.

angular.module('app')
  .directive('wordList', function() {
    return {
      link: function(scope, element, attrs) {
        scope.words = attrs.wordList.split(" ");
      },
      templateUrl: '/views/word-list-ul.html',
      replace: true
    };
  });

Since the element on which we invoke the directive will be replaced anyway, we can invoke the directive on something as wildly inappropriate as h1.

<h1 word-list="lists not headlines"></h1>

This example demonstrates the power that a directive has over the DOM, but I recommend favoring a more explicit, semantically-correct design whenever possible. Rather than replacing markup, simply validate that the element on which the directive is being invoked is semantically correct, and otherwise throw an error.

Controller

Toward the beginning of the chapter, we saw an example in which a controller prepared a scope property that we later used inside a directive's link function. Now that we know how to use templates, let's rewrite the example, replacing the link function that sets text on the DOM element with a template.

angular.module('app') .controller('MessageController', function($scope) { $scope.message = 'hello, from the external controller'; }) .directive('message', function() { return { template: "<strong>{{message}}</strong>" }; });

Since the controller and the directive are separate, both need to be invoked in the following example. The controller can be placed on an enclosing element, or on the same element as the directive, as shown.

The message is <span message ng-controller="MessageController"></span>.

Yes, as demonstrated in the example above, the link function is optional and may be omitted. The early examples of this chapter all used a link function because programmatic manipulation of the DOM is more familiar to many front-end developers. Now that we have transitioned to using templates, we will only need it for tasks in which we need access to the directive's element or the element's attributes (including the directive attribute, as we have seen.)

Also, although we demonstrated in the templates section that it is possible to prepare the model inside a link function, model preparation that does not require access to either the element or its attributes is more correctly a controller's responsibility. Thus, we are on the right track in moving this responsibility to a controller. The problem is that the controller is separate, requiring extra configuration. Can we move the controller inside the directive? Yes, we can.

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

Not only is this now a nice, well-encapsulated component, we also get to drop the ng-controller invocation from our markup.

The message is <span message></span>.

Of course, we can do all the usual things with the controller, such as defining a function on the scope and invoking that function from a control element within the template. Please refer to the Controllers chapter for examples.

Restrict

There are four ways to invoke a directive. Until now, I have only shown directives invoked as HTML attributes, since this style is by far the most useful and common. It is also the default value for the restrict option, which may be set to one or more of the following letter codes:

  • 'A' - Attribute
  • 'C' - Class
  • 'E' - Element
  • 'M' - Comment

Of the remaining three styles—class, element, and comment—only the element style is of practical interest. The class and comment styles exist to support fringe cases such as a requirement for strict HTML validation.

I consider the element directive a big gun that should only be brought out when the situation merits it. Typically, this is a use case in which you must bundle a substantial amount of markup into an HTML component that must accept multiple arguments. With an element directive, the parameters for these arguments can be expressed as the element's attributes.

What is a substantial amount of markup? In the case of our example, it will be a table with all the trimmings.

<table class="table table-condensed"> <thead> <tr> <th ng-bind="column" ng-repeat="column in columns"></th> </tr> </thead> <tbody> <tr ng-repeat="model in models"> <td ng-bind="model[column]" ng-repeat="column in columns"></td> </tr> </tbody> </table>

In our directive definition, the link function will utilize two arguments, accessed through attrs.models and attrs.columns.

angular.module('app')
  .directive('modelsTable', function() {
    return {
      restrict: 'E',
      templateUrl: '/views/models-table.html',
      link: function(scope, element, attrs) {
        scope.models = scope.$eval(attrs.models);
        scope.columns = attrs.columns.split(",");
      }
    };
  });

You might want to set replace: true on an element directive if non-standard element names cause issues in your environment. On the other hand, leaving the custom element in your DOM may assist with debugging.

We'll go back to using an external controller in this example.

angular.module('app')
  .controller('ItemsController', function($scope) {
    $scope.items = [
      {name: 'Item 1', color: 'green', price: 5.0},
      {name: 'Item 2', color: 'blue', price: 4.93}
    ];
  });

Finally, to make our remaining examples look a little better, let's add 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>

This has been a lot of preparation for an Angular example, but we're almost done. The final step is to invoke our custom models-table element directive with its models and columns attributes.

<p ng-controller="ItemsController">
  <models-table models="items" columns="name,color,price"></models-table>
</p>

Voila! The boilerplate of the HTML table has been neatly hidden away.

Conclusion

The knowledge you have gained in this chapter about directives will bring serious power to your client-side application development with Angular. As we saw in the final example, you now know how to define custom directives that build upon the power of Angular's templates and its built-in directives, with the potential to greatly reduce boilerplate and amplify productivity.

However, there is more to understand about directives, especially when the issue of shared state enters the picture. The next chapter introduces and addresses these more advanced scenarios. If you feel you know enough to serve your needs for now, feel free to skip ahead to the HTTP chapter. Otherwise, continue straight on to the second chapter on directives.

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!