AngularJS directives

One cool feature of AngularJS is that it allows you to create custom HTML elements. You can achieve this by writing directives which are invoked when a certain DOM element is created. In other words, you get to intoduce your own HTML elements and attributes. Before we jump into the code, however, here are 4 different types of restrictions that you can apply to AngularJS directives (otherwise known as EACM):

  • restrict: ‘E’ - a DOM element with a certain (custom) name, e.g. <my-directive></my-directive>
  • restrict: ‘A’ - a DOM element containing a custom attribute, e.g. <div my-directive="exp"></div>
  • restrict: ‘C’ - invocation via a class, e.g. <div></div>
  • restrict: ‘M’ - invocation via a comment: <!-- directive: my-directive exp -->

Pretty cool huh?

Before you begin, please download the code, so you can run these examples while reading this tutorial.

A basic directive

Here is the Javascript of a basic directive:

angular.module('ng').directive('testElem', function () {
    return {
        restrict: 'A',
        template: '<div><h1>hello...</h1><p ng-repeat="obj in arr">{{obj}}</p></div>',
        //templateUrl: '/partials/template.html',
        link: function (scope, iterStartElement, attr) {
            $(".mydirectiveclass").css({'background-color' : 'yellow'});
            scope.arr = ["mikhail", "is", "the", "best"];
        }
    };
});

I know what you are thinking. Whaaaaat? Let me explain:

  • First line declares the directive “testElem”. See how E is capital. This means your element or attribute name will be “test-elem”. That’s right the
  • restrict: see explanation above. By the way, you can combine them too, for instance “EA”.
  • template: an HTML string of that the directive element will be replaced by.
  • templateUrl: optionally you can have the template HTML inside another file, especially it is a long one.
  • link: the linking function. After the template has been loaded, this function is called to establish the AngularJS scope and any last-minute effects such as jQuery animation or other logic. You may call this the heart of the directive, eventhough, in my humble opinion the heart is the template.

And here is the HTML code that would use this directive. Notice that you have to add ng-app in the body so that AngularJS knows to do its thing.

<!doctype html>
<html>
<head>
    <title>Directive Test</title>
    <script type="text/javascript" src="jquery.min.js" ></script>
    <script type="text/javascript" src="angular.min.js"></script>
    <script type="text/javascript" src="mydirectives.js"></script>
</head>
<body ng-app>
    <div test-elem></div>
</body>
</html>

Compile function

If you need to grab the content of your original funky elements such as

<funky-element>
    Parse me... come ooooon! Just parse meee!
</funky-element>

… the linking function will not allow you to do so. This is because, once again, linking happens AFTER the template has been applied. Solution: instead of link: function you need to specify a compile: function. Here is the kicker: when you define a compile function in your directive definition, the link function is ignored. Instead the link function will be assigned to what compile function returns. In other words, you need to return the linking function from the compile function. Wait… those are the same words… LOL! Here is the code though

angular.module('ng').directive('funkyElement', function () {
    return {
        restrict: 'E',
        transclude: true,
        scope: 'isolate',
        template: '<div>gonna parse this: {{orig}} <br/>... and get this: {{obj}}</div>',
        //templateUrl: 'template.html',
        compile:function (element, attr, transclusionFunc) {
            return function (scope, iterStartElement, attr) {
                var origElem = transclusionFunc(scope);
                var content = origElem.text();
                scope.orig = content;
                scope.obj = my_custom_parsing(content);
            };
        }
    };
});

Assuming my_custom_parsing  sticks stars between each character, the result HTML will be as follows:

<div>gonna parse this: Parse me... come ooooon! Just parse meee! <br/>
... and get this: P*a*r*s*e* *m*e*.*.*.* *c*o*m*e* *o*o*o*o*o*n*!* *J*u*s*t* *p*a*r*s*e* *m*e*e*e*!* *</div>

Now what the hell is transclude: true? And what is scope: ‘isolate’?

Transclusion

… is a funky word for “get my content into the template… HERE”. Actually its even simpler… It means define transclusionFunc. Like so

angular.module('ng').directive('testElemTransclude', function () {
    return {
        restrict: 'EA',
        transclude: true,
        scope: 'isolate',
        template: '<h3>heading 3</h3><p>preface... blah blah</p><div ng-transclude></div>',
    };
});

And then the content of the directive’s element will be shoved into the div that has ng-transclude. Its just a move function. In my humble opinion it is pretty useless. I write way cooler apps :P

scope: true

Guess what? All the directives within the same app share the same scope. This means if one of them changes scope.obj the “obj” property will change in ALL the widgets. We will always see changes of the latter widget that applies them. To prevent this rather odd behavior we add scope: true to our directive definition and thus avoid scope clashes. Now each directive is separate from the other. I think AngularJS did this to allow directives to collaborate. But in most of my cases I follow the each man for himself philosophy. Just like good old America.

scope: {…} or 3 types of scope parameters: @, &, =

You don’t have to do scope: true, you can also do scope: {}; just as long as scope is defined (i.e. if (scope) is true). Thus, scope: true is just a shorthand to force the directive’s scope to be isolate. EACH MAN FOR HIMSELF! The scope parameter is actually what enables the directive to communicate with the outside world. Here is what I mean:

<div ng-controller="MyController">
    <div>
        somevar: <span ng-bind="somevar"></span>
        <br/>
        somevar2: <span ng-bind="somevar2"></span>
        <hr/>
    </div>
 <my-control
 param-str="control 1"
 param-callback="callback()"
 param-var="somevar"></my-control>
 <my-control
 param-str="{{ titleControl2 }}"
 param-callback="callback2()"
 param-var="somevar2"></my-control>
</div>

And the corresponding JS:

angular.module('ng').directive('myControl', function () {
    return {
        restrict: 'E',
        transclude: true,
        scope: {
            paramStr: '@', // pass as a string
            paramCallback: '&', // pass as a function and call with brackets ()
            paramVar: '=' // double binding!
        },
        template: '<div class="str">{{paramStr}}</div>'
            + '<div class="callback"><button class="btn" ng-click="paramCallback()">clickame</button></div>'
            + '<div class="var"><input type="text" class="form-control" ng-model="paramVar" /></div><hr/>'
        //link function ($scope, iterStartElement, attr) {}
    };
});

function MyController($scope) {
    $scope.somevar = "somevar...rrrr";
    $scope.somevar2 = "somevar...2!";
    $scope.titleControl2 = "Control 2 you beeches!";
    $scope.callback = function () {
        alert('callback called');
    }
    $scope.callback2 = function () {
        alert('callback2 called');
    }
}

Will produce the following HTML:

<div ng-controller="MyController" class="ng-scope">
    <div>
        somevar: <span ng-bind="somevar" class="ng-binding">somevar...rrrrdsdsa</span>
        <br>
        somevar2: <span ng-bind="somevar2" class="ng-binding">somevar...2!</span>
        <hr>
    </div>
    <my-control param-var="somevar" param-callback="callback()" param-str="control 1" class="ng-isolate-scope ng-scope">
        <div class="str ng-binding">control 1</div>
        <div class="callback"><button ng-click="paramCallback()" class="btn">clickame</button></div>
        <div class="var"><input type="text" ng-model="paramVar" class="form-control ng-pristine ng-valid"></div>
        <hr>
    </my-control>
    <my-control param-var="somevar2" param-callback="callback2()" param-str="Control 2 you beeches!" class="ng-isolate-scope ng-scope">
        <div class="str ng-binding">Control 2 you beeches!</div>
        <div class="callback"><button ng-click="paramCallback()" class="btn">clickame</button></div>
        <div class="var"><input type="text" ng-model="paramVar" class="form-control ng-pristine ng-valid"></div>
        <hr>
     </my-control>
</div>

So what we have in “scope” is a bunch of fields that you want to expose to the HTML. There are 3 types:

  • name: ‘@’ – this means in your directive you can use scope.name and access the value as a string.
  • name: ‘&’ – this is a callback, which means the parent scope will pass in the function as parameter in HTML
  • name: ‘=’ – double binding! The parent scope will pass in a member to it. Any changes the directive does to this object, including assignment are reflected in the parent scope. Any changes done by the parent scope are reflected in the directive. Read about it’s perils here.

Thus, each <my-control> element here has his own scope. And whenever changes are made to paramVar (directive scope) they propagate to somevar and somevar2 respectively in the MyController scope that houses these directives. The converse to also true. Any changes done to somevar, for instance will reflect in <my-control>’s paramVar. And of course they do! After all “=” means, they share the same object. But be careful with nulls!

Replace: true – getting rid of <my-control>

Let’s agree <my-control> is not a usual HTML element. We can get rid of it and just have the contents of the template replace it by specifying replace: true in the directive. Like so:

angular.module('ng').directive('replaceTest', function () {
    return {
        restrict: 'E',
        replace: true,
        template: '<div class="parent">'
            + '<div>AAAAA</div>'
            + '<div>BBBBB</div>'
            + '</div>'
    };
});

But beware! If your template has the more than one root element you will see the following error: Error: Template must have exactly one root element. The following directive will produce this error:

angular.module('ng').directive('replaceTest', function () {
    return {
        restrict: 'E',
        replace: true,
        template: '<div>AAAAA</div>' // one root element is OK
            + '<div>BBBBB</div>' // 2nd root element... this is too much!
    };
});

Precaution: IE8 compatibility

If you have constrained your directive to attribute or a class you should be fine. However if you want to create custom elements in your HTML, such as <mikhail-is-the-greatest /> or if you plan on using AngularJS’s custom elements such as <ng-switch>, then you need to call document.createElement for each funky element out there.

<!--[if lte IE 8]>
    <script>
        document.createElement('mikhail-is-the-greatest');
        document.createElement('ng-switch');
        document.createElement('ngrepeater');
    </script>
<![endif]-->

For IE7 I have only figured out this much: it is not compatible! Hence I always precede my directives with

<!--[if lte IE 7]>
<p>Sorry, it seems your browser is too old for this cool content. Please use a newer browser, such as <a href="http://www.microsoft.com/canada/windows/internet-explorer/oie9/default.aspx">IE9</a>, <a href="https://www.google.com/intl/en/chrome/browser/">Chrome</a> or <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a>!</p>
<![endif]-->

Bonus: $http in my compile or linking function

If you want to access the $http module from within the directive you can achieve this quite easily by adding $http parameter to the directive defining function itself:

angular.module('ng').directive('directiveWithHttp', function ($http) {
    return {
        restrict: 'A',
        ...
    };
});

Bonus: ng-repeat without an element

Consider that you want to have an ng-repeat to split out multiple elements for each iterations. For example

<h3>Title 1<h3>
<p>...</p>
<h3>Title 2<h3>
<p>...</p>
<h3>Title 3<h3>
<p>...</p>
ETC!!!

KnockoutJS allows you to have an iterator in comments. AngularJS does not. Solution? create your own element <ngrepeater> with the ng-repeat. Like so:  <ngrepeater ng-repeat=”row in rows”>…</ngrepeater>. Don’t forget to add IE8 compatibility code discussed above. Also note that with does not work for tables when you want to spit out groups of <tr>. For tables use tbody, like so

<pre>
<tbody ng-repeat="row in rows">
    <tr>..</tr>
    <tr>..</tr>
</tbody>
</pre>

Tables can handle multiple tbodies… Just not some other weird elements… Cry babies…

Download the code

Links

About Mikhail Temkine

immeasurable thirst for life... homepage, I guess

13 Responses to AngularJS directives

  1. Some One March 29, 2013 at 2:43 pm #

    Great article

  2. AlienSKP May 9, 2013 at 4:13 pm #

    Are you guys using Yeoman ?

    • amadou May 21, 2013 at 9:31 pm #

      never heard of! what is it ?

  3. ondrasak June 12, 2013 at 3:06 pm #

    Thanks! Great article

  4. tonatiuhn June 24, 2013 at 11:15 am #

    Thanks a lot for the tip about
    “`javascript
    scope: “isolate”
    “`
    in the directives, I’m wondering me if
    “`javascript
    scope: true
    “`
    is the same?

    • Mikhail Temkine February 15, 2014 at 2:30 pm #

      Sorry the reply took almost a year, but I just stumbled on this exact problem at work. I have updated the article to explain the “scope” parameter. You can see the explanation.
      Basically scope: ‘isolate’ is obsolete, I think. You need to do scope: true. In either case both of these are not true examples of what the “scope” parameter is used for. We say scope: { someVar: “=” } so that in the directive we can double-bind a parent scope variable like so: . This means any updates done to myScopeVar will be reflected in scope.someVar in the directive and vice versa. That’s what “=” means. There is also “&” for passing in function handlers and “@” for passing in strings.

      Put short, scope: {…} will also create an isolate scope. Therefore scope: true is nothing more than a shorthand for forcing the scope to be isolate. Otherwise it is shared

  5. ranadheer July 29, 2013 at 6:07 am #

    Hey, Thank you. Nice post..

    For AngularJS Directive Restrictions Read: http://coding-issues.blogspot.com/2013/07/directive-restrictions-in-angularjs.html

  6. Marc June 14, 2014 at 1:22 pm #

    Hey nice article, I look forward to read more from you.
    If anyone needs a German article: http://www.moretechnology.de/angularjs-das-javascript-framework/

    • Mikhail Temkine June 17, 2014 at 10:02 am #

      Danke sehr fur deine freundlichen worte. Ich auch will im etwas zu AngularJS in deutsch schreiben!

Trackbacks/Pingbacks

  1. AngularJS directives | Mikhail Temkine - February 7, 2013

    [...] the full article here. This entry was posted in Coding Insight by miktemk. Bookmark the [...]

  2. AngularJS directives - April 23, 2013

    [...] Read the full article here. [...]

  3. The must-know Javascript libraries - Coding Insight - August 30, 2013

    [...] to knockout, maybe even more powerful. It adds the ability to create your own HTML elements. It also adds some nice AJAX wrappers. In knockout you have to call $.ajax yourself. Here are some [...]

  4. AngularJS Learning Directives Best Resources | AngularJS 4U - April 18, 2014

    […] Codinginsight.com – AngularJS directives […]

Leave a Reply