AngularJS Directives – Basics

I have recently been working with AngularJS directives and this is probably my favorite feature of the project. It is a really clever and powerful way to extend HTML to do new things. As someone who has spent a lot of time doing Flex development, custom declarative markup that represents an underlying component is like an old friend.

In this post, we will explore a series of examples that build in complexity to help us understand directives. We are going to start out with an extremely simple example and iterate over it to see how and why you build directives.

Act One: The Setup

Here is the starting setup that we are going to be building on. It is static html with four divs positioned on the screen.

The jsFiddle is here:
http://jsfiddle.net/simpulton/x5ZLv/

Here is the HTML:

1
2
3
4
5
6
<div class="boundingBox">
   <div class="circle" style="background-color:#900; left:50px; top:50px;"></div>
   <div class="circle" style="background-color:#060; left:110px; top:70px;"></div>
   <div class="circle" style="background-color:#006; left:170px; top:90px;"></div>
   <div class="circle" style="background-color:#963; left:230px; top:110px;"></div>
</div>

And the CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.boundingBox {
    width: 600px;
    height: 600px;
    background-color: #333333;
    margin:20px;
}

.circle {
    display:block;
    position:absolute;
    height: 20px;
    width: 20px;
    background-color: #999;
    -moz-border-radius: 15px;
    -webkit-border-radius: 15px;
    border-radius: 15px;
}

.box {
    display:block;
    position:absolute;
    height: 20px;
    width: 20px;

}

#controls {
    position: absolute;
    top: 620px;
}

Nothing fancy here. Just four divs in a box.

Act Two: Enter AngularJS

Let’s take this a step further and make the properties dynamic. We are going to introduce AngularJS in this example to showcase some awesome dynamic functionality.

The jsFiddle is here:
http://jsfiddle.net/simpulton/8KyPF/

The HTML:

1
2
3
4
5
6
7
8
<div ng-app="animateApp" ng-controller="AnimateCtrl">
    <div class="boundingBox">
       <div class="circle"
           ng-repeat="shape in shapes"
           ng-style="{ 'backgroundColor':shape.color, 'left':shape.x+'px', 'top':shape.y+'px' }"
           />
    </div>
</div>

A few things worth noting in the HTML.

We are auto-bootstrapping the AngularJS application with the ng-app directive and assigning it a module.

We are going to assign a controller to the animateApp scope with the ng-controller directive. We will dig into the actual controller in just a moment.

We are using ng-repeat to loop over a data structure and instantiate the enclosing code once per item in the collection.

We are also using ng-style to set styles on the html element conditionally.

Now for the JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var module = angular.module('animateApp', []);

function AnimateCtrl($scope, $defer) {

    function buildShape () {
        return {
            color   : '#' + (Math.random() * 0xFFFFFF << 0).toString(16),
            x       : Math.min(380,Math.max(20,(Math.random() * 380))),
            y       : Math.min(180,Math.max(20,(Math.random() * 180)))
        };
    };

    // Publish list of shapes on the $scope/presentationModel
    $scope.shapes   = [];

    // Create shapes
    for (i = 0; i < 100; i++) {
        $scope.shapes.push( buildShape() );
    }
}

Here we instantiate the module and give it a name of animateApp. Next, we create our controller AnimateCtrl, and this is where all the magic happens.

We are creating a public variable called shapes on the $scope object for use in the ng-repeat directive. This is a simple array with JSON objects that we will use to set properties on the template when it renders.

We then create a loop which calls buildShape one hundred times and randomly sets properties on the object. It is worth noting that buildShape is effectively a private method since it is not attached to the $scope object.

Now we have 100 balls on the stage with random positions and colors. We have also covered a few really slick AngularJS directives in the process. My favorites are ng-repeat and ng-style for doing a lot of DOM manipulation with very little code.

Act Three: Animate!

Let’s add a bit of animation to this example before we get to our directive. We are not going to make any changes to our HTML but just add some functionality to our controller.

The jsFiddle is here:
http://jsfiddle.net/simpulton/89TvM/

Here is the JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
var module = angular.module('animateApp', []);

function animator(shapes, $defer) {
    (function tick() {
        var i;
        var now = new Date().getTime();
        var maxX      = 600;
        var maxY      = 600;

        for (i = 0; i < shapes.length; i++) {
           var shape = shapes[i];
           var elapsed = (shape.timestamp || now) - now;

           shape.timestamp = now;
           shape.x += elapsed * shape.velX / 1000;
           shape.y += elapsed * shape.velY / 1000;

           if (shape.x > maxX) {
               shape.x = 2 * maxX - shape.x;
               shape.velX *= -1;
           }
           if (shape.x < 30) {
               shape.x = 30;
               shape.velX *= -1;
           }

           if (shape.y > maxY) {
               shape.y = 2 * maxY - shape.y;
               shape.velY *= -1;
           }
           if (shape.y < 20) {
               shape.y = 20;
               shape.velY *= -1;
           }
         }

         $defer(tick, 30);
    })();
}

function AnimateCtrl($scope, $defer) {

    function buildShape () {
        var maxVelocity = 200;
        return {
            color   : '#' + (Math.random() * 0xFFFFFF << 0).toString(16),
            x       : Math.min(380,Math.max(20,(Math.random() * 380))),
            y       : Math.min(180,Math.max(20,(Math.random() * 180))),

            velX    : (Math.random() * maxVelocity),
            velY    : (Math.random() * maxVelocity)
        };
    };

    // Publish list of shapes on the $scope/presentationModel
    // Then populate the list with 100 shapes randomized in position
    // and color
    $scope.shapes   = [];
    for (i = 0; i < 100; i++) {
        $scope.shapes.push( buildShape() );
    }

    // Start timer-based, changes of the shape properties
    animator( $scope.shapes, $defer );
}

The most obvious addition to this code is the animator function. I am not going to dig into the nuts and bolts of how this works since it is not specific to AngularJS per se. The part I want to focus on in this example is the use of the $defer object.

The $defer object is basically a setTimeout wrapper with some extra functionality such as exception handling and an asynchronous id. The id allows you to call $defer.cancel to allow for clean up when it is no longer needed.

So how is it actually animating? When properties change, databinding fires and the DOM element with ng-styles updates for the associated shape. In fact, ng-styles uses databinding to update the CSS styles whenever the shape properties ‘x’, ‘y’, or ‘color’ changes. So the DOM elements look-n-feel are data-driven by the data model shape. Pretty simple!

Act Four: Attribute Directive

So we already have something that is pretty cool and well encapsulated. But can we make it even simpler?

Yes, and this is where directives come in. Directives are a way to teach HTML new tricks. For instance, a brand new element tag or attribute. Directives are not limited to new tags and attributes, but can be used to create class names and even comments.

We are going to create an attribute directive and that is going to hide the complexities of the DOM manipulation and handle it for us.

The jsFiddle is here:
http://jsfiddle.net/simpulton/5gWjY/

The HTML is here:

1
2
3
4
5
6
7
8
<div ng-app="animateApp" ng-controller="AnimateCtrl">
    <div class="boundingBox">
       <div class="circle"
           ng-repeat="shape in shapes"
           animate="shape"
           />
    </div>
</div>

Notice the animate attribute and its value shape. We are invoking the animate attribute and injecting the shape object.

And now for the directive:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var module = angular
        .module('animateApp', [])
        .directive('animate', function($defer) {
            return {
                restrict: 'A',
                link: function(scope, element, attrs)
                {
                    scope.$watch( 'shape', function(val) {
                        var changes = {
                            left : val.x + 'px',
                            top  : val.y + 'px',
                            backgroundColor : val.color
                        }

                        element.css( changes );
                    }, true );
                }
            };
        });

function animator(shapes, $defer) {
    // Edited for brevity
}

function AnimateCtrl($scope, $defer) {

    function buildShape () {
    // Edited for brevity
    };

    // Publish list of shapes on the $scope/presentationModel
    // Then populate the list with 100 shapes randomized in position
    // and color
    $scope.shapes   = [];
    for (i = 0; i < 100; i++) {
        $scope.shapes.push( buildShape() );
    }

    // Start timer-based, changes of the shape properties
    animator( $scope.shapes, $defer );
}

So let’s break down what is happening in our directive code.

A directive in its simplest form looks like this:

1
2
3
4
5
module.directive('myAwesomeDirective', function() {
    return {
        // directive definition object goes here
    };
});

You call the directive function on module and give it a name and start to define its behavior in the factory function and the directive declaration object (“DDO”) it returns.

Gotcha! Directives have camel cased names but are invoked by translating the camel case name into snake case. For example, a directive called “myAwesomeDirective” will be referenced “my-awesome-directive”.

There are quite a few properties that you can set on the DDO and I am going to cover two of them in this tutorial.

restrict – String of subset of EACM which restricts the directive to a specific directive declaration style. If omitted directives are allowed on attributes only.

and

link – Link function is responsible for registering DOM listeners as well as updating the DOM. It is executed after the template has been cloned. This is where most of the directive logic will be put.

I only wanted my directive to be used as an HTML attribute so I set restrict to A.

1
2
3
4
5
module.directive('animate', function() {
    return {
        restrict: 'A'
    };
});

And now for the link function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.directive('animate', function () {
    return {
        restrict:'A',
        link:function (scope, element, attrs) {
            scope.$watch('shape', function (val) {
                var changes = {
                    left:val.x + 'px',
                    top:val.y + 'px',
                    backgroundColor:val.color
                }

                element.css(changes);
            }, true);
        }
    };
});

The most important thing here is the $watch method on the scope object. The $watch object allows us to register a callback when shape changes and execute the closure we send in. In the callback, we are simply building a new object and applying it to the jQuery css property on element in a single call.

We are setting the last parameter to true since it is an object equality parameter which compares equality and not reference.

You can also chain the module and directive function together like I did in this example. This is considered best practice and it allows us to manage functionality via chaining. It is also convenient!

Now look at how simple the HTML element is?! You can add the animate attribute to anything and the animate functionality will be attached.

Act Five: Widget Directive

We are going to make one small variation on our previous example and make this an element directive. This makes it more widget-like which I really dig.

The jsFiddle is here:
http://jsfiddle.net/simpulton/EMA5X/

1
2
3
4
5
6
7
8
<div ng-app="animateApp" ng-controller="AnimateCtrl">
    <div class="boundingBox">
       <ball ng-repeat="shape in shapes"
           x="shape.x"
           y="shape.y"
           color="shape.color" />
    </div>
</div>?

Notice that the element is now a ball element which then ties to our ball directive. We are also setting each attribute individually.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var module = angular
        .module('animateApp', [])
        .directive('ball', function ($defer) {
            return {
                restrict:'E',
                link:function (scope, element, attrs) {
                    element.addClass('circle');

                    scope.$watch(attrs.x, function (x) {
                        element.css('left', x + 'px');
                    });
                    scope.$watch(attrs.y, function (y) {
                        element.css('top', y + 'px');
                    });
                    scope.$watch(attrs.color, function (color) {
                        element.css('backgroundColor', color);
                    });
                }
            };
        });

The two big differences in this directive is that we are setting restrict on the directive to E for element.

This is also cool because the directive silently adds the circle class to the DOM element.

Notice that two levels of databinding are occuring here:

  1. The <ball x=”shape.x” /> tag establishes databinding to update its own x, y, and color whenever the shape x, y, and color change (respectively).
  2. The .directive(‘ball’,function ($defer) {…}); also uses $watch() to establish databindings on itself. When its own x, y, color properties change, the associated DOM element styles are updated.

Conclusion

Directives are very powerful and a really great way to separate functionality from presentation. I really like that separation in Flex and I love it in AngularJS.

Also, I have to say that the AngularJS community is INCREDIBLE! I recieved tons of valuable feedback from Misko Hevery and Igor Minar. I also want to give super huge props to my new BFF, Thomas Burleson. His feedback was far beyond the call of duty, and my grasp of AngularJS has been greatly advanced under his guidance.

Resources

John Lindquist has a great screencast on directives. Check out his other screencasts while you are at it!
http://johnlindquist.com/2012/04/16/angularjs_directive_tutorial.html

Directive documentation
http://docs-next.angularjs.org/api/angular.module.ng.$compileProvider.directive

AngularJS Mailing List
https://groups.google.com/forum/?fromgroups#!forum/angular

AngularJS Directives – Basics

41 Responses

  1. Great resource and explanation of directives!

    Keep up the good work!

    Nikolaos Dimopoulos April 17, 2012 at 11:58 pm #
  2. Nice work here Lukas.
    Thumbs up on the explorations of AngularJS directives.

    Thomas Burleson April 18, 2012 at 1:45 pm #
  3. this line in the first version of the directive appears unused

    var target = attrs.animate;

    I’m curious about the performance of the last directive with 3 bindings and 3 watches vs the first with 1 watch and maybe no bindings (perhaps implied in repeat, not sure)

    bkc April 18, 2012 at 7:42 pm #
  4. @bkc Thanks for the catch! After a hundred revisions of an idea, things sometimes slip through the cracks.

    I have not noticed a significant performance hit in the last directive that is definitely a great candidate for a benchmark test. Obviously, my example is slightly contrived to illustrate the power of directives so I would be interested to see what happens on something a little more grounded in the real world. That sounds like a great idea for an upcoming blog post.

    simpulton April 18, 2012 at 7:55 pm #
  5. Very nice post!

    A bunch of corrections and clarifications:

    1/ div is not a void element, so is invalid html code.

    2/ ” by translating the camel case name into snake case” – the reason why you need to do this is that we actually support snake_case, dash-case, colon:case, data-my-directive and x-my-directive.

    3/ module.directive(‘animate’, function () { return { restrict:’A’, link: linkFn}}); is a equivalent to module.directive(‘animate’, function () { return linkFn; }); since ‘A’ and “link” are the defaults.

    4/ try using requestAnimationFrame instead of $defer to improve performance: http://jsfiddle.net/IgorMinar/eFHU3/123/

    5/ again ball is not a void element (check out the html spec, there is only a bunch of elements called void that can be self-closing) – so instead of you have to write

    But again, awesome job! Would you like a t-shirt? http://goo.gl/D9uOx

    Igor Minar April 20, 2012 at 5:39 am #
  6. ahh.. your blog engine is stripping html tags. will this work:

    <ball/> -> <ball></ball>

    Igor Minar April 20, 2012 at 5:41 am #
  7. oh yeah, it worked! so that’s what you have to do to fix all incorrectly self-closed non-void elements.

    Igor Minar April 20, 2012 at 5:42 am #
  8. Very cool! I’ve started looking into directives. Element directives are working, but I can’t seem to get attribute directives to inject values to the scope.

    http://jsfiddle.net/ItsLeeOwen/xexph/

    It looks like I’m following the docs correctly. http://docs-next.angularjs.org/api/angular.module.ng.$compileProvider.directive

    ItsLeeOwen April 20, 2012 at 5:52 am #
  9. Awesome article, very comprehensive.

    Radu April 20, 2012 at 6:42 am #
  10. Excellent explanation. Thanks!

    Aleksandar April 22, 2012 at 11:50 pm #
  11. I changed your app to use requestAnimationFrame, the animation is now smoother: http://jsfiddle.net/IgorMinar/TSAag/

    Igor Minar May 8, 2012 at 7:41 am #
  12. Now THAT is silky smooth! Totally awesome… added to my study queue! Thanks Igor!

    simpulton May 8, 2012 at 7:57 am #
  13. The code is simple and the graphics is nice and smooth. Good for meditating about entropy and thermodynamics =)

    YKY May 14, 2012 at 8:35 am #
  14. Just FYI $defer was renamed to $timeout about a month ago: https://github.com/IgorMinar/angular.js/commit/4511d39cc748288df70bdc258f98a8f36652e683

    Chris July 5, 2012 at 8:45 pm #
  15. Thanks for pointing that out, Chris! One of the joys of working with a release candidate.

    I will update as soon as I get a chance.

    simpulton July 5, 2012 at 8:50 pm #
  16. Thanks for this tutorial, its really great…

    Milan Zivkovic August 28, 2012 at 7:35 pm #
  17. Great post, thanks. I am surprised that ‘shape’ can be used like this: scope.$watch( ‘shape’, …) since ‘shape’ is not a $scope property. I would not have thought to try that. BTW, your directives don’t use $defer ($timeout) so they don’t need to inject $defer.

    Mark Rajcok September 4, 2012 at 4:49 pm #
  18. I forgot that ng-repeat creates its own scopes (“sub-scopes” of the scope associated with the controller). So each shape becomes a property of each of the scopes created by ng-repeat. And it is one of those scopes that the directive is working with.

    Mark Rajcok September 4, 2012 at 5:30 pm #
  19. this is the best, maybe the only, tutorial on directives. I hope that it replaces the directives tutorial on the guide http://docs.angularjs.org/guide/directive which looks more like an api reference than a tutorial.
    what I’m really missing, is a tutorial that talks about directives that use the compile method. that would be great.
    thank you author.

    Aladdin Mhaimeed October 28, 2012 at 1:51 pm #
  20. Thanks for your sharing! It helps me a lot

    Hung Nguyen October 30, 2012 at 3:09 am #
  21. > Misko did a crazy variation off this that was really awesome!
    > http://jsfiddle.net/mhevery/eFHU3/96/

    Dead link – gives 404 for me.

    Paul November 28, 2012 at 3:00 am #
  22. Hmmm I think it was deleted. Let me see if I can track it down and repost the link. Thanks for heads up!

    simpulton November 28, 2012 at 4:15 pm #
  23. This post got me to understand directives. Thanks so much. One point… I notice your html in Act 4 has animate=”shape” and then the directive watches on “shape”. But the directive could simply watch on attrs.animate, which will return the string “shape”. I noticed this because I felt like one of the “shape” insertions were redundant. I could simply say animate/> and the code would work or animate=”foobar”/>, it didn’t matter.

    Nick Cody December 5, 2012 at 9:03 pm #
  24. Thanks for the tutorial. One question – could you please direct me toward some info about restrict – String of subset of EACM which restricts the directive to a specific directive declaration style. If omitted directives are allowed on attributes only. which use ‘E’ and ‘A’ and are totally mysterious to me :)
    Thanks!

    Regis Zaleman December 18, 2012 at 6:48 pm #
  25. Great article. It helped me a lot to understand the concept of directives in AngularJS. Thank you.

    Sanggyu Nam January 1, 2013 at 8:13 pm #
  26. Hi Regis — sorry I forgot to reply to this. You can find the information here: http://docs.angularjs.org/guide/directive. Kind of towards the bottom it talks about the restrict property.

    simpulton January 1, 2013 at 8:25 pm #
  27. Hi simpulton

    I like the way you have explained and thanks for the details.

    However, I would like to still ask this question, how much ever stupid or irrelevant it be.

    With JQUERY will it not be just the html and controller, with the controller just directly affecting the DOM elements with the CSS in timer loop?

    So, for this example, what is the big advantage in using angularJS ? Or you just wanted to show angularJS usage taking this example, but you really mean that in a large project with lots of data handling and view updation, the MVVM pattern used by angularJS will be better off than a JQUERY modifying the DOM elements?

    -thalapathy

    Thalapathy Krishnamurthy January 23, 2013 at 5:40 am #
  28. Fair question Thalapathy —
    By isolating DOM manipulation to a single place (link function) it frees up the controller to only care about state and logic. This makes controllers VERY easy to test. This also creates essentially a ViewModel for your View to bind to which when used properly cuts down on the need for jQuery DOM manipulation drastically. So this approach is to facilitate better application architecture and testability.

    Make sense?

    simpulton January 23, 2013 at 6:08 am #
  29. Hi Simpulton

    Thanks. That confirms my understanding and puts me in a little bit comfort zone that there is no other tricky stuff that I missed. By the way, I am new to angularJS and have been reading on it, landed on your blog etc. Great set of questions you seem to solve in your blog, esp. when documentation around angularJS is a bit confusing. Keep it up!

    -thalapathy

    Thalapathy Krishnamurthy January 23, 2013 at 10:01 am #
  30. Great tutorial, directives are awesome but hard to understand at first , with this kind of tutorials is more fun.

    Daniel Vergara January 25, 2013 at 4:01 pm #
  31. Hi, i am new to angular, am so much confused while using datatables in angularjs using directives, if you have any idea, can you please help me. I am using routes and partials, in that i need to use the datatables. Searching for the solution, but am not getting properly, by reading your post, i hope you can answer. Ur approach is too good, which is understandable. Hope for quick reply.
    Thanks,
    Shanthi

    shanthi February 20, 2013 at 12:55 pm #
  32. Hi Shanthi — what do you mean by ‘datatables’?

    simpulton February 20, 2013 at 4:06 pm #
  33. Awesome ! tutorial. Thanks.

    Tukuna March 7, 2013 at 12:20 pm #
  34. Great explanation of directives. They were a mystery to me for some time and you’re explanation is nice and clear. Thank you so much!

    Paul April 3, 2013 at 6:15 pm #
  35. Killer introduction to directives. Thank you!

    Tom May 16, 2013 at 9:31 pm #
  36. Thanks for the tutorial, it helps with understanding directives. I think you should change the attribute directive sample though. you use $scope.watch(‘shape’) when in the html you say animate=”shape”. I think the watch should be $scope.watch(attrs.animate) to watch whatever is passed in the attribute, otherwise you could just have a blank attribute and it would work, but if you change the ng-repeat to be “s in shapes” there would be no way to get it to work without changing the directive declaration to $scope.watch(‘s’).

    Jason June 6, 2013 at 7:50 am #
  37. Nice article, I can only suggest to anyone that is learning from this that they should first learn about implied globals and proper variable declaration in javascript.

    Variable scope only happens in functions, so move them out of for and if/else blocks and make sure you “var i” in your loops. It is best practice to put all var declarations as the first things in the function.

    http://stackoverflow.com/questions/4909578/what-are-some-of-the-problems-of-implied-global-variables

    Scott July 8, 2013 at 6:05 pm #
  38. I totally agree. AngularJS is only as good as the foundation that you are building on.

    I am a huge fan of JavaScript The Good Parts by Crockford and JavaScript Patterns by Stefanov as well as Clean Code by Martin. Book report due in the morning! ;)

    simpulton July 8, 2013 at 7:08 pm #
  39. Hey, Nice article. Useful. For Directive restrictions i angularJS read this : http://coding-issues.blogspot.com/2013/07/directive-restrictions-in-angularjs.html

    ranadheer July 29, 2013 at 11:08 am #
  40. Great article. this article have all the things what i want. and searching from last one month. thanks sir

    Bharat Bhushan October 7, 2013 at 6:00 am #
Trackbacks/Pingbacks
  1. JavaScript frameworks | Gomes Blog - July 7, 2013

    [...] Tutorial8 Tips for Angular.js BeginnersJSFiddle Examples · angular/angular.js Wiki · GitHubAngularJS Directives – Basics | One Hungry MindAngularJS Directive TutorialAngularJS tips and tricks – broadcast online and offline status « [...]

Leave a Reply