The Hitchhiker’s Guide to the Directive


AngularJS Directive is what we have close to Web Components at this time and is the most crucial and difficult piece in AngularJS. I’ve not found any suitable article on the web to fully understand directives behavior based on options it takes so I’m writing one. Fingers crossed!

As you may know that directives can be used as an element, an atttribute, a class name, and a comment. Here are the ways we can use our directive angular in HTML:

<div angular></div>
<div class="angular"></div>
<angular></angular>
<!-- directive: angular -->

For a huge name such as this, you can split the words using hyphens while declaring and camel cased it while defining a directive.

<div the-quick-brown-fox-jumps-over-the-lazy-dog></div>
App.directive('theQuickBrownFoxJumpsOverTheLazyDog', function() {... });

A Quick Demo to load AngularJS logo using the above directive. For your reference, I’m simply injecting an IMG tag here.

    var App = angular.module('App', []);

    App.directive('angular', function() {
      return {
        restrict: 'ECMA',
        link: function(scope, element, attrs) {
          var img = document.createElement('img');
          img.src = 'http://goo.gl/ceZGf';


          // directives as comment
          if (element[0].nodeType === 8) {
            element.replaceWith(img);
          } else {
            element[0].appendChild(img);            
          }
        }
      };
    });

How Directive works?

In short, “AngularJS when bootstrapped, looks for all the directives built-in as well as custom ones, compiles them using $compile() method (which keeps track of all the directives associated with an element, sorts them by priority and produces their link function), and links with scope (created by ng-app, ng-controller, ng-include, etc) by registering listeners or setting up $watches resulted in 2 way data bindings between the scope and the element.”

Blueprint?

We’ll take a look at it briefly soon but meanwhile, here is the Directive definition:

var myModule = angular.module(...); 
myModule.directive('directiveName', function (injectables) {
  return {
    restrict: 'A',
    template: '<div></div>',
    templateUrl: 'directive.html',
    replace: false,
    priority: 0,
    transclude: false,
    scope: false,
    terminal: false,
    require: false,
    controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
    compile: function compile(tElement, tAttrs, transclude) {
      return {
        pre: function preLink(scope, iElement, iAttrs, controller) { ... },
        post: function postLink(scope, iElement, iAttrs, controller) { ... }
      }
    },
    link: function postLink(scope, iElement, iAttrs) { ... }
  };
});

Now we’ll explore each option aforementioned one by one. You will find working demos along the way as well.

Compile function

For an AngularJS newbie (including myself), its quite confusing to understand compile and link functions in the first place. I initially thought that we can use both the functions in a directive simultaneously which is completely wrong. In fact, a compile function produces a link function. This is the place where you can do the DOM manipulation mostly. This takes 2 arguments by default:

  • tElement – A template element the directive has been declared on.
  • tAttrs – A list of attributes associated with the tElement.

In addition, you can inject transcludeFn, directiveController, etc. in it. Here is the code snippet for your reference:

<div compile-check></div>

Notice that, tElement is a jquery object so you do not have to wrap it in $ again.

   App.directive('compileCheck', function() {
      return {
        restrict: 'A',
        compile: function(tElement, tAttrs) {
          tElement.append('Added during compilation phase!');
        }
      };
    });

Link function

Its job is to bind a scope with a DOM resulted in a 2-way data binding. You have access to scope here unlike compile function so that you can create custom listeners using $watch method. As expected it takes 3 arguments:

  • scope – A scope to be used by the directive.
  • element – An element the directive is bound to.
  • attrs – A list of attributes associated with the element.

Similar to compile function, you can inject transcludeFn, directiveController, etc. in it.

Why there is a need for a compile function then?

The only reason for the existence of a compile function is performance. Take an example of a directive which consumes a collection and lays out DOM for each item of the collection (similar to ng-repeat). So, if we somehow chain the DOM for each item with a scope then it’ll slow down the whole process because as soon as the scope is created and bound to the DOM – it started watching for change in data. You can imagine how terribly it’ll degrade the performance. This problem is resolved by having 2 way process, a compile method which runs only once for each item and a link method which runs every time the model changes. So, all items will be compiled first and linked later altogether.

Restrict – to be or not to be?

This simply restricts the way you can define a directive. As we’ve seen before, we can restrict it as:

  • E: elements
  • A: attributes
  • C: class names (CSS)
  • M: comments

There are different ways to make it HTML5 compliant also. Below is the angular’s built-in directive named, ng-bind:

<span ng:bind="name"></span>
<span ng_bind="name"></span>
<span ng-bind="name"></span>
<span data-ng-bind="name"></span>
<span x-ng-bind="name"></span>

Template

This basically replaces the existing contents of an element. Imagine you want to load your github profile data in a nice card like format in many places on your website but do not want to repeat the same markup all over again. Also, your template can use data bound to $scope in a controller or $rootScope unless you have not set scope option (we’ll see it soon) explicitly in the directive definition.

In the below example, I’m simply fetching the angular.js repo information using the github API.

<div ng-controller="DemoCtrl">
  <div whoiam>This text will be overwritten</div>
</div>

In the directive, you can see that we have not created a new scope for the directive so that the data has been shared between the controller and the directive because its sharing the same scope. You can even override the values for header as well as footer within a link function. Go and break things.

var App = angular.module('App', []);

// Similar to $(document).ready() in jQuery
App.run(function($rootScope) {
  $rootScope.header = 'I am bound to rootScope';
});

App.controller('DemoCtrl', function($scope) {
  $scope.footer = 'I am bound to DemoCtrl';
});

App.directive('whoiam', function($http) {
  return {
    restrict: 'A',
    template: '{{header}}<div class="thumbnail" style="width: 80px;"><div><img ng-src="{{data.owner.avatar_url}}"/></div><div class="caption"><p>{{data.name}}</p></div></div>{{footer}}',
    link: function(scope, element, attrs) {
      $http.get('https://api.github.com/repos/angular/angular.js').success(function(data) {
        scope.data = data;
      });
    }
  };
});

TemplateUrl

If we keep adding more and more details to above card, maintaining the template markups within the directive’s definition will become complicated and unmanageable. So, its better to keep our template in a separate html file and reference its location in templateUrl. As the template loading is asynchronous in this case, so the compilation/linking will be delayed until the template is fully loaded, and will be cached in $templateCache for later use.

You can either maintain a separate file as mentioned before or wrap it in <script> tags just like handlebars does. Here is the syntax:

<script type='text/ng-template' id='whoiam.html'></script>

Make sure you set the proper type otherwise angular will look for the file (specified as id, in this case) on your file system.

In the following example, I’m showing more details on the card. Make a bee line for the demo.

<script type='text/ng-template' id='whoiam.html'>
    <div class="thumbnail" style="width: 260px;">
      <div><img ng-src="{{data.owner.avatar_url}}" style="width:100%;"/></div>
      <div class="caption">
        <p><b>Name: </b>{{data.name}}</p>
        <p><b>Homepage: </b>{{data.homepage}}</p>
        <p><b>Watchers: </b>{{data.watchers}}</p>
        <p><b>Forks: </b>{{data.network_count}}</p>
      </div>
    </div>
 </script>

Replace

In the below screenshot of the example we’d seen before, you can see the elements having angular directives on them, which are sometimes unnecessary after the directives are compiled by AngularJS.

Without replace option

Without replace option

Thats where replace option comes handy – If set to true will replace the element having a directive on it with a template. This is what you see after using replace: true. Check out the demo

With replace option

With replace option

You have to use templateUrl/template along with replace.

Priority

As I said earlier, AngularJS finds all directives associated with an element and processes it. This option tells angular to sort directives by priority so a directive having higher priority will be compiled/linked before others. The reason for having this option is that we can perform conditional check on the output of the previous directive compiled. In the below example, I want to add `btn-primary` class only if a div has `btn` class on it.

<div style='padding:100px;'>
  <div primary btn>The Hitchhiker’s Guide to the Directive Demo</div>
</div>

Please note that the default priority if not set will be zero. In this example, btn directive will be executed before primary. Play with the demo!

App.directive('btn', function() {
  return {
    restrict: 'A',
    priority: 1,
    link: function(scope, element, attrs) {
      element.addClass('btn');
    }
  };
});

App.directive('primary', function() {
  return {
    restrict: 'A',
    priority: 0,
    link: function(scope, element, attrs) {
      if (element.hasClass('btn')) {
        element.addClass('btn-primary');
      }
    }
  };
});

Terminal

As per the official documentation, If set to true then the current priority will be the last set of directives which will execute on an element. It holds true unless you use custom directives in conjunction with built-in directives having priority set on them such as ngRepeat, ngSwitch, etc. Instead all custom directives having a priority greater than or equal the current priority will not be executed in this case.

In the below example, first has a higher priority than second – which has terminal set to true. And if you set the lower priority to first – It will not be executed at all. But in case of no-entry directive, it will not be executed even though it has a higher priority than ng-repeat. Is it a bug? Is it because of transclusion used in ng-repeat? Need to dig in…

<div first second></div>

<ul>
    <li ng-repeat="item in ['one', 'two', 'three']" no-entry>{{item}} </li>
</ul>
    App.directive('first', function() {
      return {
        restrict: 'A',
        priority: 3,
        link: function(scope, element, attrs) {
          element.addClass('btn btn-success').append('First: Executed, ');
        }        
      };
    });

    App.directive('second', function() {
      return {
        restrict: 'A',
        priority: 2,
        terminal: true,
        link: function(scope, element, attrs) {
          element.addClass('btn btn-success').append('Second: Executed ');
        }        
      };
    });

    App.directive('noEntry', function() {
      return {
        restrict: 'A',
        priority: 1001,
        link: function(scope, element, attrs) {
          element.append('No Entry: Executed ');
        }        
      };
    });

Controller

This can be treated as a control room for a directive. You can either bind properties/methods to $scope available or this keyword. The data bound to this will be accessible in other directives by injecting the controller using require option. In below example, I’m toggling the state of a lightbulb so that child directives will know about the current state by calling getState() method – we’ll see it soon.

App.directive('powerSwitch', function() {
      return {
        restrict: 'A',
        controller: function($scope, $element, $attrs) {
          $scope.state = 'on';

          $scope.toggle = function() {
            $scope.$apply(function() {
              $scope.state = ($scope.state === 'on' ? 'off' : 'on');
            });
          };

          this.getState = function() {
            return $scope.state;
          };
        },
     };
});

Require

This lets you pass a controller (as defined above) associated with another directive into a compile/linking function. You have to specify the name of the directive to be required – It should be bound to same element or its parent. The name can be prefixed with:

  • ? – Will not raise any error if a mentioned directive does not exist.
  • ^ – Will look for the directive on parent elements, if not available on the same element.

Use square bracket ['directive1', 'directive2', 'directive3'] to require multiple directives

Here is a demo to turn a lightbulb on/off:

  <div class="btn btn-success" power-switch>
    {{'Switched ' + state | uppercase}}

    <div lightbulb class='bulb' ng-class="bulb"></div>
  </div>

The below code is self explanatory. She loves me, She loves me not!!

    var App = angular.module('App', []);

    App.directive('powerSwitch', function() {
      return {
        restrict: 'A',
        controller: function($scope, $element, $attrs) {
          $scope.state = 'on';

          $scope.toggle = function() {
            $scope.$apply(function() {
              $scope.state = ($scope.state === 'on' ? 'off' : 'on');
            });
          };

          this.getState = function() {
            return $scope.state;
          };
        },
        link: function(scope, element, attrs) {
          element.bind('click', function() {
            scope.toggle();
          });

          scope.$watch('state', function(newVal, oldVal) {
            if (newVal === 'off') {
              element.addClass('disabled');
            } else {
              element.removeClass('disabled');
            }
          });
        }  
      };
    });

    App.directive('lightbulb', function() {
      return {
        restrict: 'A',
        require: '^powerSwitch',
        link: function(scope, element, attrs, controller) {
          scope.$watch(function() {
            scope.bulb = controller.getState();
          });
        }
      };
    });

Scope

This is a scary part but do not worry. Setting scope will only create/maintain the hierarchy between the scope of an element and its parent scope but you can still access data bound to parents’ scopes.

scope: false

Is the default option which does not create a new scope for a directive but shares the scope with its parent. In this basic example to understand scopes, I’ve logged the scope of the directive to the console. You can see that the directive has borrowed the controller’s scope so its parent scope will be $rootScope in this case.

scope: false

scope: false

scope: true

Creates a new scope but prototypically inherits from the parent scope. In below screenshot, the directive has its own scope so that its parent scope will be the controller’s scope, not $rootScope.

scope: true

scope: true

scope: ‘isolate’

Creates an isolated scope which does not prototypically inherit from the parent scope but you can access parent scope using scope.$parent. Again basic demo!

How can I then share the data between isolated scope and its parent scope as scope.$parent is not much useful in case of templates?

Well, isolated scope takes an object/hash which lets you derive properties from the parent scope and bind them to the local scope. There are three ways to do that:

  • @ – binds the value of parent scope property (which always a string) to the local scope. So the value you want to pass in should be wrapped in {{}}. Remember `a` in braces.
  • = – binds parent scope property directly which will be evaluated before being passed in.
  • & – binds an expression or method which will be executed in the context of the scope it belongs.

In this example, I’m prefixing parent property with @, children property with = and shout() method with &.

  <div class='well'>
    Bound to $rootScope: <input type="text" ng-model="parent">
    <div child parent='{{parent}}' shout="shout()"></div>    
  </div>

  <div class='form well' ng-controller="OneCtrl">
    ng-controller="OneCtrl"<br/>
    <input type="text" ng-model="children"><br/>
    I am {{children}} and my grandfather is {{parent}}
    <div child parent="{{parent}}" children="children" shout="shout()"></div>    
  </div>
    var App = angular.module('App', []);

    App.run(function($rootScope) {
      $rootScope.parent = 'ROOT';

      $rootScope.shout = function() {
        alert("I am bound to $rootScope");
      };
    });

    App.directive('child', function() {
      return {
        restrict: 'A',
        scope: {
          parent: '@',
          children: '=',
          shout: '&'
        },
        template: '<div class="btn btn-link" ng-click="shout()">I am a directive and my father is {{parent || "NIL"}} as well as {{children || "NIL"}}</div>'
      };
    });

    App.controller('OneCtrl', function($scope) {
      $scope.children = 'The One';

      $scope.shout = function() {
        alert('I am inside ng-controller');
      };
    });

See this example in action.

Transclude

There may be a time when you want your directive to consume the existing content of an element into a template. Angular not only lets you transclude the DOM but also gives you a control to insert the transcluded DOM wherever you want using ngTransclude directive. This option tells angular to get ready for transclusion by providing transclude linking function available in a compile function of a directive. There are two ways to use transclude option in directives:

transclude: true

Inside a compile function, you can manipulate the DOM with the help of transclude linking function or you can insert the transcluded DOM into the template using ngTransclude directive on any HTML tag. Notice our old school marquee tag:

  <script type='text/ng-template' id='whoiam.html'>
    <div class="thumbnail" style="width: 260px;">
      <div><img ng-src="{{data.owner.avatar_url}}" style="width:100%;"/></div>
      <div class="caption">
        <p><b>Name: </b>{{data.name}}</p>
        <p><b>Homepage: </b>{{data.homepage}}</p>
        <p><b>Watchers: </b>{{data.watchers}}</p>
        <p><b>Forks: </b>{{data.network_count}}</p>
        <marquee ng-transclude></marquee>
      </div>
    </div>
  </script>

Please note that ngTransclude will not work in the template if transclude option is not set. Check out Working Demo.

   App.directive('whoiam', function($http) {
      return {
        restrict: 'A',
        transclude: true,
        templateUrl: 'whoiam.html',
        link: function(scope, element, attrs) {
          $http.get('https://api.github.com/repos/angular/angular.js').success(function(data) {
            scope.data = data;
          });
        }
      };
    });

transclude: ‘element’

This transcludes the entire element and a transclude linking function is introduced in the compile function. You can not have access to scope here because the scope is not yet created. Compile function creates a link function for the directive which has access to scope and transcludeFn lets you touch the cloned element (which was transcluded) for DOM manipulation or make use of data bound to scope in it. For your information, this is used in ng-repeat and ng-switch.

<div transclude-element>I got transcluded <child></child></div>

You can inject transcludeFn() in both compile as well as link functions but I’ve not seen the use of it in a link function yet. Demo

   App.directive('transcludeElement', function() {
      return {
        restrict: 'A',
        transclude: 'element',
        compile: function(tElement, tAttrs, transcludeFn) {
          return function (scope, el, tAttrs) {
            transcludeFn(scope, function cloneConnectFn(cElement) {
              tElement.after('<h2>I was added during compilation </h2>').after(cElement);
            });
          };
        }
      };
    });

    App.directive('child', function(){
      return {
        restrict: 'E',
        link: function($scope, element, attrs) {
          element.html(' with my child');
        }
      };
    });

Wrap up

I’ve learned a lot of things I was unaware of about AngularJS Directive while writing this post and I hope this will be useful for other lost souls as well. Its funny to say that it seems like AngularJS source code is itself written in Angular.

About these ads

80 thoughts on “The Hitchhiker’s Guide to the Directive

  1. Pingback: Currently Open Tabs: Resources For Using AngularJS - Anti-Code.com

  2. Pingback: Retour sur BreizhCamp 2013 ← JavaTeam Sodifrance

    • I wish I found this blog post the moment I started writing directives. I could have saved hours of wandering in circles. Thanks for the great post.

  3. Pingback: Collections of links that helps me learn AngularJS | Hacking & JavaScript

  4. Pingback: angularjs 学习大礼包大赠送【权威英文版】! _ Angular _ 小G的大Q

  5. Even though I posted the pingback to this on my site anti-code.com a while ago, I just finally read this completely lol. Too many projects and too many tabs open haha.

    I’m happy I finally did, I get directives better, and $scope. Still stuck on transclude though, wish there was a better explanation of that.and when to use it exactly.

    • I think the better example is ng-switch wherein the content is shown / completely removed conditionally. And hence transclude=”element” comes handy in this case to have a clone of the element to cache it somewhere before being removed. Later you can compile and inject it again.

  6. Pingback: Webdesign and Development Resources - Page 3

  7. Pingback: Frontend Development | espaco99

  8. Pingback: 最受关注的前端开源项目资源 – CSSwang-CSS网站

  9. Pingback: 直接拿来用!最火的前端开源项目 | HTML5 CSS3 JavaScript

  10. This will be a good reference for me now. Though I know how to write directives, not all features and their respective options are properly covered. Now any info is just a Ctrl+F away :)

  11. Pingback: 直接拿来用!最火的前端开源项目(一) | 农夫庄园

  12. Pingback: How I inspire my colleagues to learn AngularJS | codef0rmer

  13. Well written article Amit. But can you please explain the part in isolate scope – sharing data within isolate scope and parent scope

  14. I couldn’t understand the difference between ‘=’, ‘@’. playing around with jsfiddle, did help me understand – something like, if I change the directive to,

    scope: { scope: {
    parent: ‘=’, parent: ‘@’
    children: ‘@’, children: ‘=’
    }, }

    then the HTML would need to be changed as
    VS

    VS

      • Different between = and @ is very simple. = implies bidirection wherein you can update parent within a directive as it was passed by reference. @ lets you pass the $eval-uated value – passed by value.

  15. Awesome article. Directives take a while to properly understand because they’re complex, flexible and require an understanding of the framework. That said, this article really helped me; definitely the best documentation I’ve read on directives!

  16. Pingback: The Angular Way | Flippin' Awesome

  17. Pingback: The AngularJS Magazine – What’s New this Week? - Dan Wahlin

  18. Excellent information package, thank you. Browsing through many angularJS articles and tutorials, I would suggest you document what version number of angular you have been writing the above for; things seem still quite in flux.

  19. Pingback: Building Pluggable Components in AngularJS | codef0rmer

  20. This was divine. I took an hour to quietly go though your article and I believe I finally understand directives. Thank you.

    Do you happen to know if there’s a way to override a directive’s logic in another directives when you can’t use require (the functions are not exposed in the scope)?

  21. @codeformer, thanks for the great write up. It has clarified a lot that I was misunderstanding about Directives. If you don’t mind can you have a look at a dilemma I have regarding Directives and access to child elements: I’ve placed the URL in the website input below.

    Kind Regards,
    Bazmo

  22. @codef0rmer I’ve updated the question on stackoverflow with more detail. Sorry to bug you, but this is the only blog I’ve found that comes close to explaining what I want to do.

  23. Hello Amit, I am totally new to angular. I am finding it hard to understand the official angularJS documentation. But you have explained in such simple words and good examples. Thanks a lot.

  24. Pingback: Live Streaming Κάλυψη Συνεδρείων

  25. Pingback: Sobre Diretivas e como usar corretamente plugins jQuery com AngularJS » pedronauck.com

  26. Pingback: Sobre Diretivas e Como usar corretamente plugins jQuery com AngularJS | pedronauck.com

  27. In the terminal example, if you set the no-entry priority to 100, then it functions correctly. I think 1001 is too high

  28. Why does having more than one element in the template when replace:true stops the operation? For instance, with template: ‘123abc’ will not work with replace:true.

  29. Pingback: Angular, Links and resources (1) | Angel "Java" Lopez on Blog

  30. Pingback: web前端开发资源集合 – Demo笔记

  31. Thank you for writing this up!

    There’s one thing that popped out to me that could be misleading: “This problem is resolved by having 2 way process, a compile method which runs only once for each item and a link method which runs every time the model changes.”

    I know you’re in the context of talking about ng-repeat but saying that the link method runs every time the model changes could be confusing. As I understand it the link method is run when items are added to the model (and which will be added to the view), but link would not run when items are removed though the model has changed. I think the point is that the compile step produces a “template” (unfortunately that word is overloaded in this context) from which an instance is created by ng-repeat for each item in the model. When the instances are created from the template, the link function is called to associate the new item with its proper $scope.

    Also, I’m also not sure what you mean by “chain the DOM” a bit before this though as I understand it you are definitely right that the compile step is there so Angular doesn’t have to go through parsing the HTML every time it needs to create a new instance within ng-repeat as that would be a huge performance problem.

  32. Pingback: AngularJS-Learning | Nisar Khan

  33. Hello. Nice explanation. But an important comment to reflect changes between the version of angular you used in your ‘priority’ example and modern versions. Angular performs link and compile in priority order as stated in older versions. But in 1.2.16, the latest stable version, angular goes in reverse order when linking.
    From the docs “When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which the directives are applied. The priority is used to sort the directives before their compile functions get called. Priority is defined as a number. Directives with greater numerical priority are compiled first. Pre-link functions are also run in priority order, but post-link functions are run in reverse order. The order of directives with the same priority is undefined. The default priority is 0.”
    Thus I had to make directive ‘primary’ have a higher priority than directive ‘btn’. Hope this helps all those having fun learning Angular!!!

  34. Pingback: Frontend Development | Experiencias Alfredo Bravo Cuero

  35. Pingback: 最火的前端开源项目 - WEB开发笔记

  36. Pingback: The Angular Way - Modern Web

  37. Pingback: [Dev Tip] WHY ANGULARJS? | sudo man

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s