AngularJS From Beginning: Directive - Part Four

I am here to continue the discussion around AngularJS. Today, we will go through one of the main features of the AngularJS i.e., custom directive. Also in case you have not had a look at our previous articles of this series, go through the following links:

In the previous articles, we discussed the AngularJS basic structure including control and model. Also, we discussed how to use the built-in filter how to create custom filters, and about AngularJS service. Now, in this article, we will discuss the directive concept in Angular JS.

Actually from a user’s point of view, directives are nothing more than custom HTML tags that are added to application templates. Directives can be simple or they can be complex. Directives are used by the AngularJS HTML compiler to enhance the functionality of the associated template. Some examples of AngularJS directives are ngModel, ngView, and ngRepeat etc. The word Compiler related to AngularJS is often confusing to developers new to the framework. Actually, the word compiler takes on a whole new meaning in the context of AngularJS. Compiling HTML in AngularJS is simply the process of searching through the DOM tree to identify HTML elements associated with directives. The compiler then populates the template and assigns events to the associated elements in the templates.

What are Directives?

Now, it is a very common question in AngularJS -- what are directives? Directives are very valuable in AngularJS and what sets AngularJS apart from most javascript client-side frameworks. Due to the AngularJS inbuilt directives, we can avoid creating model classes with hundreds of lines of code and also, we have a simplified model and view in AngularJS that allows developers to quickly create powerful javascript applications. Directives are created using the Module. directive method and the arguments are the name of the new directive and a factory function that creates the directives.

Naming Convention for Custom Directives

Since HTML is a case-insensitive language, I always refer to a directive's name in a camel case like link-Menu in the HTML template file. The AngularJS HTML compiler then normalizes the directive name into its camel case equivalent, i.e. linkMenu. Also, remember that all directive names used in templates must be unique. Directive names cannot match with any existing HTML tag name or with the AngularJS inbuilt directives name.

The following attributes can be used during the creation of new AngularJS Directives.

  1. Restrict: The restrict attribute is how AngularJS triggers the directive inside a template. The default value of the restrict option is “A”. The value of “A” causes the directives to be triggered on the attribute name. Other than “A”, the restrict option has “E” (only matches the element name), “C” (only matches class name), and “M” (only matches the comment name) or any combination among the four options.
  2. TemplateUrl: The templateUrl attribute tells the AngularJS HTML compiler to replace the custom directive inside a template with HTML content located inside a separate file. The link-Menu (say, our custom directive name) attribute will be replaced with the content of our original menu template file.
  3. Template: Specify an inline template as a string. Not used if you’re specifying your template as a URL.
  4. Replace: if true, replace the current element. If false or unspecified, append this directive to the current element.
  5. Transclude: This lets you move the original children of a directive to a location inside the new template.
  6. Scope: Create a new scope for this directive rather than inheriting the parent scope.
  7. Controller: Create a controller that publishes an API for communicating across directives.
  8. Require: Require that another directive be present for this directive to function correctly.
  9. Link: Programmatically modify resulting DOM element instances, add event listeners, and set up data binding.
  10. Compile: Programmatically modify the DOM template for features across copies of a directive, as when used in other directives. Your compile function can also return link functions to modify the resulting element instances.

Compile and Link Functions

While inserting templates is useful, the really interesting work of any directive happens in its compile or its link function. The compile and link functions are named after the two phases Angular uses to create the live view for your application. Let’s take a high-level view of Angular’s initialization process, in order:

Script loads: Angular loads and looks for the ng-app directive to find the application boundaries.

Compile phase

In this phase, Angular walks the DOM to identify all the registered directives in the template. For each directive, it then transforms the DOM based on the directive’s rules (template, replace, transclude, and so on), and calls the compile function if it exists. The result is a compiled template function, which will invoke the link functions collected from all of the directives.

Link phase

To make the view dynamic, Angular then runs a link function for each directive. The link functions typically create listeners on the DOM or the model. These listeners keep the view and the model in sync at all times. So we’ve got the compile phase, which deals with transforming the template, and the link phase, which deals with modifying the data in the view. Along these lines, the primary difference between the compile and link functions in directives is that compile functions deal with transforming the template itself, and link functions deal with making a dynamic connection between model and view. It is in this second phase that scopes are attached to the compiled link functions, and the directive becomes live through data binding. These two phases are separate for performance reasons. Compile functions execute only once in the compile phase, whereas link functions are executed many times, once for each instance of the directive. For example, let’s say you use ng-repeat over your directive. You don’t want to call compile, which causes a DOM walk on each ng-repeat iteration. Instead, you want to compile once, then link.

Let’s take a look at the syntax for each of these again to compare. For compile, we have.

compile: function(compileElement, compileAttrs, transclude) {
    return {
        pre: function preLink(scope, iElement, iAttrs, controller) {
            // pre-link logic
        },
        post: function postLink(scope, iElement, iAttrs, controller) {
            // post-link logic
        }
    };
}

And for the link, it is.

link: function postLink(scope, iElement, iAttrs) {
    // Code goes here
}

Notice that one difference here is that the link function gets access to a scope but compile does not. This is because, during the compile phase, the scope doesn’t exist yet. You do, however, have the ability to return link functions from the compile function. These link functions do have access to the scope. Notice also that both compile and link receive a reference to their DOM element and the list of attributes for that element. The difference here is that the compile function receives the template element and attributes from the template, and thus gets the prefix. The link function receives them from the view instances created from the template, and thus gets the prefix.

Scopes

You will often want to access a scope from your directive to watch model values and make UI updates when they change, and to notify Angular when external events cause the model to change. This is most common when you’re wrapping some non-Angular component from jQuery, Closure, or another library, or implementing simple DOM events. Evaluate Angular expressions passed into your directive as attributes. When you want a scope for one of these reasons, you have three options for the type of scope you will get.

  1. The existing scope from your directives DOM element.
  2. A new scope you create that inherits from your enclosing controller’s scope. Here, you will have the ability to read all the values in the scopes above this one in the tree. This scope will be shared with any other directives on your DOM element that request this kind of cope and can be used to communicate with them.
  3. An isolated scope that inherits no model properties from its parent. You will want to use this option when you need to isolate the operation of this directive from the parent scope when creating reusable components.

We can create the above-mentioned scope configuration as per the below syntax.

Scope Type Syntax
existing scope scope: false (this is the default if unspecified)
new scope scope: true<
isolate scope scope: { /* attribute names and binding style */ }

When you create an isolated scope, you don’t have access to anything in the parent scope’s model by default. You can, however, specify that you want specific attributes passed into your directive. You can think of these attribute names as parameters to the function. Note that while isolated scopes don’t inherit model properties, they are still children of their parent scope. Like all other scopes, they have a parent property that references their parent. You can pass specific attributes from the parent scope to the isolate scope by passing a map of directive attribute names. There are three possible ways to pass data to and from the parent scope. We call these different ways of passing data “binding strategies.” You can also, optionally, specify a local alias for the attribute name.

Symbol Meaning
@ Pass this attribute as a string. You can also data bind values from enclosing scopes by using interpolation {{}} in the attribute value.
= Data bind this property with a property in your directive’s parent scope.
& Pass in a function from the parent scope to be called later.

Now to demonstrate the directive concept, we develop two simple programs on the basis of directive. Before creating the specific program, first, we need to create the angular modular app.

var testApp = angular.module('TestApp', []);

First Program

Html file

<!DOCTYPE html>  
<html ng-app="TestApp">  
<head>  
    <title>AngularJS Directive</title>  
    <script src="angular.js"></script>  
    <link href="../../RefStyle/bootstrap.min.css" rel="stylesheet" />  
    <script src="app.js"></script>  
    <script src="Directive1.js"></script>  
</head>  
<body ng-controller="FactoryController">  
    <div class="panel panel-default">  
        <div class="panel-heading">  
            <h3>Products</h3>  
        </div>  
        <div class="panel-body">  
            <div unordered-list="products" list-property="name"></div>  
            <div unordered-list="products" list-property="price | currency"></div>  
        </div>  
    </div>  
</body>  
</html>

Controller (code)

testApp.controller('FactoryController', function ($scope) {  
    $scope.products = [
        { name: "Apples", category: "Fruit", price: 1.20, expiry: 10 },
        { name: "Bananas", category: "Fruit", price: 2.42, expiry: 7 },
        { name: "Pears", category: "Fruit", price: 2.02, expiry: 6 }
    ];  

    $scope.incrementPrices = function () {  
        for (var i = 0; i < $scope.products.length; i++) {  
            $scope.products[i].price++;  
        }  
    };  
});

Directive

testApp.directive("unorderedList", function () {
    return function (scope, element, attrs) {
        var data = scope[attrs["unorderedList"]];
        var propertyName = attrs["listProperty"];
        var propertyExpression = attrs["listProperty"];
        
        if (angular.isArray(data)) {
            var listElem = angular.element("<ul>");
            element.append(listElem);
            
            for (var i = 0; i < data.length; i++) {
                listElem.append(angular.element('<li>').text(scope.$eval(propertyExpression, data[i])));
            }
        }
    };
});

The output of the program is as below.

Product

Second Program

Html file

<html ng-app="TestApp">
  <head>
    <title>AngularJS Directive</title>
    <script src="angular.js"></script>
    <link href="../../RefStyle/bootstrap.min.css" rel="stylesheet" />
    <script src="app.js"></script>
    <script src="Directive2.js"></script>
  </head>
  <body ng-controller="EmployeeCtrl">
    <div class="panel panel-default">
      <div>
        <h1>Employee data:</h1>
        <div my-employee></div>
      </div>
    </div>
  </body>
</html>

Controller and Directive code

testApp.controller('EmployeeCtrl', ['$scope', function($scope) {
    var Employee = function(name, age) {
        this.name = name;
        this.age = age;
    };

    var GetEmployees = function() {
        return [
            new Employee("First employee", 56),
            new Employee("Second employee", 44),
            new Employee("Last employee", 32)
        ];
    };

    $scope.employeeData = {
        employees: GetEmployees()
    };
}]);

testApp.directive("myEmployee", function() {
    return {
        template: 'Name - {{employeeData.employees[0].name}}, Age - {{employeeData.employees[0].age}}'
    };
});

The output of the above code is.

Data