Directives are a way to teach HTML new tricks. During DOM compilation directives are matched against the HTML and executed. This allows directives to register behavior, or transform the DOM.

Angular comes with a built in set of directives which are useful for building web applications but can be extended such that HTML can be turned into a declarative domain specific language (DSL).

Invoking directives from HTML

Directives have camel cased names such as ngBind. The directive can be invoked by translating the camel case name into snake case with these special characters :, -, or _. Optionally the directive can be prefixed with x-, or data- to make it HTML validator compliant. Here is a list of some of the possible directive names: ng:bind, ng-bind, ng_bind, x-ng-bind and data-ng-bind.

The directives can be placed in element names, attributes, class names, as well as comments. Here are some equivalent examples of invoking myDir. (However, most directives are restricted to attribute only.)

  <span my-dir="exp"></span>
  <span class="my-dir: exp;"></span>
  <my-dir></my-dir>
  <!-- directive: my-dir exp -->

Directives can be invoked in many different ways, but are equivalent in the end result as shown in the following example.

Source







Demo

String interpolation

During the compilation process the compiler matches text and attributes using the $interpolate service to see if they contain embedded expressions. These expressions are registered as watches and will update as part of normal digest cycle. An example of interpolation is shown here:

<a href="img/{{username}}.jpg">Hello {{username}}!</a>

Compilation process, and directive matching

Compilation of HTML happens in three phases:

  1. First the HTML is parsed into DOM using the standard browser API. This is important to realize because the templates must be parsable HTML. This is in contrast to most templating systems that operate on strings, rather than on DOM elements.

  2. The compilation of the DOM is performed by the call to the $compile() method. The method traverses the DOM and matches the directives. If a match is found it is added to the list of directives associated with the given DOM element. Once all directives for a given DOM element have been identified they are sorted by priority and their compile() functions are executed. The directive compile function has a chance to modify the DOM structure and is responsible for producing a link() function explained next. The $compile() method returns a combined linking function, which is a collection of all of the linking functions returned from the individual directive compile functions.

  3. Link the template with scope by calling the linking function returned from the previous step. This in turn will call the linking function of the individual directives allowing them to register any listeners on the elements and set up any watches with the scope. The result of this is a live binding between the scope and the DOM. A change in the scope is reflected in the DOM.

  var $compile = ...; // injected into your code
  var scope = ...;

  var html = '<div ng-bind='exp'></div>';

  // Step 1: parse HTML into DOM element
  var template = angular.element(html);

  // Step 2: compile the template
  var linkFn = $compile(template);

  // Step 3: link the compiled template with the scope.
  linkFn(scope);

Reasons behind the compile/link separation

At this point you may wonder why the compile process is broken down to a compile and link phase. To understand this, let's look at a real world example with a repeater:

  Hello {{user}}, you have these actions:
  <ul>
    <li ng-repeat="action in user.actions">
      {{action.description}}
    </li>
  </ul>

The short answer is that compile and link separation is needed any time a change in model causes a change in DOM structure such as in repeaters.

When the above example is compiled, the compiler visits every node and looks for directives. The {{user}} is an example of an interpolation directive. ngRepeat is another directive. But ngRepeat has a dilemma. It needs to be able to quickly stamp out new lis for every action in user.actions. This means that it needs to save a clean copy of the li element for cloning purposes and as new actions are inserted, the template li element needs to be cloned and inserted into ul. But cloning the li element is not enough. It also needs to compile the li so that its directives such as {{action.descriptions}} evaluate against the right scope. A naive method would be to simply insert a copy of the li element and then compile it. But compiling on every li element clone would be slow, since the compilation requires that we traverse the DOM tree and look for directives and execute them. If we put the compilation inside a repeater which needs to unroll 100 items we would quickly run into performance problems.

The solution is to break the compilation process into two phases; the compile phase where all of the directives are identified and sorted by priority, and a linking phase where any work which links a specific instance of the scope and the specific instance of an li is performed.

ngRepeat works by preventing the compilation process from descending into the li element. Instead the ngRepeat directive compiles li separately. The result of the li element compilation is a linking function which contains all of the directives contained in the li element, ready to be attached to a specific clone of the li element. At runtime the ngRepeat watches the expression and as items are added to the array it clones the li element, creates a new scope for the cloned li element and calls the link function on the cloned li.

Summary:

Writing directives (short version)

In this example we will build a directive that displays the current time.

Source





Demo

Writing directives (long version)

An example skeleton of the directive is shown here, for the complete list see below.

  var myModule = angular.module(...);

  myModule.directive('directiveName', function factory(injectables) {
    var directiveDefinitionObject = {
      priority: 0,
      template: '<div></div>',
      templateUrl: 'directive.html',
      replace: false,
      transclude: false,
      restrict: 'A',
      scope: false,
      compile: function compile(tElement, tAttrs, transclude) {
        return {
          pre: function preLink(scope, iElement, iAttrs, controller) { ... },
          post: function postLink(scope, iElement, iAttrs, controller) { ... }
        }
      },
      link: function postLink(scope, iElement, iAttrs) { ... }
    };
    return directiveDefinitionObject;
  });

In most cases you will not need such fine control and so the above can be simplified. All of the different parts of this skeleton are explained in following sections. In this section we are interested only in some of this skeleton.

The first step in simplyfing the code is to rely on the default values. Therefore the above can be simplified as:

  var myModule = angular.module(...);

  myModule.directive('directiveName', function factory(injectables) {
    var directiveDefinitionObject = {
      compile: function compile(tElement, tAttrs) {
        return function postLink(scope, iElement, iAttrs) { ... }
      }
    };
    return directiveDefinitionObject;
  });

Most directives concern themselves only with instances, not with template transformations, allowing further simplification:

  var myModule = angular.module(...);

  myModule.directive('directiveName', function factory(injectables) {
    return function postLink(scope, iElement, iAttrs) { ... }
  });

Factory method

The factory method is responsible for creating the directive. It is invoked only once, when the compiler matches the directive for the first time. You can perform any initialization work here. The method is invoked using the $injector.invoke which makes it injectable following all of the rules of injection annotation.

Directive Definition Object

The directive definition object provides instructions to the compiler. The attributes are:

Compile function

  function compile(tElement, tAttrs, transclude) { ... }

The compile function deals with transforming the template DOM. Since most directives do not do template transformation, it is not used often. Examples that require compile functions are directives that transform template DOM, such as ngRepeat, or load the contents asynchronously, such as ngView. The compile function takes the following arguments.

NOTE: The template instance and the link instance may not be the same objects if the template has been cloned. For this reason it is not safe in the compile function to do anything other than DOM transformation that applies to all DOM clones. Specifically, DOM listener registration should be done in a linking function rather than in a compile function.

A compile function can have a return value which can be either a function or an object.

Linking function

  function link(scope, iElement, iAttrs, controller) { ... }

The link function is responsible for registering DOM listeners as well as updating the DOM. It is executed after the template has been cloned. This is where most of the directive logic will be put.

Pre-linking function

Executed before the child elements are linked. Not safe to do DOM transformation since the compiler linking function will fail to locate the correct elements for linking.

Post-linking function

Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function.

Attributes

The Attributes object - passed as a parameter in the link() or compile() functions - is a way of accessing:

function linkingFn(scope, elm, attrs, ctrl) {
  // get the attribute value
  console.log(attrs.ngModel);

  // change the attribute
  attrs.$set('ngModel', 'new value');

  // observe changes to interpolated attribute
  attrs.$observe('ngModel', function(value) {
    console.log('ngModel has changed value to ' + value);
  });
}

Understanding Transclusion and Scopes

It is often desirable to have reusable components. Below is a pseudo code showing how a simplified dialog component may work.

  <div>
    <button ng-click="show=true">show</button>
    <dialog title="Hello {{username}}."
            visible="show"
            on-cancel="show = false"
            on-ok="show = false; doSomething()">
       Body goes here: {{username}} is {{title}}.
    </dialog>
  </div>

Clicking on the "show" button will open the dialog. The dialog will have a title, which is data bound to username, and it will also have a body which we would like to transclude into the dialog.

Here is an example of what the template definition for the dialog widget may look like.

  <div ng-show="visible">
    <h3>{{title}}</h3>
    <div class="body" ng-transclude></div>
    <div class="footer">
      <button ng-click="onOk()">Save changes</button>
      <button ng-click="onCancel()">Close</button>
    </div>
  </div>

This will not render properly, unless we do some scope magic.

The first issue we have to solve is that the dialog box template expects title to be defined, but the place of instantiation would like to bind to username. Furthermore the buttons expect the onOk and onCancel functions to be present in the scope. This limits the usefulness of the widget. To solve the mapping issue we use the locals to create local variables which the template expects as follows:

  scope: {
    title: '@',             // the title uses the data-binding from the parent scope
    onOk: '&',              // create a delegate onOk function
    onCancel: '&',          // create a delegate onCancel function
    visible: '='            // set up visible to accept data-binding
  }

Creating local properties on widget scope creates two problems:

  1. isolation - if the user forgets to set title attribute of the dialog widget the dialog template will bind to parent scope property. This is unpredictable and undesirable.

  2. transclusion - the transcluded DOM can see the widget locals, which may overwrite the properties which the transclusion needs for data-binding. In our example the title property of the widget clobbers the title property of the transclusion.

To solve the issue of lack of isolation, the directive declares a new isolated scope. An isolated scope does not prototypically inherit from the child scope, and therefore we don't have to worry about accidentally clobbering any properties.

However isolated scope creates a new problem: if a transcluded DOM is a child of the widget isolated scope then it will not be able to bind to anything. For this reason the transcluded scope is a child of the original scope, before the widget created an isolated scope for its local variables. This makes the transcluded and widget isolated scope siblings.

This may seem to be unexpected complexity, but it gives the widget user and developer the least surprise.

Therefore the final directive definition looks something like this:

transclude: true,
scope: {
    title: '@',             // the title uses the data-binding from the parent scope
    onOk: '&',              // create a delegate onOk function
    onCancel: '&',          // create a delegate onCancel function
    visible: '='            // set up visible to accept data-binding
},
restrict: 'E',
replace: true

Creating Components

It is often desirable to replace a single directive with a more complex DOM structure. This allows the directives to become a short hand for reusable components from which applications can be built.

Following is an example of building a reusable widget.

Source









Demo