All you need to know about AngularJS Directives

What is a Directive?

At a very high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tells AngularJS’s HTML compiler ($compile) to attach a specified behavior to that DOM element (e.g. via event listeners), or even to transform the DOM element and its children.

Angular comes with a set of built-in directives like ngBind, ngModel, and ngClass. Similar to how we create our own filters and services, we can also create our own custom directives. When AngularJS bootstraps our application, the HTML compiler traverses the DOM, matching directives against the DOM elements.

What does compile mean in AngularJS

For AngularJS, “compilation” means attaching directives to the HTML to make it interactive. The reason we use the term “compile” is that the recursive process of attaching directives mirrors the process of compiling source code in compiled programming languages.

How matching the directives work?

An element matches a directive when the directive is part of its declaration. In the following example, we say that the <input> element matches the ngModel directive:

The following <input> element also matches ngModel:

And the following element matches the person directive:

Angular normalizes an element’s tag and attribute name to determine which elements match which directives. We typically refer to directives by their case-sensitive camelCase normalized name (e.g. ngModel). However, since HTML is case-insensitive, we refer to directives in the DOM by lower-case forms, typically using dash-delimited attributes on DOM elements (e.g. ng-model).

How AngularJS Normalization works?

The normalization process is as follows:

  1. Strip x- and data- from the front of the element/attributes.
  2. Convert the :, , or _-delimited name to camelCase.

For example, the following forms are all equivalent and match the ngBind directive:

What are the best practices in choosing a directive format/name?

According to AngularJS documentation, prefer using the dash-delimited format (e.g. ng-bind for ngBind). If you want to use an HTML validating tool, you can instead use the data-prefixed version (e.g. data-ng-bind for ngBind). The other forms shown above are accepted for legacy reasons but are advised to avoid. Also, Prefer using directives via tag name and attributes over comment and class names. Doing so generally makes it easier to determine what directives a given element matches.

In order to avoid collisions with some future standard, it’s best to prefix our own directive names. For instance, if you created a <carousel> directive, it would be problematic if HTML7 introduced the same element. A two or three letter prefix (e.g. btfCarousel) works well. Similarly, do not prefix your own directives with ng or they might conflict with directives included in a future version of Angular.

Anatomy of a Directive

Here is the anatomy of a directive in its simplest form:

Let’s try to understand each of the above lines of code in detail.

Registering a directive

Much like controllers, directives are registered on modules. To register a directive, you use the module.directive API. module.directive takes the normalized directive name followed by a factory function. This factory function should return an object with the different options to tell $compile how the directive should behave when matched.

Notice that before return statement, we can write functions that may be reused within different inner functions of our directive, or to configure things for use. What follows after this is called the Directive Definition Object.

restrict

The first configuration item on the returnable object is the restrict key. It accepts one (or multiple) of the letters EACM, representing Element, Attribute, Class, and Meta. Following is a sample directive signature that has the restriction configuration set to ‘E’ (Element)

restrict: ‘E’  – The directive will trigger in the following scenario: <directive-name/>
restrict: ‘A’  – The directive will trigger in the following scenario: <div directive-name/>
restrict: ‘C’  – The directive will trigger in the following scenario: <div class=’directive-name’/>
restrict: ‘M’ – The directive will trigger in the following scenario: <!–directive: directive-name–>

scope

The scope parameter takes 3 values:

  1. scope:falseThis is the default value and instructs your directive to share its scope with the parent scope. Any changes done on this scope will automatically be done to the parent.
  2. scope:trueThis instructs the directive to create a new child scope which inherits from the parent prototypically.
  3. scope:{}This is known as isolateScope. Isolate Scope is a way to pass individual things from the parent scope into the directive scope, without inheriting everything. There are three ways for passing scope properties as discussed below.

Isolate scope parameters

  1. ‘@’ : Attribute binding also known as one way binding.
  2. ‘=’  : Two-way binding
  3. ‘&’  : Expression binding

Isolated scopes is an interesting topic and is worthy of its own post. Will update a link here when its done.

transclude

In computer science, transclusion is the inclusion of part or all of an electronic document into one or more other documents by reference. In AngularJS, transclusion is about injecting content into an element without loosing the content of that element.

To learn more about transclusion, please read my post : Understanding transclusion in AngularJS

template

Template is an inline template with HTML that will be appended (or replaced) to an element where the directive is applied. This is particularly useful when we want to share directives across apps and you only want to pass a single file around.

templateUrl

If we prefer to load a template over ajax, we can specify the templateUrl option, which will use ajax to pull the template.

controller

Controller is the directive’s required controller instance(s) or its own controller (if any). The exact value depends on the directive’s require property. Use controller when you want to expose an API to other directives. In the following example, each directive will get its own instance of the controller, but this allows us to share the logic between as many components as we want.

controllerAs

If you are a fan of readable code and like to use controllerAs, you can do that in your directives as well. When you define a Directive Definition Object (DDO) in a directive you can add a controllerAs property. Starting with Angular 1.3 you’ll also need to add a bindToController property as well to ensure that properties are bound to the controller rather than to the scope.

require

In the above example, if we want to share the same instance of a controller, then we use require.

require also ensures the presence of another directive and then includes its controller as a parameter to the link function. So if you have two directives on one element, your directive can require the presence of the other directive and gain access to its controller methods. A common use case for this is to require ngModel.

So, fundamentally what require allows us to do is to pull in one (or multiple) directive controllers to expose them as an API. The controller name can be prefixed with ^ to indicate that the directive exposing the controller must be above the current element in the DOM, ?, to indicate that the controller is optional, or ^? to indicate it is up AND optional. Lack of a prefix indicates that the directive must be a sibling.

compile

Use the compile function to change the original DOM (template element) before AngularJS creates an instance of it and before a scope is created. While there can be multiple element instances, there is only one template element. The ng-repeat directive is a perfect example of such a scenario. That makes the compile function the perfect place to make changes to the DOM that should be applied to all instances later on, because it will only be run once and thus greatly enhances performance if you are stamping out a lot of instances.

pre-link function

Use the pre-link function to implement logic that runs when AngularJS has already compiled the child elements, but before any of the child element’s post-link functions have been called. The scope, instance element and instance attributes are passed to the pre-link function as arguments:

post-link function

Use the post-link function to execute logic, knowing that all child elements have been compiled and all pre-link and post-link functions of child elements have been executed. This is the reason the post-link function is considered the safest and default place for your code.

The scope, instance element and instance attributes are passed to the post-link function as arguments:

priority

The priority is only relevant when you have multiple directives on one element. The priority determines in what order those directives will be applied/started. In most cases you wouldn’t need a priority, but sometimes when you use the compile function, you want to make sure that your compile function runs first.

When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which the directives are applied. The priority is used to sort the directives before their compile functions get called. Priority is defined as a number. Directives with greater numerical priority are compiled first. Pre-link functions are also run in priority order, but post-link functions are run in reverse order. The order of directives with the same priority is undefined. The default priority is 0.

terminal

The terminal property is also only relevant for directives that are on the same HTML element. That is, if you have <div my-directive1></div> <div my-directive2></div>, priority and terminal in your directives my-directive1 and my-directive2 won’t affect each other. They will only affect each other if you have <div my-directive1 my-directive2></div>.

The terminal property tells Angular to skip all directives on that element that comes after it (lower priority).

Consider the example below:

So, if you apply all the three directives on the same element like this:

You’d only see “I’m myDirective2” and “I’m myDirective3” in the console.

Where as, if they are applied on different elements like:

You’d see “I’m myDirective1” as well, since they are on different elements.

To learn more about the built-in directives in AngularJS, follow this link:

Understanding AngularJS built-in directives and how to use them

Leave a Reply

Top