The 80/20 Guide to Writing AngularJS Directives

AngularJS is blowing up right now, and with good reason. There’s nothing more satisfying than using AngularJS to turn 1,000 messy lines of Backbone.js and jQuery spaghetti code into a trivial 10 lines. To put it in a broader context, you can think of AngularJS’ place in the world this way: AngularJS is to jQuery as C++11 is to x86 Assembly. However, your quest to capture all the wonderful benefits of AngularJS may be hindered because the documentation is a bit difficult to wrap your mind around. In particular, many readers have told me that the documentation for directives is pretty intimidating, and a lot of experienced users still don’t quite grok how to use them properly.

First of all, let’s take a moment to recognize that you’re almost certainly not the only one confused.  Even I sometimes have trouble distinguishing between compile and link, or figuring out how the hell ngTransclude works- and I’ve been working with AngularJS since version 0.9.4 in late 2010. You’ll find that AngularJS directives roughly follow the Pareto Distribution – 80% of the directives that you want to build will only use 20% of the features and design patterns that are available. In other words, don’t worry too much about understanding every little detail of directives. Think about git- how many programmers truly understand all the internals of interactive rebasing as opposed to simply doing pull and push to github?

For most of you, the reason that you’re writing directives is probably pretty straightforward, such as to integrate with existing Bootstrap modules and jQuery extensions, or to DRY up your UI. In this post, I’ll lay out the basic idea behind AngularJS directives, demonstrate what they do with roughly corresponding jQuery code, and provide you with enough knowledge to develop some pretty sophisticated directives.

Your First Directive: Setting a Background Image

At the highest level, a directive allows you to wire your custom UI components in to AngularJS’s two-way data-binding and scoping features, allowing you to define easily reusable ways for your users to view and interact with your underlying data. By default, a directive is a function that is run on every element with a particular attribute. This function takes as parameters the associated element and the AngularJS scope that this element is in. Let’s start out with an extremely simple example: setting the minimum height and width of an image while preserving its aspect ratio. There are several ways to do this, but the easiest is to make set the image as the background of a div using the following CSS:

1
2
3
4
5
6
.fill-bg {
  background-image: url('MYURLHERE');
  background-size : 'cover';
  background-repeat : 'no-repeat';
  background-position : 'center center';
}

Now let’s say we wanted to automate this process, so that our designers don’t have to write a separate CSS class for each image. We’ll do this by adding an attribute called ‘cover-background-image’ to some divs so our Javascript knows which image to set as the background. We can do this with jQuery or AngularJS, but lets do jQuery first. The general idea looks like this (JSFiddle: http://jsfiddle.net/vkarpov15/BQvbg/1/)

1
2
3
4
5
6
7
8
$('div[cover-background-image]').each(function(i, el) {
  $(element).css({
    'background-image': 'url()',
    'background-size' : 'cover',
    'background-repeat' : 'no-repeat',
    'background-position' : 'center center'
  });
});

The general process will be the same with a directive – AngularJS calls your custom directive code for each element with a given attribute. Below is the equivalent directive in AngularJS, and on JSFiddle:

1
2
3
4
5
6
7
8
9
10
11
12
angular.
  module('myApp', []).
  directive('myBackgroundImage', function () {
    return function (scope, element, attrs) {
      element.css({
        'background-image': 'url(' + attrs.myBackgroundImage + ')',
        'background-size': 'cover',
        'background-repeat': 'no-repeat',
        'background-position': 'center center'
      });
    };
  });

Making The Directive Dynamic

At this point some of you might be thinking, “Hey, the only difference is that AngularJS requires more code!” This is true for a relatively simple example like the one above. However, the real advantage to the AngularJS approach comes when you introduce two-way data-binding and the power of AngularJS scopes. Lets say that we want to introduce some carousel behavior to this image (switching the image every 5 seconds and allowing the user to navigate between images). In jQuery, supporting multiple carousels on the same page turns out to be a huge pain, because we have no easy way for mapping which images and which buttons belong to which carousel. When you try it the hard way, things end up looking a little something like this JSFiddle:

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
$(document).ready(function () {
  $('div[cover-background-image]').each(function (i, el) {
    var ctr = -1;
    var images = eval($(el).attr('cover-background-image'));
    var name = $(el).attr('carousel-name');
    var nextImage = function() {
      ctr = (ctr + 1) % images.length;
      $(el).css({
        'background-image': 'url(' + images[ctr] + ')',
        'background-size': 'cover',
        'background-repeat': 'no-repeat',
        'background-position': 'center center'
      });
    };
    var previousImage = function() {
      ctr = (ctr - 1);
      if (ctr < 0) {
        ctr = images.length - 1;
      }
      $(el).css({
        'background-image': 'url(' + images[ctr] + ')',
        'background-size': 'cover',
        'background-repeat': 'no-repeat',
        'background-position': 'center center'
      });
    };
    $("div[carousel-next='" + name + "']").each(function (i, el) {
      $(el).click(function() {
        nextImage();
      });
    });
    $("div[carousel-prev='" + name + "']").each(function (i, el) {
      $(el).click(function() {
        previousImage();<
      });
    });
    nextImage();
    var nextImageTimeout = function() {
      nextImage();
      setTimeout(nextImageTimeout, 5 * 1000);
    };
    setTimeout(nextImageTimeout, 5 * 1000);
  });

As you can see, this is pretty quickly getting a bit more complex than we’d like. More importantly, we’ve needed to hard-code the my-background-image pseudo-directive with certain properties that will make our lives difficult later.

First of all, we require the input of an array of images to be cycled. That may be fine for this specific application, but what if we want to re-use this code for a single image with no cycling? Then we’ve got a timer that’s doing nothing. Furthermore, putting an array variable in ‘my-background-image’ requires that the variable be visible from the scope of the jQuery code. This leads to both headaches and lots of image arrays in the global scope. Finally, lets say that we want to add additional behaviors, such as swipe handling via Hammer.js. It becomes difficult to configure this behavior on a per-carousel basis – what if we have some carousels that should have swipe recognition and some that shouldn’t?

Now that we’ve seen the difficulties that come with trying to make customizable and reusable UI behaviors and components, lets see how AngularJS provides us with the right framework and tools to do this task easily. Here’s the first of four design patterns that you’ll see frequently with AngularJS directives. With each pattern, I’ll describe to you how the pattern works, and provide examples of this pattern in action both in my own JSFiddles and what others have written.

Basic Directive Design Pattern: Watch and Update

Example: Display a cropped image using CSS and update it whenever the underlying variable changes

Alternative Example: angular-ui/bootstrap’s progressbar directive

One design pattern you’ll see frequently with AngularJS directives is “Watch and Update:” watching a single value and updating the DOM when this value changes to reflect our internal state. Often this can be taken care of using combinations of the built-in ngClass and ngStyledirectives, but for more complex manipulations you may want to use your own directive to make sure your code is DRY. Our new myBackgroundImage directive will use a very simple “watch and update” pattern (view on JSFiddle):

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
angular.
module('myApp', []).
directive('myBackgroundImage', function () {
  return function (scope, element, attrs) {
    scope.$watch(attrs.myBackgroundImage, function(v) {
      element.css({
        'background-image': 'url(' + v + ')',
        'background-size': 'cover',
        'background-repeat': 'no-repeat',
        'background-position': 'center center'
      });
    });
  };
});
function CarouselController($scope, $timeout) {
  $scope.images = [];
  $scope.image = ""
  $scope.index = 0;
  $scope.setImages = function(images) {
    $scope.images = images;
    $scope.image = images[0];
    $scope.index = 0;
  };
  $scope.nextImage = function() {
    $scope.index = ($scope.index + 1) % $scope.images.length;
    $scope.image = $scope.images[$scope.index];
  };
  $scope.prevImage = function() {
    $scope.index = ($scope.index - 1 >= 0 ? $scope.index - 1 : $scope.images.length - 1);
    $scope.image = $scope.images[$scope.index];
  };
  var nextImageTimeout = function() {
    $scope.nextImage();
    $timeout(nextImageTimeout, 5 * 1000);
  };
  $timeout(nextImageTimeout, 5 * 1000);
}

With two-way data-binding, our directive changes very minimally from the previous example. Instead of taking the my-background-image attribute as a vanilla string, we tell AngularJS to interpret it as a variable and watch its value with a scope.$watch call. When the value changes, we update our CSS. We then move the carousel functionality to an AngularJS controller, so that we can reuse our directive in other non-carousel contexts. As you can see in the JSFiddle, we added an “Add Image” button that pushes another image to the top carousel’s images collection, allowing the carousel to update automatically.

Most importantly, AngularJS handles scoping for us, meaning that we can reference functions within the AngularJS controller from our HTML. Imagine trying to do something similar with onclick and jQuery – we would have to get very creative with refactoring our code to take all of our user behaviors out of myBackgroundImage.

Now that we’ve gotten the carousel functionality, it’s simple enough to add an ngSwipedirective to our AngularJS module that will enable our users to navigate between images with swipes. Speaking of which:

Basic Directive Design Pattern: Wiring External Event Handlers to call $apply

Example: Writing a directive to wire Hammer.js swipeleft and swiperight with AngularJS

Alternative Example: Hooking up the Google Places Autocomplete with AngularJS 

Another common task in writing AngularJS directives, especially when integrating with non-AngularJS libraries, is hooking up event handlers to update your AngularJS scope. Without going into too much detail about AngularJS’ internals, each scope has:

1. An $apply function that notifies AngularJS when some event has happened that may require updating the view.

2. An $eval function, which does a safe eval on its parameter in the scope.

When you include external event handlers, such as the .on() calls that many jQuery plugins support, AngularJS does not know that they exist, so your directive needs to tell AngularJS how to handle them. In our case, lets hook up a directive that will listen to Hammer.js’s swipe left and swipe right events (view on JSFiddle – you don’t need a phone or tablet to trigger a swipe event, just drag your mouse quickly across one of the images) :

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
angular.
  module('myApp', []).
  directive('myBackgroundImage', function () {
    return function (scope, element, attrs) {
      scope.$watch(attrs.myBackgroundImage, function(v) {
        element.css({
          'background-image': 'url(' + v + ')',
          'background-size': 'cover',
          'background-repeat': 'no-repeat',
          'background-position': 'center center'
        });
      });
    };
  }).directive('swipeLeft', function() {
    return function(scope, element, attrs) {
      $(document).ready(function() {
        Hammer(element).on('swipeleft', function() {
          scope.$eval(attrs.swipeLeft);
          scope.$apply();
        });
      });
    };
  }).directive('swipeRight', function() {
    return function(scope, element, attrs) {
      $(document).ready(function() {
        Hammer(element).on('swiperight', function() {
          scope.$eval(attrs.swipeRight);
          scope.$apply();
        });
      });
    };
  });

The calls to $eval evaluate the swipeRight and swipeLeft attributes, which will typically be function calls. For example, in our little carousel JSFiddle, these calls will look something like this:

1
2
<div my-background-image='image' swipe-left='nextImage()' swipe-right='prevImage()'>
</div>

Notice that in the second image, we only allow the user to swipe left, whereas on the first one we allow swiping both left and right. Our swipeLeft and swipeRight directives are completely decoupled from the actual carousel implementation – we can use them anywhere to handle any sort of action we want to take when the user swipes on any element. 

In contrast, this is a far cry from the mostly equivalent jQuery implementation (view onJSFiddle), where we declare our Hammer.js handlers in jQuery (lines 46 and 47) and have no way of reusing them from HTML. We also have no easy way of allowing only “swipe left” without having to deal with some sort of configuration object. One potential alternative is to expose swipe left and swipe right as separate attributes (similar to our approach with the ‘cover-background-image’ function), but then we run into a brick wall with scoping – we don’t have an easy way to allow these event handlers to access the previousImage and nextImagefunctions within the ‘cover-background-image’ code.

 Basic Directive Design Pattern: Combining “watch and update” with $eval on an external event handler

Example: Wiring bootstrap-slider to work with AngularJS

Alternative Example: angular-ui/bootstrap’s btnRadio directive

Most directives will need data to go both ways. You will need to a) update your view to reflect internal state, and b) define some set of UI-accessible user behaviors that can update the internal state.

First, take a look at bootstrap-slider. This slider is a simple drag-and-drop interface for setting numerical values. We’re going to use it for controlling which image is displayed in our image carousel. The directive looks like this (view on JSFiddle):

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
angular.
  module('myApp', []).
  directive('bootstrapSlider', function() {
    return function(scope, element, attrs) {
      $(document).ready(function() {
        var init = scope.$eval(attrs.ngModel);
        var min = scope.$eval(attrs.bootstrapSliderMin);
        var max = scope.$eval(attrs.bootstrapSliderMax);
        $(element[0]).slider({
          value : init,
          min : min,
          max : max,
          tooltip : 'hide'
        });
        // Update view to reflect model
        scope.$watch(attrs.ngModel, function(v) {
          $(element[0]).slider('setValue', v);
        });
        // Update model to reflect view
        $(element[0]).slider().on('slide', function(ev) {
          scope.$apply(function() {
            scope[attrs.ngModel] = ev.value;
          });