Learning Custom Directives In AngularJS – A Practical Approach


AngularJS Introduction

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 attributes of HTML elements that AngularJS provides us 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. So 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 an HTML element according to our needs. We can extend the abilities of HTML’s template to do anything that we can imagine.

Why use Directives

Directives help us to make reusable components that can be used throughout an angular application.

Directive syntax

Let’s see a basic pseudo-code for defining a directive.

angularModuleName.directive('directiveName', function() {
    return {
        restrict: String,
        priority: Number,
        terminal: Boolean,
        template: String or Template Function,
        templateUrl: String,
        replace: Boolean or String,
        scope: Boolean or Object,
        transclude: Boolean,
        controller: String or
            function(scope, element, attrs, transclude, otherInjectables) { ... },
        controllerAs: String,
        require: String,
        link: function(scope, iElement, iAttrs) { ... },
        compile: return an Object OR
            function(tElement, tAttrs, transclude) {
                return {
                    pre: function(scope, iElement, iAttrs, controller) { ... },
                    post: function(scope, iElement, iAttrs, controller) { ... }
                }
                // or
                return function postLink(...) { ... }
            }
      };
});

Basically a directive returns a definition object with a bunch of key and value pairs. These key-value pairs decide the behavior of a directive. Well 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: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. Optional means if you do not specify the option then the default value is attribute.

    Example:

    As attribute:

    As element:

    As class:

    As comment:

  • Priority:It specifies the priority of invocation by AngularJS. Directive with high priority will be invoked first than other directive with lower. 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 that have 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 then this option is not used.
  • Replace:It can be set either true or false. Template of the directive gets appended to the parent element in which directive is used. If this option is set false or it can replace the parent element if set to true.
  • Scope:It specifies the scope of the directive. It is a very important option that should be set very carefully according to the requirement of the directive you are building. There are three ways in 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 then the scope of the directive will be the same as the scope of the DOM element on which directive is used.
    2. New scope that inherits from the enclosing controller’s scope. If the scope is set to true then a new scope is created for the directive that 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 isolate scope that will inherit nothing from its parent. This type of scope is generally used in making reusable components.

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

  • Transclude:As I explained earlier directive can replace or append its content/template to the parent DOM element but using Transclude option you can also move the original content within the template of the directive is Transclude is set to true.
  • Controller:You can specify the controller name or controller function to be used for the directive using this option.
  • ControllerAs:The alternate name can be given to the controller using option.
  • Require:It specifies the name of the other directive to be used. If your directive depends on upon another directive then you can specify the name of the same using this option.
  • Link:It is used to define a function that can be used to programmatically modify template DOM element instances such as adding event listeners, setting up data binding, etc.
  • Compile:It can be used to define a function that can be used to programmatically modify the DOM template for features across copies of a directive. It can also return link function to modify the resulting element instances.

Example 1: Creating first directive

Let’s write our very first directive using the above syntax:

var ngCustomDirectivesApp = angular.module('ngCustomDirectivesApp')
ngCustomDirectivesApp.directive('customLabel', function () {
    return {

        restrict: 'EA',
        template: '

My First Directive

This is a simple example.

'
} })

Explanation:

Like any other Angular app, first I created an angular module then I defined a directive function which simply returns an object with two attributes: ‘restrict‘ and ‘template‘. As you can see, for the restrict 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 containing 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 parse this directive it will split the name with a hyphen. So 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 come with a ng- prefix. In DOM directive will be used as:

<custom-label></custom-label>

OR

<div custom-label></div>

Output:

Note: I have used jumbotron class of bootstrap for the 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:

ngCustomDirectivesApp.directive('likeButton', function () {
    return {
        restrict: 'EA',
        templateUrl: '../partials/likebutton.html',
        replace: true,
        link: function (scope, element, attrs) {
            element.bind('click', function () {
                element.toggleClass('like');
            })
        }
    }
})

Template:

<button type="button" class="btn-sty">
    <span class="glyphicon glyphicon-thumbs-up"></span>
</button>

CSS:

.like {
    background-color: blue;
    color: white;
}
.btn-sty {
    border-radius: 12px;
}

Output:

Explanation:

Just like the previous example, the definition of this directive is same except that in this directive instead of writing inline template I have written the template for the directive in a separate file. Let’s try to understand the link function. As you can see, the link function has three parameter, first is scope through which we can access the scope of the directive instance element, second is element through which we can access the element of the directive means in this example I have accessed the button as an element, third is attr which is the attributes of the directive element. Now back to our code, in the 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 directives have a scope associated with them for accessing 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 explained earlier, the values of the scope property decides how the scope will be created and used inside a directive. There are three different values that are set for the scope property of directive. These values can either be true, false or {}.

Scope: false

When the scope is set to false, 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 then changes will be reflected in the directive scope. Also, the same will happen if directive will try to modify the data of parent scope since both parent and directive access the same scope both can see changes of each other.

Directive definition:

ngCustomDirectivesApp.controller('dashboardController', function ($scope) {
 
    $scope.dataFromParent = "Data from parent";

})
ngCustomDirectivesApp.directive('parentScope', function () {
    return {
        restrict: 'E',
        scope: false,
        template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>'
    }
}) 
"dashboardController"> "text" ng-model="dataFromParent" style="border:1px solid green" /> parent-scope> div>

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 the 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 box, changes will be reflected on the other one because both of the input boxes accesses the same dataFromParent from the same controller. So in short, when the scope is set to false, the controller and directive are using the same scope object. Therefore 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 and 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.

ngCustomDirectivesApp.controller('dashboardController', function ($scope) {
 
    $scope.dataFromParent = "Data from parent";

})
ngCustomDirectivesApp.directive('inheritedScope', function () {
    return {
        restrict: 'E',
        scope: true,
        template: ''
    }
})
"dashboardController"> "text" ng-model="dataFromParent" style="border:1px solid green" /> parent-scope> inherited-scope> div>

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 the 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 {}, then a new scope object is created for the directive. But this time, it will not inherit from the parent scope object, it will be completed 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.

ngCustomDirectivesApp.controller('dashboardController', function ($scope) {
 
    $scope.dataFromParent = "Data from parent";

})
ngCustomDirectivesApp.directive('isolatedScope', function () {
    return {
        restrict: 'E',
        scope: {},
        template: ''
    }
})
"dashboardController"> "text" ng-model="dataFromParent" style="border:1px solid green" /> parent-scope> inherited-scope> isolated-scope> div>

Output:

Explanation:

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

Example 4: Understanding Isolated Scope

As I have previously stated, if you set the scope of a directive to Object literal {}, then 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 could 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 that directive should not pollute the scope of the parent. So isolated scope provides some filters to communicate or exchange data between parent scope object and directive scope object. To pass some data from parent scope to directive scope we need to some properties to the Object literal that we set to scope property. Let’s see the syntax first then I will explain them.

scope: {
    varibaleName1:'@attrName1',
    varibaleName2:'=attrName2',
    varibaleName3:'&attrName3'
}

OR,

scope: {
    attrName1:'@',
    attrName2:'=',
    attrName3:'&'
}

There are three options available to communicate data from parent to the directive in isolated scope.

@: Text binding or one-way binding or read-only access. It is one way binding between directive and parent scope, it expects mapped the attribute to be an expression( {{ }} ) or string. Since it provides one-way binding so 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 parent scope and directive scope are synced.

&: Method binding or behavior binding. It is used to bind any methods from 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 scope options. First, create a controller that will act as a parent for the directive. In controller define a scope variable named dataFromParent and a function named changeValue to modify the variable.

ngCustomDirectivesApp.controller('dashboardController', function ($scope) {
    $scope.dataFromParent = "Data from parent";
    $scope.changeValue = function () {
        $scope.dataFromParent = "Changed data from parent";
    }

})

Now let’s create our directive.

ngCustomDirectivesApp.directive('isolatedScopeWithFilters', function () {
    return {
        restrict: 'E',
        scope: {
            oneWayBindData: '@oneWayAttr',
            twoWayBindData: '=twoWayAttr',
            methodBind:'&parentMethodName'
        },
        template: '

Change Value'
} })

As you can see in scope I have added three properties. These properties will be used in our directive to bind data. Directive is very simple, we create two text boxes and one button. First text box is bound with oneWayDataproperty 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 our directive here, 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 that we used their 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.

div ng-controller="dashboardController">
   
    isolated-scope-with-filters one-way-attr="{{ dataFromParent}}" two-way-attr="dataFromParent" parent-method-name="changeValue()">/isolated-scope-with-filters>
   p>{{dataFromParent}}/p>
/div>

one-way-attr is mapped with expression which will evaluate the value of dataFromParent model from parent scope that is our controller, two-way-attr is directly mapped with dataFromParent model and parent-method-attr is mapped with the function of controller to change the value of model.

Run the code and see for yourself that directive is working.

Example 5: Working with Controller

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

Step1: Create parent accordion directive that will hold children accordion elements.

ngCustomDirectivesApp.directive('accordion', function () {
    return {
        restrict: 'EA',
        template: '
'
, replace: true, transclude: true, controllerAs: 'accordionController', controller: function () { var children = []; this.OpenMeHideAll = function (selectedchild) { angular.forEach(children, function (child) { if (selectedchild != child) { child.show = false; } }); }; this.addChild = function (child) { children.push(child); } } } })

In template, we have defined a plain div but the important point to notice is that we have used ng-transclude, ng-transclude 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.

ngCustomDirectivesApp.directive('accordionChild', function () {
    return {
        restrict: 'EA',
        template: '
{{title}}
'
, replace: true, transclude: true, require: '^accordion', scope: { title:'@' }, link: function (scope,element,attrs,accordionController) { scope.show = false; accordionController.addChild(scope); scope.toggle = function () { scope.show = !scope.show; accordionController.OpenMeHideAll(scope); } } } })

The accordion element will have a heading and a body to hold data or other elements so in template we will create a head div and attach a click event to toggle, then we have to create a body div to hold content, to be able to hold dynamic content we have to use ng-transclude in this div. Require attribute is used to specify that accordion directive is required for this directive. ng-show is used to hide and show 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 content and controller of the accordion directive is passed to access methods of it.

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

Step 3: Use directive in the view.

<accordion>
    <accordion-child title="Element 1">Data 1</accordion-child>
    <accordion-child title="Element 2">Data 2</accordion-child>
    <accordion-child title="Element 3">Data 3</accordion-child>
</accordion>

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. Elements of list can be dragged to sort items in list according our need.

Step 1: Define a directive to create list on the basis of array of items passed to it.

ngCustomDirectivesApp.directive('smartList', function () {
    return {
        restrict: 'EA',
        templateUrl: '../partials/listdirective.html',
        replace: true,
        scope: {
            items: '='
        },
        controller: function ($scope) {
            $scope.source = null;
        },
        controllerAs:'listCTRL'
    }
})

Template:

<ul class="ul-sty">
    <li ng-repeat="item in items" class="li-sty" draggable>
        {{item }}
    </li>
</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 upcoming steps. In scope attribute of directive, we have used items for two-way data binding means we will be able to access model of parent scope in directive scope. We have also defined a controller which holds a variable named source.

Step 2: Creating draggable directive.

ngCustomDirectivesApp.directive('draggable', function () {
    return {
        require:'^smartList',
        link: function (scope, element, attr, listCTRL) {
            element.css({
                cursor: 'move',
            });
            attr.$set('draggable', true);

            function isBefore(x, y) {
                if (x.parentNode == y.parentNode) {
                    for (var i = x; i; i = i.previousSibling)
                    {
                        if (i == y)
                            return true;
                    }
                }
                return false;
            }
            element.on('dragenter', function (e) {
                if (e.target.parentNode != null) {
                    if (isBefore(listCTRL.source, e.target)) {
                        e.target.parentNode.insertBefore(listCTRL.source, e.target)
                    }
                    else {
                        e.target.parentNode.insertBefore(listCTRL.source, e.target.nextSibling)
                    }
                }
            })
            element.on('dragstart', function (e) {
                listCTRL.source = element[0];
                e.dataTransfer.effectAllowed = 'move';
            })
        }
    }
})

This draggable directive is used in the template that we have defined in 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 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 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 appropriate place.

Step 3: Define parent controller.

ngCustomDirectivesApp.controller('dashboardController', function ($scope) {
 
    $scope.itemsdata = ['Apple', 'Mango', 'Banana', 'PineApple', 'Grapes', 'Oranges'];

})

Step 4: Use directive:

<div ng-controller="dashboardController">

    <smart-list items="itemsdata" />

</div>

In our directive, itemsdata from the controller is passed on the scope of the directive.

Output:
Output

You can drag items to sort items according to 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 directives in AngularJS official documentation. If you want master directives then the best way to achieve this by start creating directives in our project. I have attached the complete code with the article. You can also download or clone it from the GitHub (https://github.com/vikas0sharma/Custom-Directive-Examples). I hope it will help you understand directives in AngularJS.

 

Advertisements

Author: Vikas Sharma

I am currently working as a Software Engineer in Magic Software and have an experience of more than a year in C#.Net. I have my graduation in Bachelor of Computer Applications and hold a diploma in Software Engineering GNIIT from NIIT. I am very passionate about programming, love to learn new technology and believe in sharing knowledge. My work experience includes Development of Enterprise Applications using C#,.Net,Sql Server,AngularJS and Javascript.