Angular Directives

Angular Directives

Have you ever been jealous of Angular's ability to create new names for HTML tags and attributes? Directives like ng-repeat and ng-model make Angular incredibly useful. As it turns out, you can define your own custom directives with angular. There are a couple main reasons why you would want to create a custom directive:

  1. You want to use the same HTML code in multiple places in your app.
  2. You want to add special functionality/listeners to DOM elements.

Directives as Reusable Elements

Because a directive is just a special HTML tag name or attribute (or class name), it can be used anywhere that you write HTML. For example, you may want to show your name and contact information on every page of your app. To accomplish this you decide to use a directive:

// app.js
angular.module('myApp', [])

.directive('myInfo', function () {
    return {
        template: '<span>Warren Buffett -- reach me at warren.buffett@gmail.com</span>'
    };
});
// somewhere in your html code
<my-info></my-info>

Angular will insert your template code into your my-info tag. You can also use directives as HTML attributes, like this:

<div my-info></div>

It is also possible to use a directive as a class name, but this is generally not good practice:

<div class="my-info"></div>

Note: This can only be done if the restrict option on the directive is set to 'C'.

Directive Types

If you want your directive to be used only as an HTML attribute, you can specify that using the restrict option. You directive declaration would look something like this:

.directive('myInfo', function () {
    return {
        template: '<span>Warren Buffett -- reach me at warren.buffett@gmail.com</span>',
        restrict: 'A'
    };
});

With the restrict option set to 'A', the directive would only work if referenced like this:

<div my-info></div>

Here are the different uses for restrict from the Angular docs:

The restrict option is typically set to:

  • 'A' - only matches attribute name
  • 'E' - only matches element name
  • 'C' - only matches class name
  • 'M' - only matches comment with the following format: <!-- directive: directive-name -->

These restrictions can all be combined as needed:

  • 'AEC' - matches either attribute or element or class name

-- Angular Docs

Directive Templates

Directive templates can be in string format, but if they are large they should be included in separate HTML files. This is accomplished with the templateUrl option:

// app.js
angular.module('myApp', [])

.directive('myInfo', function () {
    return {
        restrict: 'E',
        templateUrl: 'info.html'
    };
});
// info.html
<span>Warren Buffett -- reach me at warren.buffett@gmail.com</span>

Directives with Controllers

The great thing about directives is that they can be placed anywhere in HTML, including within the scope of a controller:

// app.js
angular.module('myApp', [])

.controller('myController', function ($scope) {
    $scope.name = 'Warren Buffett';
    $scope.email = 'warren.buffett@gmail.com';
})

.directive('myInfo', function () {
    return {
        template: '<span>{{name}} -- reach me at {{email}}</span>',
        restrict: 'E'
    };
});
// html file
<div ng-controller="myController">
    <my-info></my-info>
</div>

If the $scope changes, the directive will change as well.

Directive Scope

Directives tied to a controller $scope might be useful with certain controllers, but it would be better if the data passed to a directive wasn't tied in a strict way to the $scope. For example, what if you want to display the information of multiple users at the same time? You could make two different controllers that are side-by-side and have different data. But that would be messy. Instead, you could pass different data to two different instances of the same directive. To do this, you must modify the scope of the directive. Each directive has its own individual scope. (This scope will be the same as its containing controller if not specified otherwise.) Directive scope is assigned using the scope option:

// app.js
angular.module('myApp', [])

.controller('myController', function ($scope) {
    $scope.warren = {
        name: 'Warren Buffett',
        email: 'warren.buffett@gmail.com'
    };
    $scope.dieter = {
        name: 'Dieter Rams',
        email: 'dieter.rams@vitsoe.com'
    };
})

.directive('myInfo', function () {
    return {
        restrict: 'A',
        scope: {
            'contactInfo': '=myInfo'
        },
        template: '<span>{{contactInfo.name}} -- reach me at {{contactInfo.email}}</span>'
    };
});
// html file
<body ng-controller="myController">
    <div my-info="warren"></div>
    <div my-info="dieter"></div>
</body>

Giving each directive its own scope allows you to use directives in ng-repeat!

Functional Directives

The second reason to create a directive is to manipulate the DOM in a way that is not available out-of-the-box. Manual DOM manipulation should always be done through directives, not controllers or anything else. The directive API offers a function called link to aid in manipulating the DOM.

Here is the definition of link from the docs:

Directives that want to modify the DOM typically use the link option. link takes a function with the following signature, function link(scope, element, attrs) { ... } where:

  • scope is an Angular scope object.
  • element is the jqLite-wrapped element that this directive matches.
  • attrs is a hash object with key-value pairs of normalized attribute names and their corresponding attribute values.

In other words, scope is the same scope object that you're already used to. element is essentially a jQuery object relating to the directive element in the DOM, so you can call functions like .on('click', ...) on it. attrs is just an object which stores the values of HTML attributes on the directive element.

Angular does not come with an ng-enter directive for input fields, so the only way to know when the enter key has been pressed is to use ng-submit on a form. That is not always a good solution, so I'm going to implement a myEnter directive:

// app.js
angular.module('myApp', [])

.controller('myController', function ($scope) {
    $scope.greet = function () {
        $scope.greeting = 'Hello, ' + $scope.name;
    };
})

.directive('myEnter', function () {
    var link = function (scope, element, attrs) {
        // bind is a jQuery function
        element.on('keydown', function (event) {
            // check which key was pressed (the enter key has a code of 13)
            if (event.which === 13) {
                scope.$apply(function () {
                    // run the code passed to my-enter in the html
                    scope.$eval(attrs.myEnter);
                });
                event.preventDefault();
            }
        });
    };


    return {
        restrict: 'A',
        link: link
    };
});
// html
<input type="text" my-enter="greet()" ng-model="name"> {{ greeting }}

Parts of this code might seem confusing on first examination. To learn more about scope.$apply visit this article:
http://www.sitepoint.com/understanding-angulars-apply-digest/