Want more updates, tutorials, and awesomeness in general? Sign up!

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 plunker is here:
http://plnkr.co/edit/y6cNxP?p=preview/

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 plunker is here:
http://plnkr.co/edit/ErNLMn?p=preview/

The HTML:

1
2
3
4
5
6
7
8
9
<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>
</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
angular.module('animateApp', [])

.controller('AnimateCtrl', function($scope) {

    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 plunker is here:
http://plnkr.co/edit/6fT6op?p=preview/

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
angular.module('animateApp', [])
.controller('AnimateCtrl', function($scope, $timeout) {

    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, $timeout );
});

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

         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;
           }
         }

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

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 $timeout object.

The $timeout object is basically a setTimeout wrapper with some extra functionality such as exception handling and an asynchronous id. The id allows you to call $timeout.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 plunker is here:
http://plnkr.co/edit/B48Q12?p=preview/

The HTML is here:

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

    </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
angular
.module('animateApp', [])
.directive('animate', function($timeout) {
    return {
        restrict: 'A',
        link: function(scope, element, attrs)
        {
            var target = attrs.animate;
            scope.$watch( 'shape', function(val) {
                var changes = {
                    left : val.x + 'px',
                    top  : val.y + 'px',
                    backgroundColor : val.color
                }

                element.css( changes );
            }, true );
        }
    };
})
.controller('AnimateCtrl', function($scope, $timeout) {

    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, $timeout);
});
function animator(shapes, $timeout) {
    // Edited for brevity
}

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 plunker is here:
http://plnkr.co/edit/X0c6tL?p=preview/

1
2
3
4
5
6
7
8
9
<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
angular
.module('animateApp', [])
.directive('ball', function ($timeout) {
    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 ($timeout) {…}); 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

41 comments… add one

Leave a Comment