Click here to Skip to main content
11,412,516 members (66,668 online)
Click here to Skip to main content

Extending HTML with AngularJS Directives

, 28 Aug 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
Use AngularJS directives to teach HTML new tricks.

AngularExplorer screenshot

Introduction to AngularJS

AngularJS is Google's framework for developing Web applications. Angular provides a number of essential services that work very well together and were designed to be extensible. These services include data-binding, DOM manipulation, routing/view management, module loading, and more.

AngularJS is not just another library. It provides a complete integrated framework, so it reduces the number of libraries you have to deal with. It comes from Google, the same people who built Chrome and are helping create the foundations for the next generation of web applications (for more on that check out the polymer project at www.polymer-project.org/). I believe that in five or ten years, we won't be using AngularJS to develop web apps anymore, but we will be using something similar to it.

To me, the most exciting feature of AngularJS is the ability to write custom directives. Custom directives allow you to extend HTML with new tags and attributes. Directives can be reused within and across projects, and are roughly equivalent to custom controls in platforms like .NET.

The sample included with this article includes nearly 50 custom directives created based on Bootstrap, Google JavaScript APIs, and Wijmo. The sample is fully commented and includes documentation, so it should serve as a good reference when you start writing your directives. You can see the sample live here: http://demo.componentone.com/wijmo/Angular/AngularExplorer/AngularExplorer

Creating directives tailored to your needs is fairly easy. These directives can be tested, maintained, and re-used in multiple projects. Properly implemented directives can be enhanced and re-deployed with little or no change to the applications that use them.

This document focuses on AngularJS directives, but before we get into that topic we will quickly go over some AngularJS basics to provide the context. 

To use AngularJS, you must include it as a reference in your HTML page, and add an ng-app attribute to the HTML or body tags on the page. Here is a very short sample to show how this works:

<html>
  <head>
    <script src="http://code.angularjs.org/angular-1.0.1.js"></script>
  </head>
  <body ng-app ng-init="msg = 'hello world'">
    <input ng-model="msg" />
    <p>{{msg}}</p>
  </body>
</html>

When AngularJS loads, it scans the document for the ng-app attribute. This tag is usually set to the name of the application's main module. Once the ng-app attribute is found, Angular will process the document, loading the main module and its dependencies, scanning the document for custom directives, and so on.

In this example, the ng-init attribute initializes an msg variable to "hello world" and the ng-model attribute binds the content of the variable to an input element. The text enclosed in curly braces is a binding expression. AngularJS evaluates the expression and updates the document whenever the value of the expression changes. You can see this in action here: jsfiddle.net/Wijmo/HvSQQ/

AngularJS Modules

Module objects serve as the root of AngularJS applications. They contain objects such as config, controller, factory, filter, directive, and a few others.

If you are familiar with .NET and new to Angular, the table below shows a rough analogy that helps explain the role played by each type of AngularJS object:

AngularJS .NET Comment
module Assembly Application building block
controller ViewModel Contains the application logic and exposes it to views
scope DataContext Provides data that can be bound to view elements
filter ValueConverter Modifies data before it reaches the view
directive Component Re-usable UI element
factory, service Utility classes Provide services to other module elements

For example, this code creates a module with a controller, a filter, and a directive:

var myApp = angular.module("myApp", []);
myApp.controller("myCtrl", function($scope) {
  $scope.msg = "hello world";
});

myApp.filter("myUpperFilter", function() {
  return function(input) {
    return input.toUpperCase();
  }
});

myApp.directive("myDctv", function() {
  return function(scope, element, attrs) {
    element.bind("mouseenter", function() {
      element.css("background", "yellow");
    });
    element.bind("mouseleave", function() {
      element.css("background", "none");
    });
  }
});

The module method takes as parameters the module name and a list of dependencies. In this example, we are creating a module that does not depend on any other modules, so the list is empty. Note that the array must be specified, however, even if it is empty. Omitting it would cause AngularJS to retrieve a named module specified previously. We will discuss this in more detail in the next section.

The controller constructor gets a $scope object that is responsible for holding all the properties and methods exposed by the controller. This scope will be managed by Angular and passed to views and directives. In this example, the controller adds a single msg property to the scope. An application module may have multiple controllers, each responsible for one or more views. Controllers do not have to be members of the module, but it is good practice to make them so.

The filter constructor returns a function that will be used to modify input for display. Angular provides several filters, but you can add your own and use them in exactly the same way. In this example we define a filter that converts strings to uppercase. Filters can be used not only to format values, but also to modify arrays. Formatting filters provided by AngularJS include number, date, currency, uppercase, and lowercase. Array filters include filter, orderBy, and limitTo. Filters may take parameters, and the syntax is always the same: someValue | filterName:filterParameter1:filterParameter2....

The directive constructor returns a function that takes an element and modifies it according to parameters defined in the scope. In this example we bind event handlers to the mouseenter and mouseleave events to highlight the element content when the mouse is over it. This is our very first directive, and barely scratches the surface of what directives can do. AngularJS directives can be used as attributes or elements (or even comments), and they can be nested and communicate with each other. We will cover a lot of that in later sections.

Here is a page that uses this module:

<body ng-app="myApp" ng-controller="myCtrl">
  <input ng-model="msg" />
  <p my-dctv >
    {{ msg | myUpperFilter }}
  </p>
</body>

You can see this in action here: jsfiddle.net/Wijmo/JKBbV/

Notice that the names of the app module, controller, and filter are used as attribute values. They represent JavaScript objects and therefore these names are case-sensitive.

The name of the directive, on the other hand, is used as an attribute name. It represents an HTML element, and therefore is case-insensitive. However, AngularJS converts camel-cased directive names to hyphen-separated strings. So the "myDctv" directive becomes "my-dctv" (just like the built-in directives ngApp, ngController, and ngModel become "ng-app", "ng-controller", and "ng-model".

Project Organization

AngularJS was designed to handle large projects. You can break up your projects into multiple modules, split modules into multiple files, and organize these files in whatever way makes sense to you. Most projects I have seen tend to follow the convention suggested by Brian Ford in his blog Building Huuuuuge Apps with AngularJS. The general idea is to break up modules into files and to group them by type. So controllers are placed in a controllers folder (and named XXXCtrl), directives go in a directives folder (and are named XXXDctv), etc.

A typical project folder might look like this:

Root
        default.html
        styles
               app.css
        partials
               home.html
               product.html
               store.html
        scripts
               app.js
               controllers
                       productCtrl.js
                       storeCtrl.js
               directives
                       gridDctv.js
                       chartDctv.js
               filters
                       formatFilter.js
               services
                       dataSvc.js
               vendor
                       angular.js
                       angular.min.js

Imagine for example that you want to use a single module, defined in the app.js file. You could define it this way:

// app.js
angular.module("appModule", []);

To add elements to the module, you would then ask for the module by name and add elements to it as we showed before. For example, the formatFilter.js file would contain something like this:

// formatFilter.js
// retrieve module by name
var app = angular.module("appModule");

// add a filter to the module
app.filter("formatFilter", function() {
  return function(input, format) {
    return Globalize.format(input, format);
  }
}})

If your app contains multiple modules, remember to specify the dependencies when you create each module. For example, an application that contained three modules named app, controls, and data could specify them as follows:

// app.js (the main application module, depends on "controls" and "data" modules)
angular.module("app", [ "controls", "data"])

// controls.js (the controls module, depends on "data" module)
angular.module("controls", [ "data" ])

// data.js (the data module, no dependencies)
angular.module("data", [])

The main page in your application would specify the name of the main module in the ng-app directive, and AngularJS would automatically bring in all the required dependencies:

<html ng-app="app">
...
</html>

The main page and all its views would then be able to use elements defined in the three modules.

For an example of a fairly large application organized in the manner described above, please see the AngularExplorer sample included with this article.

Now that we have covered the basics of AngularJS, it is time to deal with our main topic: directives. In the next few chapters, we will cover the basic concepts and will create quite a few directives to demonstrate their possibilities, which are quite amazing.

If you want to learn a bit more about AngularJS before continuing (or at any time really), I recommend Dan Wahling's excellent video " AngularJS Fundamentals in 60-ish Minutes". There are also some interesting videos put together by members of the AngularJS team on the " About those directives" page.

AngularJS Directives: Why?

I said earlier that to me directives are the most exciting feature of AngularJS. That's because they are the one feature that is really unique to AngularJS. Great as they are, the other features in AngularJS are also available in other frameworks. But the ability to create reusable libraries of components that can be added to applications in pure HTML is something incredibly powerful, and to my knowledge AngularJS is the only framework that provides that capability to web applications today.

There are several JavaScript products that provide controls to web developers. For example, Boostrap is a popular "front-end framework" that provides styles and some JavaScript components. The problem is that in order to use the components, the HTML author must switch into JavaScript mode and write jQuery code to activate the tabs. The jQuery code is simple enough, but it has to be synchronized with the HTML, and this is a tedious and error-prone process that does not scale well.

The AngularJS home page shows a simple directive that wraps the Bootstrap tab component and makes it really easy to use in pure HTML. The directive makes tabs as easy to use as ordered lists. Plus, the directive can be re-used in many projects by many HTML developers. The HTML is as simple as this:

<body ng-app="components">
  <h3>BootStrap Tab Component</h3>
  <tabs>
    <pane title="First Tab">
      <div>This is the content of the first tab.</div>
    </pane>
    <pane title="Second Tab">
      <div>This is the content of the second tab.</div>
    </pane>
  </tabs>
</body>

You can see this in action here: jsfiddle.net/Wijmo/ywUYQ/

As you can see, the page looks like regular HTML, except it has been extended with <tabs> and <pane> tags implemented as directives. The HTML developer doesn't have to write any JavaScript. Of course, someone has to write the directives, but those are generic. They can be written once and reused many times (just like BootStrap, jQueryUI, Wijmo, and all those other great libraries).

Because directives are so useful, and not all that hard to write, many people are already creating directives for popular libraries. For example, the AngularJS team has created a set of directives for Boostrap called UI Bootstrap; ComponentOne ships AngularJS directives with its Wijmo library; and there are several public repositories of directives for jQueryUI widgets.

But wait a minute! If there are so many sources of ready-made directives, why should you learn how to create them yourself? Good question. Maybe you don't. So look around before writing your own. But there are a couple of good reasons to learn:

  1. You may have special needs. Suppose for example you work for a financial company that uses a certain type of form across many applications. The form can be implemented as a data grid, with custom functionality to download data in a certain way, edit and validate the data in a certain way, and upload the changes back to the server in a certain way. It is unlikely that anyone outside your corporation will have something useful to you. But you could write a custom directive and make it available to all HTML developers on your team that would allow them to write:
  2. <body ng-app="abcFinance">
      <h3>Offshore Investment Summary</h3>
      <abc-investment-form
        customer="currentCustomer"
        country="currentCountry">
      </abc-investment-form data>
    </body>

    The "abcInvestmentForm" directive could be used in many applications, providing consistency. The directive would be centrally maintained and could be updated to reflect new business practices or requirements with little impact on the applications.

  3. Maybe the directive you want really doesn't exist yet. Perhaps you happen to like a library that nobody wrote directives for yet, and you don't want to wait. Or maybe you simply don't like the directives that you found, and you would like to tweak them.

OK, I guess if you are reading this article you are already sold on the idea of directives and eager to get started. So let's move on.

AngularJS Directives: How?

The directive we showed in the beginning of this article was very simple. It only specified a "link" function and nothing else. A typical directive contains more elements:

// create directive module (or retrieve existing module)
var m = angular.module("myApp");

// create the "my-dir" directive 
myApp.directive("myDir", function() {
  return {
    restrict: "E",        // directive is an Element (not Attribute)
    scope: {              // set up directive's isolated scope
      name: "@",          // name var passed by value (string, one-way)
      amount: "=",        // amount var passed by reference (two-way)
      save: "&"           // save action
    },
    template:             // replacement HTML (can use our scope vars here)
      "<div>" +
      "  {{name}}: <input ng-model='amount' />" +
      "  <button ng-click='save()'>Save</button>" +
      "</div>",
    replace: true,        // replace original markup with template
    transclude: false,    // do not copy original HTML content
    controller: [ "$scope", function ($scope) { …  }],
    link: function (scope, element, attrs, controller) {…}
  }
});   

Note how the directive name follows a pattern: the "my" prefix is analogous to a namespace, so if the application uses directives from multiple modules it will be easy to determine where they are defined. This is not a requirement, but it is a recommended practice that makes a lot of sense.

The directive constructor returns an object with several properties. These are all documented in the AngularJS site, but the explanations they provide are always as clear as they should be. So here is my attempt at explaining what these properties do:

  • restrict: Determines whether the directive will be used in HTML. The valid options are "A", "E", "C", and "M" for attribute, element, class, or comment. The default is "A", for attribute.  But we are more interested in element attributes, because that's how you create UI elements such as the "tab" directive shown earlier.
  • scope: Creates an isolated scope that belongs to the directive, isolating it from the scope of the caller. Scope variables are passed in as attributes in the directive tag. This isolation is essential when creating reusable components, which should not rely on the parent scope. The scope object defines the names and types of the scope variables. The example above defines three scope variables:
    • name: "@" (by value, one-way):
      The at sign "@" indicates this variable is passed by value. The directive receives a string that contains the value passed in from the parent scope. The directive may use it but it cannot change the value in the parent scope (it is isolated). This is the most common type of variable.
    • amount: "=" (by reference, two-way)
      The equals sign "=" indicates this variable is passed by reference. The directive receives a reference to a value in the main scope. The value can be of any type, including complex objects and arrays. The directive may change the value in the parent scope. This type of variable is used when the directive needs to change the value in the parent scope (an editor control for example), when the value is a complex type that cannot be serialized as a string, or when the value is a large array that would be expensive to serialize as a string.
    • save: "&" (expression)
      The ampersand "&" indicates this variable holds an expression that is executed in the context of the parent scope. It allows directives to perform actions other than simply changing a value.
  • template: String that replaces the element in the original markup. The replacement process migrates all attributes from the old element to the new one. Notice how the template may use variables defined in the isolated scope. This allows you to write macro-style directives that don't require any additional code. In most cases, however, the template is simply an empty <div> that will be populated using code in the link function discussed below.
  • replace: Determines whether the directive template should replace the element in the original markup or be appended to it. The default value is false, which causes the original markup to be preserved.
  • transclude: Determines whether the custom directive should copy the content in the original markup. For example, the "tab" directive shown earlier had transclude set to true because the tab element contains other HTML elements. A "dateInput" directive on the other hand would have no HTML content so you would set transclude to false (or just omit it altogether).
  • link: This function contains most of the directive logic. It is responsible for performing DOM manipulations, registering event listeners, etc. The link function takes the following parameters:
    • scope: Reference to the directive's isolated scope. The scope variables are initially undefined, and the link function registers watches to receive notifications when their values change.
    • element: Reference to the DOM element that contains the directive. The link function normally manipulates this element using jQuery (or Angular's jqLite if jQuery is not loaded).
    • controller: Used in scenarios with nested directives. This parameter provides child directives with a reference to the parent, allowing the directives to communicate. The tab directive discussed earlier is a good example: jsfiddle.net/Wijmo/ywUYQ/

Note that when the link function is called, the scope variables passed by value ("@") will not have been initialized yet. They will be initialized at a later point in the directive life cycle, and if you want to receive notifications you have to use the scope.$watch function, discussed in the next section.

If you are not familiar with directives yet, the best way to really understand all this is to play with some code and try out different things. This fiddle lets you do that: jsfiddle.net/Wijmo/LyJ2T/

The fiddle defines a controller with three members (customerName, credit, and save). It also defines a directive similar to the one listed above, with an isolated scope with three members (name, amount, and save). The HTML shows how you can use the controller in plain HTML and with the directive. Try changing the markup, the types of the isolated variables, the template, and so on. This should give you a good idea of how directives work.

Communication between Directive and Parent Scopes

OK, so directives should have their own isolated scope so they can be re-used in different projects and be bound to different parent scopes. But how exactly do these scopes communicate?

For example, assume you have a directive with an isolated scope declared as in the example above:

scope: {              // set up directive's isolated scope
  name: "@",          // name var passed by value (string, one-way)
  amount: "=",        // amount var passed by reference (two-way)
  save: "&"           // save command
},

And assume the directive is used in this context:

<my-dir
  name="{{customerName}}"
  amount="customerCredit"
  save="saveCustomer()"
/>

Notice how the "name" attribute is enclosed in curly brackets and "amount" is not. That is because "name" is passed by value. Without the brackets, the value would be set to the string "customerName". The brackets cause AngularJS to evaluate the expression before and set the attribute value to the result. In contrast, "amount" is a reference, so you don't need brackets.

The directive could retrieve the values of the scope variables simply by reading them off the scope object:

var name = scope.name;
var amount = scope.amount;

This would indeed return the current value of the variables, but if the values changed in the parent scope, the directive would not know about it. To be notified of these changes, it would have to add watchers to these expressions. This can be done with the scope.$watch method, which is defined as:

scope.$watch(watchExpression, listenerFunction, objectEquality);

The watchExpression is the thing you want to watch (in our example, "name" and "amount"). The listenerFunction is the function that gets called when the expressions change value. This function is responsible for updating the directive to reflect the new values.

The last argument, objectEquality, determines how AngularJS should compare the variable's old and new values. If you set objectEquality to true, then AngularJS will do a deep comparison between the old and new values rather than a simple reference comparison. This is very important when the scope variable is a reference ("=") rather than a value ("@"). For example if the variable is an array or a complex object, setting objectEquality to true will cause the listenerFunction to be called even if the variable is still referencing the same array of object, but the contents of the array or object have changed.

Going back to our example, you could watch for changes in the scope variables using this code:

scope.$watch("name", function(newValue, oldValue, srcScope) {
  // handle the new value of "name"
});
scope.$watch("amount", function(newValue, oldValue, srcScope) {
  // handle the new value of "amount"
});

Notice that the listenerFunction gets passed the new and old values, as well as the scope object itself. You will rarely need these arguments since the new value is already set on the scope, but in some cases you may want to inspect exactly what changed. And in some rare cases, the new and old values might actually be the same. This may happen while the directive is being initialized.

How about the other direction? In our example, the "amount" variable is a reference to a value, and the parent scope may be watching it for changes the same way we are.

In most cases you don't have to do anything at all. AngularJS automatically detects changes that happen as a result of user interactions and processes all the watchers for you. But this is not always the case. Changes that happen because of browser DOM events, setTimeout, XHR, or third party libraries are not detected by Angular. In these cases, you should call the scope.$apply method, which will broadcast the change to all registered listeners.

For example, imagine our directive has a method called updateAmount that performs some calculations and changes the value of the "amount" property. Here's how you would implement that:

function updateAmount() {
  // update the amount value
  scope.amount = scope.amount * 1.12;
  // inform listeners of the change
  if (!scope.$$phase) scope.$apply("amount");
}

The scope.$$phase variable is set by AngularJS while it is updating the scope variables. We test this variable to avoid calling $apply from within an update cycle.

Summarizing, scope.$watch handles inbound change notifications and scope.$apply handles outbound change notifications (but you rarely have to call it).

As usual, the best way to really understand something is to watch it in action. The fiddle at jsfiddle.net/Wijmo/aX7PY/ defines a controller and a directive. Both have methods that change data in an array, and both listen to changes applied by each other. Try commenting out the calls to scope.$watch and scope.$apply to see their effect.

Shared Code / Dependency Injection

When you start writing directives, you will probably create utility methods that are useful to many directives. Of course you don't want to duplicate that code, so it makes sense to group these utilities and expose them to all directives that need them.

You can accomplish this by adding a factory to the module that contains the directives, and then specifying the factory name in the directive constructors. For example:

// the module
var app = angular.module("app", []);

// utilities shared by all directives
app.factory("myUtil", function () {
  return {
    // watch for changes in scope variables
    // call update function when all have been initialized
    watchScope: function (scope, props, updateFn, updateOnTimer) {
      var cnt = props.length;
      angular.forEach(props, function (prop) {
        scope.$watch(prop, function (value) {
          if (--cnt <= 0) {
            if (updateOnTimer) {
              if (scope.updateTimeout) clearTimeout(scope.updateTimeout);
              scope.updateTimeout = setTimeout(updateFn, 50);
            } else {
              updateFn();
            }
          }
        })
      })
    },

    // change the value of a scope variable and notify listeners
    apply: function (scope, prop, value) {
      if (scope[prop] != value) {
        scope[prop] = value;
        if (!scope.$$phase) scope.$apply(prop);
      }
    }
  )
});

The "myUtil" factory listed above contains two utility functions:

  • watchScope adds watchers for several scope variables and calls an update function when any of them changes, except during directive initialization. It can optionally use a timeout to avoid calling the update function too often.
  • apply changes the value of a scope variable and notifies listeners of the change (unless the new value is the same as the previous one).

To use these utility functions from a custom directive, you would write:

app.directive("myDir", ["$rootScope", "myUtil", 
               function ($rootScope,   myUtil) {
  return {
    restrict: "E",
    scope: {
      v1: "@", v2: "@", v3: "@", v4: "@", v5: "@", v6: "@"
    },
    template: "<div/>",
    link: function (scope, element, attrs) {
      var ctr = 0,
          arr = ["v1", "v2", "v3", "v4", "v5", "v6"];
      myUtil.watchScope(scope, arr, updateFn);
      function updateFn() {
        console.log("# updating my-dir " + ++ctr);
        // modify DOM here
      }
    }
  }
}]);

As you can see, we simply added the "myUtil" factory to the directive constructor, making all its methods available to the directive.

You can see this code in action in this fiddle: jsfiddle.net/Wijmo/GJm9M/

Despite the apparent simplicity, there's a lot of interesting things going on that make this work. AngularJS examined the directive, detected the "myUtil" parameter, found the "myUtil" factory by name in the module definition, and injected a reference in the right place. Dependency Injection is a deep topic, and is described in the AngularJS documentation.

The fact that the dependency injection mechanism relies on names creates a problem related to minification. When you minify your code to put into production, variable names change and this can break the dependency injection. To work around this issue, AngularJS allows you to declare module elements using an array syntax that includes the argument names as strings. If you look at the directive definition code above, notice that the declaration contains an array with the parameter names (in this case only "myUtil") followed by the actual constructor. This allows AngularJS to look for the "myUtil" factory by name even if the minification process changes the name of the constructor parameter.

Important note on Minification: If you plan to minify your directives, you must use the array declaration technique on all directives that take parameters, and also on controller declarations that contain parameters. This fact is not well-documented, and will prevent directives with controller functions from working after minification. The Bootstrap tab directive listed in the Angular home page for example is not minifiable, but this one is: jsfiddle.net/Wijmo/ywUYQ/.

In addition to factory, AngularJS includes three other similar concepts: provider, service, and value. The differences between these are subtle. I have been using factories since I started working with Angular and so far have not needed any of the other flavors.

Examples

Now that we've reviewed all the basics, is time to go over a few examples to show how this all works in practice. The next sections describe a few useful directives that illustrate the main points and should help you get started writing your own.

Bootstrap Accordion Directive

Our first example is a pair of directives that create Boostrap accordions:

Bootstrap Accordion screenshot
Bootstrap Accordion Sample

The Bootstrap site has an example that shows how you can create an accordion using plain HTML:

<div class="accordion" id="accordion2">
  <div class="accordion-group">
    <div class="accordion-heading">
      <a class="accordion-toggle" data-toggle="collapse"
         data-parent="#accordion2" href="#collapseOne">
        Collapsible Group Item #1
      </a>
    </div>
    <div id="collapseOne" class="accordion-body collapse in">
      <div class="accordion-inner">
        Anim pariatur cliche...
      </div>
    </div>
  </div>
  <div class="accordion-group">
    <div class="accordion-heading">
      <a class="accordion-toggle" data-toggle="collapse"
        data-parent="#accordion2" href="#collapseTwo">
        Collapsible Group Item #2
      </a>
    </div>
    <div id="collapseTwo" class="accordion-body collapse">
      <div class="accordion-inner">
        Anim pariatur cliche...
      </div>
    </div>
  </div>
</div>

This works, but it is a lot of markup. And the markup contains references based on hrefs and element ids, which make maintenance non-trivial.

Using custom directives you could get the same result using this HTML:

<btst-accordion>
  <btst-pane title="<b>First</b> Pane">
    <div>Anim pariatur cliche …
  </btst-pane>
  <btst-pane title="<b>Second</b> Pane">
    <div>Anim pariatur cliche …
  </btst-pane>
  <btst-pane title="<b>Third</b> Pane">
    <div>Anim pariatur cliche …
  </btst-pane>
</btst-accordion>

This version is much smaller, easier to read and to maintain.

Let's see how this is done. First, we define a module and the "btstAccordion" directive:

var btst = angular.module("btst", []);
btst.directive("btstAccordion", function () {
  return {
    restrict: "E",        // the Accordion is an element
    transclude: true,     // it has HTML content
    replace: true,        // replace the original markup with our template
    scope: {},            // no scope variables required
    template:             // template assigns class and transclusion element
      "<div class='accordion' ng-transclude></div>",
    link: function (scope, element, attrs) {

      // make sure the accordion has an id
      var id = element.attr("id");
      if (!id) {
        id = "btst-acc" + scope.$id;
        element.attr("id", id);
      }

      // set data-parent and href attributes on accordion-toggle elements
      var arr = element.find(".accordion-toggle");
      for (var i = 0; i < arr.length; i++) {
        $(arr[i]).attr("data-parent", "#" + id);
        $(arr[i]).attr("href", "#" + id + "collapse" + i);
      }

      // set collapse attribute on accordion-body elements 
      // and expand the first pane to start
      arr = element.find(".accordion-body");
      $(arr[0]).addClass("in"); // expand first pane
      for (var i = 0; i < arr.length; i++) {
        $(arr[i]).attr("id", id + "collapse" + i);
      }
    },
    controller: function () {}
  };
});

The directive sets transclude to true because it has HTML content. The template used the ng-transclude directive to indicate which of the template elements will receive the transcluded content. In this case the template has only one element, so there is no other option, but that is not always the case.

The interesting part of the code is the link function. It starts by ensuring the accordion element has an id. If it doesn't, the code creates a unique ID based on the ID of the directive's scope. Once the element has an ID, the function uses jQuery to select the child elements that have the class "accordion-toggle" and sets their "data-parent" and "href" attributes. Finally, the code looks for "accordion-body" elements and sets their "collapse" attribute.

The directive also includes a controller member that contains an empty function. This is required because the accordion will have child elements which will check that the parent is of the proper type and specifies a controller.

The next step is the definition of the accordion pane directive. This one is very simple, most of the action happens right in the template and there's almost no code:

btst.directive('btstPane', function () {
  return {
    require: "^btstAccordion",
    restrict: "E",
    transclude: true,
    replace: true,
    scope: {
      title: "@"
    },
    template:
      "<div class='accordion-group'>" +
      "  <div class='accordion-heading'>" +
      "    <a class='accordion-toggle' data-toggle='collapse'>{{title}}</a>" +
      "  </div>" +
      "<div class='accordion-body collapse'>" +
      "  <div class='accordion-inner' ng-transclude></div>" +
      "  </div>" +
      "</div>",
    link: function (scope, element, attrs) {
      scope.$watch("title", function () {
        // NOTE: this requires jQuery (jQLite won't do html)
        var hdr = element.find(".accordion-toggle");
        hdr.html(scope.title);
      });
    }
  };
});

The require member specifies that the "btstPane" directive must be used within a "btstAccordion". The transclude member indicates panes will have HTML content. The scope has a single "title" property that will be placed in the pane header.

The template is fairly complex in this case. It was copied directly from the Boostrap sample page. Notice that we used the ng-transclude directive to mark the element that will receive the transcluded content.

We could stop here. The "{{title}}" property included in the template is enough to show the title in the proper place. However, this approach would only allow plain text in the pane headers. We used the link function to replace the plain text with HTML so you can have rich content in the accordion headers.

That's it. We have finished our first pair of useful directives. They are small but illustrate some important points and techniques: how to define nested directives, how to generate unique element IDs, how to manipulate the DOM using jQuery, and how to use the $watch function to listen to changes in scope variables.

Google Maps Directive

The next example is a directive that creates Google maps:

Google Maps screenshot
Google Maps Directive Sample

Before we start working on the directive, remember to add a reference to the Google APIs to the HTML page:

<!-- required to use Google maps -->
 <script type="text/javascript"
   src="https://maps.googleapis.com/maps/api/js?sensor=true">
</script>

Next, let's define the directive:

var app = angular.module("app", []);
app.directive("appMap", function () {
  return {
    restrict: "E",
    replace: true,
    template: "<div></div>",
    scope: {
      center: "=",        // Center point on the map
      markers: "=",       // Array of map markers
      width: "@",         // Map width in pixels.
      height: "@",        // Map height in pixels.
      zoom: "@",          // Zoom level (from 1 to 25).
      mapTypeId: "@"      // roadmap, satellite, hybrid, or terrain
    },

The center property is defined as by reference ("=") so it will support two-way binding. The app can change the center and notify the map (when the user selects a location by clicking a button), and the map can also change it and notify the app (when the user selects a location by scrolling the map).

The markers property is also defined as by reference because it is an array and serializing it as a string could be time-consuming (but it would also work).

The link function in this case contains a fair amount of code. It has to:

  1. initialize the map,
  2. update the map when scope variables change, and
  3. listen to map events and update the scope.

Here is how this is done:

link: function (scope, element, attrs) {
  var toResize, toCenter;
  var map;
  var currentMarkers;

  // listen to changes in scope variables and update the control
  var arr = ["width", "height", "markers", "mapTypeId"];
  for (var i = 0, cnt = arr.length; i < arr.length; i++) {
    scope.$watch(arr[i], function () {
      if (--cnt <= 0)
        updateControl();
    });
  }

  // update zoom and center without re-creating the map
  scope.$watch("zoom", function () {
    if (map && scope.zoom)
      map.setZoom(scope.zoom * 1);
  });
  scope.$watch("center", function () {
    if (map && scope.center)
    map.setCenter(getLocation(scope.center));
  });

The function that watches the scope variables is similar to the one we described earlier when we discussed sharing code. It calls an updateControl function when there are any changes to the variables. The updateControl function actually creates the map using the currently selected options.

The "zoom" and "center" scope variables are treated differently, because we don't want to re-create the map every time the user selects a new location or zooms in or out. These two functions check if the map has been created and simply update it.

Here is the implementation of the updateControl function:

// update the control
function updateControl() {

  // get map options
  var options = {
    center: new google.maps.LatLng(40, -73),
    zoom: 6,
    mapTypeId: "roadmap"
  };
  if (scope.center) options.center = getLocation(scope.center);
  if (scope.zoom) options.zoom = scope.zoom * 1;
  if (scope.mapTypeId) options.mapTypeId = scope.mapTypeId;
  // create the map and update the markers
  map = new google.maps.Map(element[0], options);
  updateMarkers();

  // listen to changes in the center property and update the scope
  google.maps.event.addListener(map, 'center_changed', function () {
    if (toCenter) clearTimeout(toCenter);
    toCenter = setTimeout(function () {
    if (scope.center) {
      if (map.center.lat() != scope.center.lat ||
          map.center.lng() != scope.center.lon) {
        scope.center = { lat: map.center.lat(), lon: map.center.lng() };
        if (!scope.$$phase) scope.$apply("center");
      }
    }
  }, 500);
}

The updateControl function starts by preparing an options object that reflects the scope settings, then uses the options object to create and initialize the map. This is a common pattern when creating directives that wrap JavaScript widgets.

After creating the map, the function updates the markers and adds an event handler so it is notified when the map center changes. The event handler checks to see if the current map center is different from the scope's center property. If it is, then the handler updates the scope and calls the $apply function so AngularJS will notify any listeners that the property has changed. This is how two-way binding works in AngularJS.

The updateMarkers function is pretty simple and does not contain anything that is directly related to AngularJS, so we won't list it here.

In addition to the map directive, this example contains:

  • Two filters that convert coordinates expressed as regular numbers into geographic locations such as 33°38'24"N, 85°49'2"W.
  • A geo-coder that converts addresses into geographic locations (also based on the Google APIs).
  • A method that uses the HTML5 geolocation service to get the user's current location.

Google's mapping APIs are extremely rich. This directive barely scratches the surface of what you can do with it, but hopefully it is enough to get you started if you are interested in developing location-aware applications.

You can find documentation for Google's mapping APIs here: https://developers.google.com/maps/documentation/

You can find Google's licensing terms here: https://developers.google.com/maps/licensing

Wijmo Chart Directive

The next example is a chart that shows experimental data and a linear regression. This sample illustrates the scenario described earlier where you have a particular need that is specialized and unlikely to be covered by standard directives shipped with commercial products:

Wijmo Chart screenshot

Wijmo Chart Directive Sample

This chart directive is based on the Wijmo line chart widget, and is used like this:

<app-chart 
    data="data" x="x" y="y" 
    reg-parms="reg"
    color="blue" >
</app-chart>

The parameters are as follows:

  • data: a list of objects with properties to plot
  • x, y: the names of the properties that will be shown on the x and y axis
  • reg: the linear regression results, an object with properties that represent the regression parameters and the coefficient of determination (AKA R2).
  • color: the color of the symbols on the chart.

In the initial version of the directive, the regression was calculated within the chart itself, and the "reg" parameter was not needed. But I decided that was not the right design, because the regression parameters are important outside the chart and should therefore be calculated in the scope of the controller. 

Without further ado, here is the directive implementation:

app.directive("appChart", function (appUtil) {
  return {
    restrict: "E",
    replace: true,
    scope: {
      data: "=",      // array that contains the data for the chart.
      x: "@",         // property that contains the X values.
      y: "@",         // property that contains the Y values.
      regParms: "=",  // regression parameters (a and b coefficients)
      color: "@"      // color for the data series.
    },
    template:
    "<div></div>",
    link: function (scope, element, attrs) {

      // watch for changes in the scope variables
      appUtil.watchScope(scope, ["x", "y", "color"], updateChartControl, true, true);

      // update chart data when data changes
      scope.$watch("data", updateChartData);

This first block of code defines the directive type and scope as usual. The link function uses the watchScope method that we presented earlier to watch several scope variables and call an updateChartControl method whenever any of the scope variables change.

Notice that we use a separate call to the scope.$watch data because we expect the chart data to change more often than the other properties, so we will provide a more efficient hander called updateChartData to handle those changes.

Here is the implementation of the updateChartControl method, which actually creates the chart.

// create/update the chart control
function updateChartControl(prop, val) {

  // use element font in the chart
  var fontFamily = element.css("fontFamily");
  var fontSize = element.css("fontSize");
  var textStyle = { "font-family": fontFamily, "font-size": fontSize };

  // set default values
  var color = scope.color ? scope.color : "red";

  // build options
  var options = {
    seriesStyles: [
      { stroke: color, "stroke-width": 0 },
      { stroke: "black", "stroke-width": 1, "stroke-opacity": .5 }
    ],
    seriesHoverStyles: [
      { stroke: color, "stroke-width": 0 },
      { stroke: "black", "stroke-width": 2, "stroke-opacity": 1 }
    ],
    legend: { visible: false },
    showChartLabels: false,
    animation: { enabled: false },
    seriesTransition: { enabled: false },
    axis: {
      x: { labels: { style: textStyle }, annoFormatString: "n0" },
      y: { labels: { style: textStyle }, annoFormatString: "n0" }
    },
    textStyle: textStyle
  };

  // create the chart
  element.wijlinechart(options);

  // go update the chart data
  updateChartData();
}

The code is similar to what we used in the Google maps directive earlier. It builds an options object containing configuration information, some of which is based on the directive parameters, and then uses this options object to create the actual chart by calling the element.wijlinechart method.

After creating the chart widget, the code calls the updateChartData method to populate the chart. The updateChartData method creates two data series. The first represents the data passed in through the scope variables, and the second represents the regression. The first series has as many data points as were passed in by the controller, and is shown as symbols. The second series represents the linear regressions, and therefore has only two points. It is shown as a solid line.

Wijmo Grid Directive

Our last example is a directive that implements an editable data grid:

Wijmo Grid screenshot

Wijmo Grid Directive Sample

This directive is based on the Wijmo grid widget, and is used like this:

<wij-grid
  data="data"
  allow-editing="true"
  after-cell-edit="cellEdited(e, args)" >
    <wij-grid-column
      binding="country" width="100" group="true">
    </wij-grid-column>
    <wij-grid-column
      binding="product" width="140" >
    </wij-grid-column>
    <wij-grid-column
      binding="amount" width="100" format="c2" aggregate="sum" >
    </wij-grid-column>
</wij-grid>

The "wij-grid" directive specifies the attributes for the grid, and the "wij-grid-column" directive specifies the attributes for individual grid columns. The markup above defines an editable grid with three columns "country", "product", and "amount". Values are grouped by country and group rows show the total amounts for each group.

The most interest part of this directive is the connection between the parent directive "wij-grid" and this child directives "wij-grid-column". To enable this connection, the parent directive specifies a controller function as follows:

app.directive("wijGrid", [ "$rootScope", "wijUtil", function ($rootScope, wijUtil) {
  return {
    restrict: "E",
    replace: true,
    transclude: true,
    template: "<table ng-transclude/>",
    scope: {
      data: "=",          // List of items to bind to.
      allowEditing: "@",  // Whether user can edit the grid.
      afterCellEdit: "&", // Event that fires after cell edits.
      allowWrapping: "@", // Whether text should wrap within cells.
      frozenColumns: "@"  // Number of non-scrollable columns
    },
    controller: ["$scope", function ($scope) {
      $scope.columns = [];
      this.addColumn = function (column) {
        $scope.columns.push(column);
      }
    }],
    link: function (scope, element, attrs) {
      // omitted for brevity, see full source here: 
      // http://jsfiddle.net/Wijmo/jmp47/
    }
  }
}]);

The controller function is declared using the array syntax mentioned earlier so it can be minified. In this example, the controller defines an addColumn function that will be called by the child "wij-grid-column" directives. The parent directive will then have access to the column information specified in the markup.

Here is how the "wij-grid-column" directive uses this function:

app.directive("wijGridColumn", function () {
  return {
    require: "^wijGrid",
    restrict: "E",
    replace: true,
    template: "<div></div>",
    scope: {
      binding: "@",     // Property shown in this column.
      header: "@",      // Column header content.
      format: "@",      // Format used to display numeric values in this column.
      width: "@",       // Column width in pixels.
      aggregate: "@",   // Aggregate to display in group header rows.
      group: "@",       // Whether items should be grouped by the values in this column.
      groupHeader: "@"  // Text to display in the group header rows.
    },
    link: function (scope, element, attrs, wijGrid) {
      wijGrid.addColumn(scope);
    }
  }
});

The require member specifies that the "wij-grid-column" directive requires a parent directive of type "wij-grid". The link function receives a reference to the parent directive (controller) and uses the addColumn method to pass its own scope to the parent. The scope contains all the information needed by the grid to create the column.

More Directives

In addition to the examples discussed in this article, the sample attached contains almost 50 other directives that you can use and modify. The sample application itself is structured following the principles suggested here, so you should have no problems navigating it.

In the sample, the directives can be found in three files under the scripts/directives folder:

  • btstDctv: Contains 13 directives based on the Bootstrap library. The directives include tabs, accordion, popover, tooltip, menu, typeahead, and numericInput.
  • googleDctv: Contains two directives based on Google's JavaScript APIs: a map and a chart.
  • wijDctv: Contains 24 directives based on the Wijmo library. The directives include input, layout, grids, and charts.

All three directive modules are included in source and minified format. We used Google's Closure minifier, which you can use on-line here: http://closure-compiler.appspot.com/home.

There is an on-line version of the Angular Explorer sample here: http://demo.componentone.com/wijmo/Angular/AngularExplorer/AngularExplorer.

Conclusion

I hope you had fun reading this article and that you are as excited about AngularJS and custom directives as I am.

Please feel free to use the code in the sample and to contact me with any feedback you may have. I am especially interested in ideas for new directives and on ways to make the directives presented more powerful and useful.

References

  1. AngularJS by Google. The AngularJS home page.
  2. AngularJS Directives documentation. The official documentation on AngularJS directives.
  3. AngularJS directives and the computer science of JavaScript. Interesting article on writing AngularJS directives.
  4. Video Tutorial: AngularJS Fundamentals in 60-ish Minutes. A nice video introducing AngularJS by Dan Wahling.
  5. About those directives. A series of videos on directives and more by members of the AngularJS team.
  6. Egghead.io. A series of how-to videos on AngularJS by John Lindquist.
  7. Polymer Project. What is coming after AngularJS.
  8. Wijmo AngularJS Samples. Several on-line demos created using AngularJS and custom directives.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Bernardo Castilho
Chief Technology Officer ComponentOne
United States United States
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pin
Champion Chen at 4-Jun-14 18:18
memberChampion Chen4-Jun-14 18:18 
GeneralRe: My vote of 5 Pin
Bernardo Castilho at 5-Jun-14 6:47
memberBernardo Castilho5-Jun-14 6:47 
QuestionLocation initialization Pin
Buratino2k at 10-Mar-14 23:47
memberBuratino2k10-Mar-14 23:47 
AnswerRe: Location initialization Pin
Bernardo Castilho at 12-Mar-14 5:56
memberBernardo Castilho12-Mar-14 5:56 
GeneralRe: Location initialization Pin
Buratino2k at 12-Mar-14 6:08
memberBuratino2k12-Mar-14 6:08 
GeneralRe: Location initialization Pin
Bernardo Castilho at 12-Mar-14 21:32
memberBernardo Castilho12-Mar-14 21:32 
GeneralRe: Location initialization [modified] Pin
Buratino2k at 13-Mar-14 0:05
memberBuratino2k13-Mar-14 0:05 
GeneralRe: Location initialization Pin
Bernardo Castilho at 13-Mar-14 7:46
memberBernardo Castilho13-Mar-14 7:46 
QuestionNice Article Pin
umesh-vishwa at 2-Mar-14 22:46
memberumesh-vishwa2-Mar-14 22:46 
AnswerRe: Nice Article Pin
Bernardo Castilho at 3-Mar-14 15:00
memberBernardo Castilho3-Mar-14 15:00 
QuestionGreat! Pin
Software Developer's Journal at 16-Oct-13 23:21
memberSoftware Developer's Journal16-Oct-13 23:21 
AnswerRe: Great! Pin
Bernardo Castilho at 17-Oct-13 4:11
memberBernardo Castilho17-Oct-13 4:11 
QuestionGreat explanation Pin
Member 10339457 at 16-Oct-13 1:21
memberMember 1033945716-Oct-13 1:21 
AnswerRe: Great explanation Pin
Bernardo Castilho at 16-Oct-13 4:33
memberBernardo Castilho16-Oct-13 4:33 
QuestionGoogle Maps directive question Pin
Evan Roth at 9-Oct-13 15:10
memberEvan Roth9-Oct-13 15:10 
AnswerRe: Google Maps directive question Pin
Bernardo Castilho at 9-Oct-13 20:20
memberBernardo Castilho9-Oct-13 20:20 
GeneralRe: Google Maps directive question [modified] Pin
Evan Roth at 10-Oct-13 7:43
memberEvan Roth10-Oct-13 7:43 
GeneralRe: Google Maps directive question Pin
Bernardo Castilho at 10-Oct-13 12:15
memberBernardo Castilho10-Oct-13 12:15 
GeneralRe: Google Maps directive question Pin
Evan Roth at 11-Oct-13 8:28
memberEvan Roth11-Oct-13 8:28 
GeneralWow. Helped me a lot Pin
Member 10283069 at 18-Sep-13 11:20
memberMember 1028306918-Sep-13 11:20 
GeneralRe: Wow. Helped me a lot Pin
Bernardo Castilho at 18-Sep-13 12:03
memberBernardo Castilho18-Sep-13 12:03 
GeneralMy vote of 5 Pin
Michael Cyze at 16-Aug-13 2:30
memberMichael Cyze16-Aug-13 2:30 
GeneralRe: My vote of 5 Pin
Bernardo Castilho at 16-Aug-13 7:05
memberBernardo Castilho16-Aug-13 7:05 
GeneralMy vote of 5 Pin
Mihai MOGA at 13-Jul-13 21:52
professionalMihai MOGA13-Jul-13 21:52 
GeneralRe: My vote of 5 Pin
Bernardo Castilho at 15-Jul-13 5:38
memberBernardo Castilho15-Jul-13 5:38 
QuestionWhat browser versions will this work in? Pin
Fla_Golfr at 20-Jun-13 3:36
professionalFla_Golfr20-Jun-13 3:36 
AnswerRe: What browser versions will this work in? Pin
Bernardo Castilho at 20-Jun-13 5:22
memberBernardo Castilho20-Jun-13 5:22 
GeneralVery interesting Pin
Dánjal Salberg Adlersson at 20-Jun-13 2:40
memberDánjal Salberg Adlersson20-Jun-13 2:40 
GeneralRe: Very interesting Pin
Bernardo Castilho at 20-Jun-13 5:15
memberBernardo Castilho20-Jun-13 5:15 
GeneralRe: Very interesting Pin
Bernardo Castilho at 22-Jul-13 15:11
memberBernardo Castilho22-Jul-13 15:11 
GeneralRe: Very interesting Pin
Dánjal Salberg Adlersson at 11-Sep-13 1:31
memberDánjal Salberg Adlersson11-Sep-13 1:31 
GeneralRe: Very interesting Pin
Bernardo Castilho at 11-Sep-13 3:27
memberBernardo Castilho11-Sep-13 3:27 
GeneralMy vote of 5 Pin
Ramkumar_S at 18-Jun-13 23:31
memberRamkumar_S18-Jun-13 23:31 
GeneralRe: My vote of 5 Pin
Bernardo Castilho at 19-Jun-13 3:30
memberBernardo Castilho19-Jun-13 3:30 
GeneralMy vote of 5 Pin
Brad Green at 18-Jun-13 7:04
memberBrad Green18-Jun-13 7:04 
GeneralRe: My vote of 5 Pin
Bernardo Castilho at 18-Jun-13 8:15
memberBernardo Castilho18-Jun-13 8:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150427.1 | Last Updated 28 Aug 2013
Article Copyright 2013 by Bernardo Castilho
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid