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: '</pre>
<div>
<h1>hello...</h1>
{{obj}}</div>
<pre>',
        //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>Directive Test<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></pre>
<div></div>
<pre>

Compile function

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


    Parse me... come ooooon! Just parse meee!

… 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: '</pre>
<div>gonna parse this: {{orig}}
... and get this: {{obj}}</div>
<pre>',
        //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:

</pre>
<div>gonna parse this: Parse me... come ooooon! Just parse meee!
... 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>
<pre>

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: '</pre>
<h3>heading 3</h3>
<pre>
preface... blah blah</pre>
<div></div>
<pre>',
    };
});

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:

</pre>
<div>
<div>somevar:
 somevar2:

<hr />

</div>
</div>
<pre>

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: '</pre>
<div class="str">{{paramStr}}</div>
<pre>'
            + '</pre>
<div class="callback"><button class="btn">clickame</button></div>
<pre>'
            + '</pre>
<div class="var"><input class="form-control" type="text" /></div>

<hr />

<pre>'
        //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:

</pre>
<div class="ng-scope">
<div>somevar: <span class="ng-binding">somevar...rrrrdsdsa</span>
 somevar2: <span class="ng-binding">somevar...2!</span>

<hr />

</div>
<div class="str ng-binding">control 1</div>
<div class="callback"><button class="btn">clickame</button></div>
<div class="var"><input class="form-control ng-pristine ng-valid" type="text" /></div>

<hr />

<div class="str ng-binding">Control 2 you beeches!</div>
<div class="callback"><button class="btn">clickame</button></div>
<div class="var"><input class="form-control ng-pristine ng-valid" type="text" /></div>

<hr />

</div>
<pre>

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: '</pre>
<div class="parent">' + '
<div>AAAAA</div>
' + '
<div>BBBBB</div>
' + '</div>
<pre>'
    };
});

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: '</pre>
<div>AAAAA</div>
<pre>' // one root element is OK
            + '</pre>
<div>BBBBB</div>
<pre>' // 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]>

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>!
<![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

</pre>
<h3>Title 1</h3>
<h3>
...</h3>
<h3>Title 2</h3>
<h3>
...</h3>
<h3>Title 3</h3>
<h3>
...
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

....

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

Download the code

Links

Leave a Reply

Your email address will not be published.