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.

Advertisements

An approach to use jQuery Plugins with AngularJS


Lets face it, we can not completely get rid of jQuery and its plugins ecosystem, even though Angular has a built-in subset of jQuery under the name jQLite. At one point or another, we often need some sort of jQuery plugins in our application and we can/should not port entire plugin into Angular world in order to use it but we can avoid the plugin initialization code to be scattered across.

How?

Simply by writing a directive for it.

I would like to give you a small demo of Toolbar.js which is a jquery plugin I found recently on geekli.st. This is how we create a tooltip style toolbar in jQuery:

<!-- Click this to see a toolbar -->
<div id="format-toolbar" class="settings-button">
    <img src="http://paulkinzett.github.com/toolbar/img/icon-cog-small.png">
</div>

<!-- Our tooltip style toolbar -->
<div id="format-toolbar-options">
	<a href="#"><i class="icon-align-left"></i></a>
	<a href="#"><i class="icon-align-center"></i></a>
	<a href="#"><i class="icon-align-right"></i></a>
</div>
<!-- Typical jQuery plugin invocation -->
$('#format-toolbar').toolbar({
	content: '#format-toolbar-options', 
	position: 'left'
});

Enter the dragon a.k.a Angular

We’ll keep our markup intact by just adding a custom attribute named `toolbar-tip` – which will be an angular directive we’ll going to write soon. So our markup will change to:

<div id="format-toolbar1" class="settings-button" toolbar-tip="{content: '#format-toolbar-options', position: 'top'}">
	<img src="http://paulkinzett.github.com/toolbar/img/icon-cog-small.png">
</div>

One thing to notice here is that I’ve moved all the options of a toolbar into an HTML so that we can use the same directiv anywhere else with different options/settings.

Finally,

<script>
var App = angular.module('Toolbar', []);

App.directive('toolbarTip', function() {
	return {
		// Restrict it to be an attribute in this case
		restrict: 'A',
		// responsible for registering DOM listeners as well as updating the DOM
		link: function(scope, element, attrs) {
		    $(element).toolbar(scope.$eval(attrs.toolbarTip));
		}
	};
});
</script>

Woohoo! Is not that awesome?? Everything is simple, maintainable and testable!!!

Demo

http://jsfiddle.net/codef0rmer/TH87t/

Using jPlayer in your Angular application


Recently I was working on my personal project called eShell – A single page application often used in E-Learning businesses to run interactive templates. You can check it out on my github profile.

AngularJS makes it pretty easy to place generic code at once place and use it across an app without duplicating the same  over and over again which is most likely to be the case when you write it in jQuery.
So, lets dive in to write a jPlayer directive:

Bootstrap an App

Add some dummy audio files.

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

App.run(function($rootScope) {
      $rootScope.audio1 = 'http://s0.vocaroo.com/media/download_temp/Vocaroo_s0PPjt87YRjJ.mp3';
      $rootScope.audio2 = 'http://s1.vocaroo.com/media/download_temp/Vocaroo_s1M4Uvki8bCP.mp3';
});

Add a custom HTML tag/attribute

Set up a DOM where you want your player to appear. The data-audio attribute holds the audio path defined in App.run above.

<div class='speaker' data-audio="audio1" data-autoplay="true" data-pauseothers="true" jplayer></div>

<!-- Custom HTML Tags are not supported in IE6,7,8 by default, try using html5shiv.js -->
<jplayer class='speaker' data-audio="audio2" data-autoplay="false" data-pauseothers="false"></jplayer>

Writing a directive in AngularJS

Finally, we’ve to write code for a directive named jplayer to make above code functional.

App.directive('jplayer', function() {
      return {
        restrict: 'EA',
        template: '<div></div>',
        link: function(scope, element, attrs) {
          var $control = element,
              $player = $control.children('div'),
              cls = 'pause';

          var updatePlayer = function() {
            $player.jPlayer({
              // Flash fallback for outdated browser not supporting HTML5 audio/video tags
              // http://jplayer.org/download/
              swfPath: 'js/jplayer/',
              supplied: 'mp3',
              solution: 'html, flash',
              preload: 'auto',
              wmode: 'window',
              ready: function () {
                $player
                  .jPlayer("setMedia", {mp3: scope.$eval(attrs.audio)})
                  .jPlayer(attrs.autoplay === 'true' ? 'play' : 'stop');
              },
              play: function() {
                $control.addClass(cls);

                if (attrs.pauseothers === 'true') {
                  $player.jPlayer('pauseOthers');
                }
              },
              pause: function() {
                $control.removeClass(cls);
              },
              stop: function() {
                $control.removeClass(cls);
              },
              ended: function() {
                $control.removeClass(cls);
              }
            })
            .end()
            .unbind('click').click(function(e) {
              $player.jPlayer($control.hasClass(cls) ? 'stop' : 'play');
            });
          };

          scope.$watch(attrs.audio, updatePlayer);
          updatePlayer();
        }
      };
 });

Live Demo

http://plnkr.co/C7R4r0