AngularJS Custom Directives for dummies

AngularJS directives are the commands which communicate with angular library and tell angular to do a job on the HTML tags, they extends the functionalities of the HTML elements. AngularJS provides many built-in directives e.g. ng-app, ng-model, ng-repeat, ng-controller etc. Thus, ng-model is used to do data binding. AngularJS also gives us the ability of writing our own custom directives, We can extend the abilities of HTML’s template to do anything, we can imagine.

Directives not only could be defined as new HTML tags but also as attributes, CSS classes or even HTML comments. Directives help us to make the reusable components to be used throughout an Angular Application.

Directive Syntax

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-value pairs. These key-value pairs decide the behaviour of a directive. Don’t get scared if you don’t understand, the syntax may seem dangerous to you but it is not, because most keys are optional and some are mutually exclusive. Let’s see what each of this properties means and how affect the behaviour of the directive.

  • Restrict
    The “restrict” attribute tells Angular, with one letter, how are we going to create our new directive. 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 like ‘EA’. 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 a 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
    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
    This is similar to template but the template is loaded from the specified URL, asynchronously.
  • 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
    Scopes are key concept to understand Angular. Scope is what glues JavaScript code with HTML and allow us to replace placeholders from templates with real values. 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.
    False: No scope will be created for the directive. The scope of the directive                       will be the same as the scope of the DOM element on which the directive is                      used.
    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.
    {…} (Object Literal) : A new “isolate” scope is created for the directive’s                          element. The ‘isolate’ scope differs from normal scope in that it does not                        prototypically inherit from its parent scope. This is useful when creating                           reusable components, which should not accidentally read or modify data
  • Transclude
    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
  • ControllerAs
    The alternate name can be given to the Controller
  • 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
  • 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.

Creating First Directive

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

var app = angular.module("DirectiveApp", []);
   app.directive('customMessage', function() {
   return {restrict: 'EA',
	   template: '
<div>
<h2>My First Custom Directive</h2>
This is a simple         example.</div>
'
	   }
});

angularcustomdirective1

Custom Directive 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

app.directive('likeButton', function () {
		return {
			restrict: 'E',
			templateUrl: 'templates/likeButton.html',
			replace: true,
			link: function (scope, element, attrs) {
				element.bind('click', function() {
					if (element.hasClass('glyphicon-thumbs-up'))
						element.removeClass("glyphicon-thumbs-up").addClass("glyphicon-thumbs-down");
					else {
						element.removeClass("glyphicon-thumbs-down").addClass("glyphicon-thumbs-up");
					}
				});
			}
		}
	});

Template

<span class="glyphicon glyphicon-thumbs-up" style="font-size: 50px; margin-left: 25px;"></span>

Explanation

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 span 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 changing CSS classes.

angularcustomdirective2

 Example using Scope

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

SCOPE SET TO FALSE

If the scope is set to false, the directive will use its parent scope as its own scope (generally it is the controller scope where the directive is under), 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 and Controller Definition

 //Controller Declaration
	app.controller('mainController', function($scope) {

		$scope.dataFromParent = "This is a Test";

	});

	//Directive Declaration
	app.directive('scopeFalseDirective', function() {
		return {
			restrict: 'E',
			scope: false,
			template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>'
		}
	});

Use of the Directive

<div ng-controller="mainController">
	Controller: <input type="text" ng-model="dataFromParent" style="border:1px solid green" />

	Scope False Directive: <scope-false-directive></scope-false-directive></div>

Explanation

When the scope is set to false, the Controller and the directive are using the same scope object, in this case the scope declared in the mainController. Hence, any changes to the controller and directive will be in sync.

angularcustomdirective3

SCOPE SET TO 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.

Directive and Controller Definition

//Controller Declaration
app.controller('secondController', function ($scope) {

	$scope.dataFromParent = "Antoher Test";

});

//Directive Declaration
app.directive('scopeTrueDirective', function () {
	return {
		restrict: 'E',
		scope: true,
		template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>'
	}
});

Use of the Directive

<div ng-controller="secondController">
	Controller: <input type="text" ng-model="dataFromParent" style="border:1px solid green" />

	Scope True Directive: <scope-true-directive></scope-true-directive></div>

angularcustomdirective4

ISOLATED SCOPE

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 is particularly 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 ways 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 prefixes to the Object literal that we set to the scope property.

Prefixes
Let’s see the syntax
scope: {
attrName1:’@’,
attrName2:’=’,
attrName3:’&’
}

The meaning of each prefix is the following:

  • @ 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

Directive and Controller Definition

//Controller Declaration
app.controller('thirdController', function ($scope) {

	$scope.dataFromParent = "Data from parent (controller)";
	$scope.changeValue = function () {
		$scope.dataFromParent = "CHANGED data from parent (controller)";
	}

});

//Directive Declaration
app.directive('scopeIsolatedDirective', function () {
	return {
		restrict: 'E',
		scope: {
			oneWayBindData: '@', //One way attribute,
			twoWayBindData: '=', //Two way attribute
			methodBind: '&', //parentMethodBinding
		},
		template: ' One way Binding: <input type="text" ng-model="oneWayBindData"  style="border:1px solid red;width:300px;"/>' +
			'
 Two Way Binding <input type="text" ng-model="twoWayBindData"  style="border:1px solid red;width:300px;"/>' +
			'
Method Biding : <button type="button" ng-click="methodBind()">Change Value</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.

Use of the Directive

<div ng-controller="thirdController">

	<scope-isolated-directive one-way-bind-data="{{ dataFromParent}}" two-way-bind-data="dataFromParent" method-bind="changeValue()"></scope-isolated-directive>

	Controller dataFromParent Property Value: <span><b>{{dataFromParent}}</b></span></div>

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

 Custom Directive using Controller

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.

1.- Define a directive to create a list base of an array of items passed to it.

app.directive('draggableList', function() {
	return {
		restrict: 'EA',
		templateUrl: '../templates/draggableListdirective.html',
		replace: true,
		scope: {
			items: '='
		},
		controller: function($scope) {
			$scope.source = null;
		},
		controllerAs: 'listController'
	}
});

Template

<ul>
	<li ng-repeat="item in items" 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 the upcoming steps. We have defined a controller, which holds a variable named as source.

2.- Creating draggable directive.

app.directive('draggable', function() {
	return {
		require: '^draggableList',
		link: function (scope, element, attr, listController) {
			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(listController.sourace, e.target)) {
						e.target.parentNode.insertBefore(listController.source, e.target)
					} else {
						e.target.parentNode.insertBefore(listController.source, e.target.nextSibling)
					}
				}
			});

			element.on('dragstart', function(e) {
				listController.source = element[0];
				e.dataTransfer.effectAllowed = 'move';
			});
		}
	}
});

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.

3.- Define the parent controller.

app.controller('draggableExampleController', function ($scope) {

	$scope.itemsdata = ['Apple', 'Mango', 'Banana', 'PineApple', 'Grapes', 'Oranges'];

});

4.- Use of the directive

<div ng-controller="draggableExampleController">

	<draggable-list items="itemsdata" /></div>

angularcustomdirective6

 Custom Directive using Transclusion

1.- Create parent accordion directive, which will hold the children accordion elements.

app.directive('accordion', function() {
	return {
		restrict: 'EA',
		template: '
<div ng-transclude></div>
',
		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);
			}
		}
	}
});

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.

2.- Create accordion child element directive.

app.directive('accordionItem', function() {
	return {
		restrict: 'EA',
		template: '
<div>
<div class="heading" ng-click="toggle()">{{title}}</div>
<div class="content" ng-show="show" ng-transclude></div>
</div>
',
		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 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.

3.- Use of the directive

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

angularcustomdirective7

Hope this post is useful to understand better how the directive works. You can find the code of this article HERE

Advertisements
This entry was posted in AngularJs and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s