AngularJS From Beginning: Directive - Part Four

I am here to continue the discussion around AngularJS. Today, we will go through one of the main feature 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 and 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 example 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 populate the template and assign 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 line of code and also, we have simplified model and view in AngularJS that allows developers to quickly create a powerful javascript applications. Directives are created using 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 camel case like link-Menu in html template file. The AngularJS HTML compiler then normalizes the directive name into its camel case equivalent, i.e. linkMenu. Also, remember that all directives name used in templates must be unique. Directives names cannot match with any existing HTML tag name or with the AngularJS inbuilt directives name.

The following attributes can be used during creation of a 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”, restrict option has “E” (only match element name), “C” (only match class name) and “M” (only match the comment name) or any combination among four options.

  2. TemplateUrl

    The templateUrl attribute tells the AngularJS HTML compiler to replace 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 - 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 which 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 creates 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:
  1. compile: function compile(tElement, tAttrs, transclude) {  
  2. return  {  
  3. pre: function preLink(scope, iElement, iAttrs, controller) { ... },  
  4. post: function postLink(scope, iElement, iAttrs, controller) { ... }  
  5. }  
  6. }  

And for link, it is:

  1. link: function postLink(scope, iElement, iAttrs) { ... }  

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 isolate 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 below syntax,

Scope TypeSyntax
existing scopescope: false (this is the default if unspecified)
new scopescope: true
isolate scopescope: { /* 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.

SymbolMeaning
@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 program on basis of directive. Before creating the a specific program, first we need to create the angular modular app.
  1. var testApp = angular.module('TestApp', []);  
First Program
 
Html file
  1. <!DOCTYPE html>  
  2. <html ng-app="TestApp">  
  3. <head>  
  4.     <title>AngularJS Direcrive</title>  
  5.     <script src="angular.js"></script>  
  6.     <link href="../../RefStyle/bootstrap.min.css" rel="stylesheet" />  
  7.     <script src="app.js"></script>  
  8.     <script src="Directive1.js"></script>  
  9. </head>  
  10. <body ng-controller="FactoryController">  
  11.     <div class="panel panel-default">  
  12.         <div class="panel-heading">  
  13.             <h3>Products</h3>  
  14.         </div>  
  15.         <div class="panel-body">  
  16.             <div unordered-list="products" list-property="name "></div>  
  17.             <div unordered-list="products" list-property="price | currency"></div>  
  18.         </div>  
  19.     </div>  
  20. </body>  
  21. </html>  
Controller (code)
  1. testApp.controller('FactoryController'function ($scope) {  
  2.     $scope.products = [{ name: "Apples", category: "Fruit", price: 1.20, expiry: 10 },  
  3.                         { name: "Bananas", category: "Fruit", price: 2.42, expiry: 7 },  
  4.                         { name: "Pears", category: "Fruit", price: 2.02, expiry: 6 }  
  5.     ];  
  6.   
  7.     $scope.incrementPrices = function () {  
  8.         for (var i = 0; i < $scope.products.length; i++) {  
  9.             $scope.products[i].price++;  
  10.         }  
  11.     }  
  12. });  
Directive
  1. testApp.directive("unorderedList"function () {  
  2.     return function (scope, element, attrs) {  
  3.         var data = scope[attrs["unorderedList"]];  
  4.         var propertyName = attrs["listProperty"];  
  5.         var propertyExpression = attrs["listProperty"];  
  6.         if (angular.isArray(data)) {  
  7.             var listElem = angular.element("<ul>");  
  8.             element.append(listElem);  
  9.             for (var i = 0; i < data.length; i++) {  
  10.                 listElem.append(angular.element('<li>').text(scope.$eval(propertyExpression, data[i])));  
  11.             }  
  12.         }  
  13.     }  
  14. });  
The output of the program is as below,

 
Second Program
 
Html file
  1. <html ng-app="TestApp">  
  2. <head>  
  3. <title>AngularJS Direcrive</title>  
  4. <script src="angular.js"></script>  
  5. <link href="../../RefStyle/bootstrap.min.css" rel="stylesheet" />  
  6. <script src="app.js"></script>  
  7. <script src="Directive2.js"></script>  
  8. </head>  
  9. <body ng-controller="EmployeeCtrl">  
  10. <div class="panel panel-default">  
  11. <div>  
  12. <h1>Employee data:</h1>  
  13. <div my-employee></div>  
  14. </div>  
  15. </div>  
  16. </body>  
  17. </html>  
Controller and Directive code
  1. testApp.controller('EmployeeCtrl', ['$scope', function($scope)  
  2. {  
  3.     var Employee = function(name, age)   
  4.     {  
  5.         this.name = name;  
  6.         this.age = age;  
  7.     };  
  8.     var GetEmployees = function()   
  9.     {  
  10.         return [  
  11.             new Employee("First employee", 56),  
  12.             new Employee("Second employee", 44),  
  13.             new Employee("Last employee", 32)  
  14.         ];  
  15.     };  
  16.     $scope.employeeData =   
  17.       {  
  18.         employees: GetEmployees()  
  19.     };  
  20. }]);  
  21. testApp.directive("myEmployee", function()  
  22.  {  
  23.     return {  
  24.         template: 'Name - {{employeeData.employees[0].name}}, Age - {{employeeData.employees[0].age}}'  
  25.     };  
  26. });  
The output of the above code is,
output