One of the most powerful concepts AngularJS is the directive. Directives are a key building block, allowing behavior to be assigned to HTML elements in a declarative way, turning the HTML DOM into a rich template language for responsive, interactive applications.

Directives represent a very powerful and flexible tool, and it's often unclear how best to apply this tool to various problems. This article is a collection of design examples that apply directives to solve particular classes of problem. In the process we will explore the key features of AngularJS directives and see how these features can interact to produce positive as well as negative results.

This article assumes some familiarity with AngularJS concepts such as scopes, controllers and templates. If you're completely new to Angular, I suggest reading the PhoneCat tutorial as a starting point, and getting some experience using pre-existing directives before attempting to build your own.

Directive Basics

Before we get stuck in let's quickly recap the general principles of directives, to establish the terminology that will appear in the remainder of the article.

As noted above, directives allow behavior to be assigned to HTML elements. This occurs during Angular's template compilation process, in which it walks a HTML DOM tree and matches each node against the table of defined directives.

A defined directive can apply to a particular element name, a particular attribute name, a particular class (in the CSS sense) or to a special format of HTML comment. Most directives apply to element and/or attribute names, since these are the most natural to use in templates.

Given the various ways directives can apply, a given DOM node can potentially have several different directives applied to it. Here's a simple example of an HTML element with three attribute-based directives:

Each of the attributes instantiates a different directive and each directive operates largely independently of the others. Since they are all operating on the same element, is is important that their respective concerns are well-separated to avoid strange collisions in behavior.

Directive Instantiation Phases

A directive is instantiated in two distinct phases.

The directive is first given an opportunity to "compile" its template, and in this phase it is operating on the original template DOM as given in the source HTML document, before any scopes have been assigned.

Most directives do not make use of this compilation phase, but a key example of the use of this phase is the ngRepeat directive, which produces zero or more copies of the template element it is given. During its compile phase it parses the repeat expression given in its attribute value, which thus informs how many copies of the element will be created.

Once directives have had an opportunity to "compile" they are then given the opportunity to "link". During the link phase the directive recieves its instance element and the corresponding scope, and the directive is responsible for connecting these together in whichever way is appropriate.

To understand the difference between the compile and link phases, the ngRepeat directive is again helpful. Considering again a simple example of a repeated element:

The ng-repeat attribute causes this element to be cloned potentially many times in the resulting DOM. From the perspective of the ngSrc directive, its compilation phase occurs only once, before those clones are created. Its linking phase is, on the other hand, likely to occur several times: once for each clone that is created, with each link call receiving a different instance element and scope.

Relationship To Scopes and Controllers

Angular's concept of scopes is also important when working with directives. Fundamentally a scope is just a collection of named values, possibly inheriting other named values from a parent scope.

Each scope is usually managed by a controller, which places data into the scope for use by templates as well as providing an API through which the template, as well as other controllers, can manipulate that data.

There are many ways to create scopes in Angular, and many applications of controllers. Directives are the most common means by which scopes are created, and instantiating controllers for those new scopes is often a key part of the link phase of a directive.

Simple Template Directives

By far the simplest case of a directive is one that exists just as an abstraction over a more complex HTML template, taking arguments from its attributes and making them available in the template.

This is such a common case that AngularJS allows it to be implemented in a completely declarative way:

This can then be used as follows:

In the above template declaration, the scope property tells the compiler that this directive needs its own local scope, and in turn requests that the scope should be populated with a name key whose value is bound to the contents of the name attribute. The @ prefix on the property value indicates that we wish to interpret the attribute value as an interpolated string, which means we can also use interpolation syntax if required:

In terms of the directive phases described earlier, the Angular compiler is effectively providing a default compile and link phase for this kind of directive. In the compile phase, the provided template is recursively passed back into the compiler for processing. The link phase can then just link the new scope to the compiled template, producing the desired result.

This simple usage of directives is only a small step above the built-in ngInclude directive. The difference is that our custom directive can take data from custom element attributes rather than only from values already present in the parent scope, creating a small layer of abstraction between the templates.

Simple template directives are a great way to make reusable, self-contained bundles of markup that can be called on many times in your application.

Template Wrappers

A small extension of the simple template case is a template that wraps some other caller-provided content. Consider for example a reusable modal dialog directive:

Since this template is longer than the previous one we'll keep it in a separate file and reference it by URL. Here's the contents of the template file:

First notice that in the scope property in the declaration we're now using the & prefix instead of the @ used previously. This requests that the given attribute be parsed as an expression, and the scope populated not with the result of the expression but instead a function that can be called to evaluate the expression. This allows the caller to provide an event handler that will be called on close, as we'll see in a moment.

The other point of note here is the use of the transclude property on the directive declaration, along with the ng-transclude attribute within the template. The former requests that the entire contents of the directive's template element be compiled and saved, while the latter causes the saved contents to be recalled and inserted as the contents of the annotated element.

With this all in place, we can make use of our modal dialog like this:

Here we assume that a parent controller has provided this dialogResult function in the scope. Its implementation is left as an exercise for the reader, but the result of this template will be an instance of the modal dialog template from above, with the provided question and buttons embedded in its content element. When the "close" widget on the modal is clicked, it will have the same effect as clicking the cancel button due to the use of the same event-handler for both.

Event-handling Directives

In the previous example we saw how a directive can provide an event-handling interface in addition to its other behavior. In the interests of separating concerns though, it's often useful to have directives whose only purpose is to detect events and signal them via expressions.

The built-in ngClick, ngBlur etc directives are examples of this in the standard library. Implementing an event-handling directive makes a good first example of a directive with custom compile and link phases, as opposed to providing a template and using the built-in functionality.

Let's consider the example of a directive that signals an event if the mouse pointer hovers over an element for three seconds. This is a contrived example but simple enough not to distract too much from the mechanism.

Unlike our previous examples, we do not specify the scope or template properties here. Only one directive on each element can use scope, so it is good manners to avoid its use on small directives intended for use in conjunction with others. We also want to have no visual effect on the document, so the use of template would be inappropriate here.

Instead, we manually implement the compile and link phases of our directive. In our compile phase, we make use of the $parse service, which is where Angular's expression parse is implemented. It takes a string containing an expression like "beginTakeover()", and returns a function that takes a scope and returns the result of evaluating the expression in that scope.

The compile function ends by returning the link function. In the link function it's time to bind our behavior to the DOM, in this case by registering some DOM event handlers on our instance element iElement.

When the hover condition is eventually detected through the successful completion of our timeout, we finally call the function we obtained during the compile phase, passing in the related scope.

Since we set restrict to 'A' in our declaration, this directive is valid only as an attribute. Where we left this unstated in the previous examples it defaulted to applying to both elements and attributes.

With all of that in place, it is a simple matter to use this directive:

Assuming some CSS is provided to make this element big enough to hover the mouse over, the beginTakeover function in the current scope will be called after the mouse dwells for three seconds, as we intended.

Recall that earlier we noted that there may be many calls to the link function for each call to the compile function. That is true here, for example if we were to combine ng-repeat with on-dwell:

In the above scenario, the directive's compile function will be called once, being passed in the single element that resulted from parsing the above HTML snippet. However, the returned link function will be for each object in the ads collection, and will be passed instead the cloned element that ng-repeat created, along with a child scope that contains one of the ads in the ad variable, causing ad.beginTakeover() to be called with the correct object for each element.

To keep distinct the concepts of the template element passed into compile and the instance element passed into link, it is conventional to name these and respectively.

Directives With Controllers

In our earliest examples we saw how a directive can be used just as a simple container for encapsulating a template. Sometimes a static template is not enough however, and a controller is desired to bring some behavior into play.

The following is a declaration of a simple "image carousel" directive, which takes an array of image URLs and shows them one at a time, with buttons provided to navigate to the previous and next images.

Here the caller provides, via the image-urls attribute, an expression that evaluates to an array of strings containing image URLs. Our controller is responsible for selecting an appropriate current image URL. The selection can potentially change whenever the list of images changes (e.g. if there are now fewer items in the list) or when the user clicks on one of the navigation buttons. Angular also helpfully calls our $watchCollection callback once after first registration, triggering us to call update for the first time to initialize.

Here is the directive's template:

This combination of a scope, a template and a controller makes it easy to encapsulate a re-usable interactive visual component, with the caller just providing the data.

On Directives that Load Data

Some developers are tempted to use directive controllers to load data from some data source and then display it. In most cases this is not advisable since it mixes the concern of loading the data with the concern of displaying it. It's better to at least separate the data loading into a separate controller, which can then be used via the ngController directive:

Better still, if your app is using a router it's often best to have the route controller be responsible for data loading, and limit the templates to just displaying data from the route's scope. This way the templates are completely unaware of where the data comes from.

Multiple Directives on One Element

As we saw earlier, it's easy to combine multiple isolated directives on a single element as long as their functionality doesn't conflict. Sometimes, however, it is desirable for one directive to interact with another, with one directive providing a public API that can be consumed by another.

By far the most common use of this is in implementing custom form controls using ngModel. All of the form control handling in AngularJS is implemented by applying a specific UI element directive (the view) to the same element as the ngModel directive, with the latter providing an API to the former.

Let's see how that looks from the perspective of the UI element directive, by implementing a simple radio-button-based boolean form element.

This is slightly different from our earlier examples in that this directive provides a link implementation but just uses the default compile implementation, since no processing of the template element is required. In this case Angular behaves as if we had a compile function that simply immediately returned the given link function.

The main new feature here is the require property in the definition. This tells Angular that if there is also an ngModel instance connected to this directive, then provide its controller as an extra parameter to the link function. The question mark at the beginning indicates that this is an optional dependency, so the link function will recieve null if there is no ngModel present.

A full description of the functionality of ngModel is best left for an article of its own, but suffice it to say that it's purpose is to allow a separation between the value stored in the scope -- that is, the model value -- from the form of the value used for presentation to the user. Between the two can be arbitrary transformations and validation steps.

In the above example, our interaction with the model is modest: we just translate between the boolean we store as the view model and the string we receive from the primitive form elements in the template. Here is the template, incidentally:

For convenience we're also using separate instances of ngModel on our internal radio buttons, but of course these instances are distinct from the one applied directly to our yesNoPicker element.

When this element is used in the template it must be used alongside ngModel in order to instantiate the controller we expect:

This technique allows us to consume the API provided by another directive on the same element. In the next section, we'll see how such an API can be provided.

Interacting with a Parent Directive

Sometimes it is necessary to build a template construct that is too complex to be declared with only one element. Constructs like ngSwitch require both a container element that establishes a context and then zero or more child elements that complete the definition.

These multi-element constructs can be implemented using the same require mechanism we saw in the previous section. To demonstrate, let's build a directive that provides an easy way to create an HTML table from a list of objects.

In this case we will actually need two directives: one to establish the table container, and the other to declare the individual columns.

This example brings together many features we've visited in earlier examples. The autoTable directive is just like our image carousel example where a template is combined with a controller, but this time the controller also provides the registerColumn method as its public API.

The col directive declares that it requires the autoTable directive, but this time it uses the ?^ prefix to indicate that this directive is required on the parent element. Again we declare it as optional, this time so that we don't interfere with the normal use of the col element in a plain HTML table.

Earlier we used $parse to process AngularJS expressions, while in this example we used $interpolate to process a string that may contain template interpolation syntax, like 'Hello {{ name }}!'. Its result is the same: a function that takes a scope (or scope-like object) and returns the string result.

Here, as usual, is the template for the autoTable directive:

The col directive does not need a template because it is used only to provide data to the autoTable directive and it will be removed from the DOM once the template is initialized.

This pair of directives can then be used together as follows:

This then expects a users array looking like this:

It is a rare situation that requires a multi-directive construct like this, but when the need arises Angular's template system still provides for a clean separation of concerns between the different components by allowing each directive to provide a public API.

Conclusion

Throughout this article we have seen that directives are a very flexible mechanism that can be employed to solve several different classes of problem. Fundamentally all of these different techniques can be implemented in terms of the low-level ability to define a custom compilation function, but Angular provides several shortcuts to simplify the most common cases of custom directives.

The examples in this article are intended to show some different ways the directive features can be combined to solve real-world problems. In summary, the features we've discussed are:

  • Directive-specific scopes with bindings to the parent scope via HTML attribute values, allowing data to be passed in from a parent directive.

  • Directive-specific controller APIs, allowing data to be passed in from a child or sibling directive.

  • Custom compilation and linking functions, allowing for arbitrary interactions with both the template and instance DOM elements.

  • Child content transclusion, allowing a directive to wrap arbitrary child content inside additional template HTML.

These building blocks together add up to support all of the varied functionality in Angular's standard directive library, as well as supporting your application's specialized DOM-manipulation needs.

For an example of a novel use of directives, see my other article on view-specific sidebars, which shows how directives can be used to move elements out of their original declaration context and into other parts of the document, such as the head element or an application's sidebars.

Directives are easily the most powerful feature of AngularJS, but also arguably the most misunderstood. I hope this article has shed some light on the various capabilities of directives and how they can be applied to produce a modular, maintainable application, or a useful reusable utility library.