Learning Custom Directives In AngularJS - A Practical Approach

Introduction

In my previous article of AngularJS, I tried to explain the basics of AngularJS to get you started. In this article, I will explain the core concept of AngularJSl  i.e., directives. First, we will see what directives are and then I will try to explain how to build the custom directives, using some examples, which I have prepared. Moreover, in this article, I will be focusing more on the practical examples and the code rather than confusing you by writing tough definitions and heavy terms.

Directives

Directives in AngularJS are the attributes of HTML elements, where AngularJS provides us the ability  to augment the capabilities of HTML elements. AngularJS provides many built-in directives e.g. ng-app, ng-controller, ng-repeat, ng-model, ng-bind and so on. Thus, by using ng-app on HTML tag, we mark the beginning of an AngularJS app. AngularJS also gives us the feature of writing our own custom directives to extend the capability of HTML elements, as per our requirement. We can extend the abilities of HTML's template to do anything, we can imagine.

Why Use Directives?

Directives help us to make the reusable components to be used throughout an Angular Application.

Directive Syntax

Let’s see a basic pseudo-code to define a directive.

  1. angularModuleName.directive('directiveName'function() {  
  2.     return {  
  3.         restrict: String,  
  4.         priority: Number,  
  5.         terminal: Boolean,  
  6.         template: String or Template Function,  
  7.         templateUrl: String,  
  8.         replace: Boolean or String,  
  9.         scope: Boolean or Object,  
  10.         transclude: Boolean,  
  11.         controller: String or  
  12.             function(scope, element, attrs, transclude, otherInjectables) { ... },  
  13.         controllerAs: String,  
  14.         require: String,  
  15.         link: function(scope, iElement, iAttrs) { ... },  
  16.         compile: return an Object OR  
  17.             function(tElement, tAttrs, transclude) {  
  18.                 return {  
  19.                     pre: function(scope, iElement, iAttrs, controller) { ... },  
  20.                     post: function(scope, iElement, iAttrs, controller) { ... }  
  21.                 }  
  22.                 // or  
  23.                 return function postLink(...) { ... }  
  24.             }  
  25.       };  
  26. });  
Basically, a directive returns a definition object with a bunch of key-value pairs. These key-value pairs decide the behavior of a directive. Well, the syntax may seem dangerous to you but it is not, because most keys are optional and some are mutually exclusive. Let’s try to understand the meaning of these key-value pairs in real life. 
  • Restrict
    It is used to specify how a directive is going to be used in DOM. The directive can be used as an attribute (A), element (E), class (C) or comment (M). These options can be used alone or in combinations. It is optional which means if you do not specify the option, the default value will be the attribute.

    Example

    As an attribute <div custom-directive/>
    As an element <custom-directive></custom-directive>
    As a class <div class="custom-directive"/>
    As a comment <!--custom-directive-->

  • Priority
    It specifies the priority of invocation by AngularJS. Directive with high priority will be invoked faster than the other directives with lower priority. Default value is 0.

  • Terminal
    Well, I never used this option but the documentation says it is used to tell Angular to stop invoking any further directives on an element, which has a higher priority than this directive. It is optional.

  • Template
    It specifies inline HTML template for the directive.

  • TemplateURL
    You can also load the template of your directive, using a URL of the file containing code for the template. If the inline template is used, this option is not used.

  • Replace
    It can be set either true or false. The template of the directive gets appended to the parent element in which the directive is used only if this option is set to false. It can replace the parent element, if set to true.

  • Scope
    It specifies the scope of the directive. It is a very important option, which should be set very carefully according to the requirement of the directive, you are building. There are three ways in which the scope of a directive can be set.

    1. Existing scope from directive's DOM element. If the scope is set to false, which is the default value? The scope of the directive will be the same as the scope of the DOM element on which the directive is used.

    2. New scope inherits from the enclosing Controller's scope. If the scope is set to true, a new scope is created for the directive, which will inherit all the properties from the parent DOM element scope or we can say, it will inherit from the Controller's scope of the DOM enclosing directive.

    3.  An isolated scope will inherit nothing from its parent. This type of scope is generally used in making reusable components.

      For now, the statements, mentioned above and points may not make much sense to you unless you see all the options in action. Don't worry. I will try to clear all the points, using examples in the upcoming sections.

  • Transclude
    As I told earlier, directive can replace or append its content/template to the parent DOM element. Using Transclude option, you can also move the original content within the template of the directive, if Transclude is set to true.

  • Controller
    You can specify the Controller name or Controller function, which is to be used for the directive, using the option, mentioned.

  • ControllerAs
    The alternate name can be given to the Controller, using the mentioned option.

  • Require
    It specifies the name of the other directives to be used. If your directive depends upon another directive, you can specify the name of the same, using the mentioned option.

  • Link
    It is used to define a function, which can be used to programmatically modify template DOM element instances, such as adding the event listeners and setting up data binding etc.

  • Compile
    It can be used to define a function, which can be used to programmatically modify the DOM template for the features across the copies of a directive. It can also return a link function to modify the resulting element instances.

Example 1 Creating First Directive

Let’s write our very first directive, using the syntax, mentioned above.

  1. var ngCustomDirectivesApp = angular.module('ngCustomDirectivesApp')  
  2. ngCustomDirectivesApp.directive('customLabel'function () {  
  3.     return {  
  4.   
  5.         restrict: 'EA',  
  6.         template: '<div class="jumbotron"><h2>My First Directive</h2><p>This is a simple example.</p></div>'  
  7.     }  
  8. })  
Explanation

Like any other Angular app, first I created an Angular module. Subsequently, I defined a directive function, which simply returns an object with two attributes, namely ‘restrict' and ‘template'.

As you can see, for restricting the property, I used EA, which means this directive can be used both as an element or an attribute in DOM. Template property contains a string, where I have defined the structure of the directive. This directive will render a div, which contains a heading and a paragraph. The point to be noticed here is the naming convention for the directive. As you can see, I have followed the camel-case naming convention because when Angular parses this directive, it will split the name with a hyphen. Hence, customLabel will become custom-label and on our HTML template, we will have to use the custom-label name. You may have noticed that all the pre-defined directives of AngularJS comes with a ng- prefix. In DOM, the directive will be used, as mentioned below.

<custom-label></custom-label>
OR
<div custom-label></div>


Output

Output
Note 

I have used jumbotron class of bootstrap for div, as I have included bootstrap for the Application.

Example 2 Using Link Function

In this example, I will create a simple ‘like button' directive, which will use its link function to programmatically bind a click event on the directive instance element to perform some action.

Directive definition
  1. ngCustomDirectivesApp.directive('likeButton'function () {  
  2.     return {  
  3.         restrict: 'EA',  
  4.         templateUrl: '../partials/likebutton.html',  
  5.         replace: true,  
  6.         link: function (scope, element, attrs) {  
  7.             element.bind('click'function () {  
  8.                 element.toggleClass('like');  
  9.             })  
  10.         }  
  11.     }  
  12. })  
Template
  1. <button type="button" class="btn-sty">  
  2.     <span class="glyphicon glyphicon-thumbs-up"></span>  
  3. </button>  
CSS
  1. .like {  
  2. background-colorblue;  
  3. colorwhite;  
  4. }  
  5. .btn-sty {  
  6. border-radius: 12px;  
  7. }  
Output

Output
Output

Explanation

Just like the previous example, the definition of the directive is same but in this directive, instead of writing an inline template, I have written the template for the directive in a separate file. Let’s try to understand the link function. You can see, the link function has three parameters, where the first one is scope through which we can access the scope of the directive instance element. The second is an element through which we can access the element of the directive. This means in this example, I have accessed the button as an element. The third and the last one is an attribute of the directive element. Now, let's go back to our code. In link function, I have grabbed the element and have to bind the click event in which I am just toggling the class.

Example 3 Understanding Scope

In this example, I will try to explain the scope of a directive. All the directives have a scope associated with them to access the methods and data inside the template and link function that I talked about in the last example. Directives don’t create their own scope unless specified explicitly, and use their parent’s scope as their own. Like I told earlier, the values of the scope property decide how the scope will be created and used inside a directive. There are three different values that can be set for the scope property of directive. These values can either be true,false, or {}.

Scope - false

When the scope is set to false, the directive will use its parent scope as its own scope, which means it can access and modify all the data/variables of parent scope. If parent modifies its data in its scope, the changes will be reflected in the directive scope too. The same will happen, if the directive tries to modify the data of parent scope. Since both the parent and the directive, access the same scope, both can see changes of each other.

Directive definition
  1. ngCustomDirectivesApp.controller('dashboardController'function ($scope) {  
  2.    
  3.     $scope.dataFromParent = "Data from parent";  
  4.   
  5. })  
  6. ngCustomDirectivesApp.directive('parentScope'function () {  
  7.     return {  
  8.         restrict: 'E',  
  9.         scope: false,  
  10.         template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>'  
  11.     }  
  12. })   
  13. <div ng-controller="dashboardController">  
  14.     <input type="text" ng-model="dataFromParent" style="border:1px solid green" />  
  15.     <parent-scope></parent-scope>  
  16. </div>  
Output

Output

Explanation

First, I created a Controller and defined a scope variable dataFromParent.

Next, I created a directive and set its scope to false.

In template, I simply created an input box which is bound to dataFromParent through ng-model.

Then, I created a parent div whose Controller is the same Controller that I defined in the first step.

In this div, I created an input box which is also bound to dataFromParent.

Then, I used the directive in this same div so, Controller scope will act as the parent scope for the directive.

Now, if you change the value of any of the input boxes, the changes will be reflected on the other one because both of the input boxes access the same dataFromParent from the same Controller.

In short, when the scope is set to false, the Controller and the directive are using the same scope object. Hence, any changes to the controller and directive will be in sync.

Scope - true

When the scope is set to true, a new scope is created and assigned to the directive. Then, the scope object is prototypically inherited from its parent's scope. So, in this case, any change made to this new scope object will not be reflected back to the parent scope object. However, because the new scope is inherited from the parent scope, any change made in the parent scope will be reflected in the directive scope.
  1. ngCustomDirectivesApp.controller('dashboardController'function ($scope) {  
  2.    
  3.     $scope.dataFromParent = "Data from parent";  
  4.   
  5. })  
  6. ngCustomDirectivesApp.directive('inheritedScope'function () {  
  7.     return {  
  8.         restrict: 'E',  
  9.         scope: true,  
  10.         template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>'  
  11.     }  
  12. })  
  13. <div ng-controller="dashboardController">  
  14.   
  15.     <input type="text" ng-model="dataFromParent" style="border:1px solid green" />  
  16.     <parent-scope></parent-scope>  
  17.     <inherited-scope></inherited-scope>  
  18. </div>  
Output

Output

Explanation

Just like the previous directive, I have defined a new directive. But in this directive, I have set the scope to true which means in this case, directive will no longer access the parent scope object (Controller scope) instead it will create a new scope object for itself. But, it will be inherited from the parent scope. So, when any change is made in first text box, all other text boxes will also be updated. But, if any change is made in a third text box which is our directive, then changes will not be reflected in first two text boxes. First, two text boxes access data directly from the Controller but third text box access data in its new scope due to prototypal inheritance.

Scope - { }

When the scope is set to Object literal {}, a new scope object is created for the directive but this time, it will not inherit from the parent scope object. It will be completely detached from its parent scope. This scope is also known as Isolated Scope. The advantage of creating such type of directive is that they are generic and can be placed anywhere inside the Application without polluting the parent scope.
  1. ngCustomDirectivesApp.controller('dashboardController'function ($scope) {  
  2.    
  3.     $scope.dataFromParent = "Data from parent";  
  4.   
  5. })  
  6. ngCustomDirectivesApp.directive('isolatedScope'function () {  
  7.     return {  
  8.         restrict: 'E',  
  9.         scope: {},  
  10.         template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>'  
  11.     }  
  12. })  
  13. <div ng-controller="dashboardController">  
  14.     <input type="text" ng-model="dataFromParent" style="border:1px solid green" />  
  15.     <parent-scope></parent-scope>  
  16.     <inherited-scope></inherited-scope>  
  17.     <isolated-scope></isolated-scope>  
  18. </div>  
Output

Output
Explanation

In this case, a new scope is created, which has no access to its parent scope object and hence, the data will not be bound.

Example 4 Understanding Isolated Scope

As I have told you, if you set the scope of a directive to Object literal {}, an isolated scope is created for the directive which means directive has no access to the data/variables or methods of the parent scope object. This can be very useful, if you are creating a re-usable component, but in most of the cases, we need some kind of communication between directive and parent scope and also want the directive to not pollute the scope of the parent. Hence, an isolated scope provides some filters to communicate or exchange the data between parent scope object and directive scope object. In order to pass some data from the parent scope to directive scope, we need to define some properties to the Object lateral that we set to the scope property. Let's see the syntax first. Subsequently, I will explain them.
  1. scope: {  
  2. varibaleName1:'@attrName1',  
  3. varibaleName2:'=attrName2',  
  4. varibaleName3:'&attrName3'  
  5. }  
Or,
  1. scope: {  
  2. attrName1:'@',  
  3. attrName2:'=',  
  4. attrName3:'&'  
  5. }  
There are three options available to communicate the data from parent to the directive in an isolated scope.
  • Text binding or one-way binding or read-only access. It is one way binding between directive and parent scope. It expects mapping the attribute to be an expression( {{ }} ) or a string. Since it provides one-way binding, so the changes made in parent scope will be reflected in directive scope but any change made in directive scope will not be reflected back in the parent scope. 

  • = Model binding or two-way binding. It is two-way binding between parent scope and directive. It expects the attribute value to be a model name. Changes between the parent scope and directive scope are synchronized.

  • & Method binding or behavior binding. It is used to bind any methods from the parent scope to the directive scope, so it gives us the advantage of executing any callbacks in the parent scope. 
Example

Let's create a simple directive to understand the usage of all the scope options. First, create a controller, which will act as a parent for the directive. In controller, define a scope variable named as dataFromParent and a function named as changeValue to modify the variable.
  1. ngCustomDirectivesApp.controller('dashboardController'function ($scope) {  
  2.     $scope.dataFromParent = "Data from parent";  
  3.     $scope.changeValue = function () {  
  4.         $scope.dataFromParent = "Changed data from parent";  
  5.     }  
  6.   
  7. })  
Now, let’s create our directive.
  1. ngCustomDirectivesApp.directive('isolatedScopeWithFilters'function () {  
  2.     return {  
  3.         restrict: 'E',  
  4.         scope: {  
  5.             oneWayBindData: '@oneWayAttr',  
  6.             twoWayBindData: '=twoWayAttr',  
  7.             methodBind:'&parentMethodName'  
  8.         },  
  9.         template: '<input type="text" ng-model="oneWayBindData" style="border:1px solid red;"/><br/><input type="text" ng-model="twoWayBindData" style="border:1px solid red;"/><br/><button type="button" ng-click="methodBind()">Change Value</button>'  
  10.     }  
  11. })  
You can see in the scope, that I have added three properties. These properties will be used in our directive to bind the data. Directive is very simple, we create two text boxes and one button. First text box is bound with oneWayData property of scope, second text box is bound with twoWayData of scope and button’s ng-click event is bound with methodBind property of scope. See carefully the prefixes used in the scope properties.

Let's use this directive in a div, set its controller to the controller we defined in the first step. Now, add the directive. The directive element will have three attributes named one-way-attr, two-way-attr and parent-method-name. These attributes are same, as we defined in our directive definition with on exception, which we used, using hyphen instead of camel-case as per Angular syntax. Also, add a paragraph tag and map its value, using the expression with dataFromParent, so that we see the real time value of the dataFromParent model.
  1. <div ng-controller="dashboardController">  
  2.      
  3.     <isolated-scope-with-filters one-way-attr="{{ dataFromParent}}" two-way-attr="dataFromParent" parent-method-name="changeValue()"></isolated-scope-with-filters>  
  4.    <p>{{dataFromParent}}</p>  
  5. </div>  
  6. one-way-  
one-way-attr is mapped with the expression, which will evaluate the value of dataFromParent model from the parent scope, which is our controller, two-way-attr is directly mapped with dataFromParent model and parent-method-attr is mapped with the function of the controller to change the value of the model.

Run the code and see your directive is working or not.

Example 5 Working With Controller

Let's create an example to understand how a controller can be used for the communication between the different directives. In this example, we will create an accordion to the directive.

Step 1

Create parent accordion directive, which will hold the children accordion elements.
  1. ngCustomDirectivesApp.directive('accordion'function () {  
  2.     return {  
  3.         restrict: 'EA',  
  4.         template: '<div ng-transclude></div>',  
  5.         replace: true,  
  6.         transclude: true,  
  7.         controllerAs: 'accordionController',  
  8.         controller: function () {  
  9.             var children = [];  
  10.             this.OpenMeHideAll = function (selectedchild) {  
  11.                 angular.forEach(children, function (child) {  
  12.                     if (selectedchild != child) {  
  13.                         child.show = false;  
  14.                     }  
  15.                 });  
  16.             };  
  17.   
  18.             this.addChild = function (child) {  
  19.                 children.push(child);  
  20.             }  
  21.         }  
  22.     }  
  23. })  
In the template, we have defined a plain div but the important point to notice is that we have used ng-transclude, ng-transclude, which will make div able to hold children elements inside in it. Transclude option is set to true for the same reason to allow div to hold children elements. Then a controller is defined, this is the focus area of this directive. In the controller, define a function to push child elements in an array, then define a function to open selected child and hide all other children.

Step 2

Create accordion child element directive.
  1. ngCustomDirectivesApp.directive('accordionChild'function () {  
  2.     return {  
  3.         restrict: 'EA',  
  4.         template: '<div><div class="heading" ng-click="toggle()">{{title}}</div><div class="content" ng-show="show" ng-transclude></div></div>',  
  5.         replace: true,  
  6.         transclude: true,  
  7.         require: '^accordion',  
  8.         scope: {  
  9.             title:'@'  
  10.         },  
  11.         link: function (scope,element,attrs,accordionController) {  
  12.             scope.show = false;  
  13.             accordionController.addChild(scope);  
  14.             scope.toggle = function () {  
  15.                 scope.show = !scope.show;  
  16.                 accordionController.OpenMeHideAll(scope);  
  17.             }  
  18.         }  
  19.     }  
  20. })  
The accordion element will have a heading and a body to hold the data or other elements, so in the template, we will create a head div and attach a click event to toggle. Subsequently, we have to create a body div to hold the content, to be able to hold the dynamic content. We have to use ng-transclude in this div. Require attribute is used to specify that accordion directive is required for the directive. ng-show is used to hide and show the content on click event of head div. Isolated scope is created to make it a re-usable component and title attribute is used for on-way data binding. In link function, the scope is used to access show model, which will be used to show and hide the content and controller of the accordion directive, which is passed to access methods of it.

When the user clicks on the heading of the accordion element, the reference of that element is passed to the function of the accordion controller to show that particular element and hide all the other elements.

Step 3

Use directive in the view.
  1. <accordion>  
  2.     <accordion-child title="Element 1">Data 1</accordion-child>  
  3.     <accordion-child title="Element 2">Data 2</accordion-child>  
  4.     <accordion-child title="Element 3">Data 3</accordion-child>  
  5. </accordion>  
Output

Output
Example 6 Working With Controller

Let’s create another example of using controller in a directive. In this example, we will create a sort-able list. The elements of the list can be dragged to sort the items in a list according to our need.

Step 1

Define a directive to create a list on the basis of array of items passed to it.
  1. ngCustomDirectivesApp.directive('smartList'function () {  
  2.     return {  
  3.         restrict: 'EA',  
  4.         templateUrl: '../partials/listdirective.html',  
  5.         replace: true,  
  6.         scope: {  
  7.             items: '='  
  8.         },  
  9.         controller: function ($scope) {  
  10.             $scope.source = null;  
  11.         },  
  12.         controllerAs:'listCTRL'  
  13.     }  
  14. })  
Template
  1. <ul class="ul-sty">  
  2.     <li ng-repeat="item in items" class="li-sty" draggable>  
  3.         {{item }}  
  4.     </li>  
  5. </ul>  
The directive is very simple. It contains an unordered list, whose list items are generated by ng-repeat. We have also added an attribute draggable, which will make the list items draggable. We will define draggable directive in the upcoming steps. In scope attribute of the directive, we have used the items for two-way data binding, which means we will be able to access the model of the parent scope in the directive scope. We have also defined a controller, which holds a variable named as source.

Step 2

Creating draggable directive.
  1. ngCustomDirectivesApp.directive('draggable'function () {  
  2.     return {  
  3.         require:'^smartList',  
  4.         link: function (scope, element, attr, listCTRL) {  
  5.             element.css({  
  6.                 cursor: 'move',  
  7.             });  
  8.             attr.$set('draggable'true);  
  9.   
  10.             function isBefore(x, y) {  
  11.                 if (x.parentNode == y.parentNode) {  
  12.                     for (var i = x; i; i = i.previousSibling)  
  13.                     {  
  14.                         if (i == y)  
  15.                             return true;  
  16.                     }  
  17.                 }  
  18.                 return false;  
  19.             }  
  20.             element.on('dragenter'function (e) {  
  21.                 if (e.target.parentNode != null) {  
  22.                     if (isBefore(listCTRL.source, e.target)) {  
  23.                         e.target.parentNode.insertBefore(listCTRL.source, e.target)  
  24.                     }  
  25.                     else {  
  26.                         e.target.parentNode.insertBefore(listCTRL.source, e.target.nextSibling)  
  27.                     }  
  28.                 }  
  29.             })  
  30.             element.on('dragstart'function (e) {  
  31.                 listCTRL.source = element[0];  
  32.                 e.dataTransfer.effectAllowed = 'move';  
  33.             })  
  34.         }  
  35.     }  
  36. })  
This draggable directive is used in the template, which we have defined in the step 1. In link function of this directive, we have passed controller of directive defined in step 1. It sets the attribute of the element to draggable, one function is defined to compare parent node of the elements passed. Event drag start on drag enter is attached to the element to handle drag and drop. We store the element, which is dragged on the controller variable to compare with the element on which the current element is dropped. Other than this, rest of the code is very simple. We are just inserting the node in the appropriate place.

Step 3

Define the parent controller.
  1. ngCustomDirectivesApp.controller('dashboardController'function ($scope) {  
  2.    
  3.     $scope.itemsdata = ['Apple''Mango''Banana''PineApple''Grapes''Oranges'];  
  4.   
  5. })  
Step 4

Use directive, as shown below.
  1. <div ng-controller="dashboardController">  
  2.   
  3.     <smart-list items="itemsdata" />  
  4.   
  5. </div>  
In our directive, itemsdata from the controller is passed on the scope of the directive.

Output

Output

You can drag the items to sort the items, as per your need.

Conclusion

I tried to cover all the aspects of directive, using examples. Directives can be used in various scenarios of the project. You can learn more about the directives in AngularJS official documentation. If you want the master directives, the best way to achieve this is to start creating the directives in our project. I have attached the complete code with the article. You can also download or clone it from GitHub. I hope, it will help you to understand the directives in AngularJS.