Know about Custom Directive


While working on one of the applications being developed on the AngularJS framework, we came across a situation where we had to add a reusable and independent functionality on a DOM element. This could easily be done using jQuery. However, since the app was being developed using AngularJS, the best practice was to stick to that only, which was made easy with the AngularJS “Directive” tool.

What are Custom Directives?

At high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS HTML compiler ($compile) to attach a specified behavior to that DOM element (e.g. via event listeners), or even to transform the DOM element and its children.
AngularJS comes with a set of these directives built-in, like ngBindngModel, and ngClass. Much like you create controllers and services, you can create your own directives for AngularJS to use. When AngularJS bootstraps your application, the HTML compiler traverses the DOM matching directives against the DOM elements.

Normalization

AngularJS normalizes an element’s tag and attribute name to determine which elements match which directives. We typically refer to directives by their case-sensitive camelCase normalized name (e.g. ngModel). However, since HTML is case-insensitive, we refer to directives in the DOM by lower-case forms, typically using dash-delimited attributes on DOM elements (e.g. ng-model).
The normalization process is as follows:
  1. Strip x- and data- from the front of the element/attributes.
  2. Convert the :-, or _-delimited name to camelCase.
For example, the following forms are all equivalent and match the ngBind directive:
<div ng-controller="Controller">
  Hello <input ng-model='name'> <hr/>
  <span ng-bind="name"></span> <br/>
  <span ng:bind="name"></span> <br/>
  <span ng_bind="name"></span> <br/>
  <span data-ng-bind="name"></span> <br/>
  <span x-ng-bind="name"></span> <br/>
</div>

Directive types

$compile can match directives based on element names (E), attributes (A), class names (C), and comments (M).
The built-in AngularJS directives show in their documentation page which type of matching they support.
The following demonstrates the various ways a directive (myDir in this case) that matches all 4 types can be referenced from within a template.
<my-dir></my-dir>
<span my-dir="exp"></span>
<!-- directive: my-dir exp -->
<span class="my-dir: exp;"></span>

A directive can specify which of the 4 matching types it supports in the restrict property of the directive definition object. The default is EA.

Defining a Directive

This section lists simple steps to define a custom directive in an AngularJS module. First we need to define an AngularJS app then our own custom directive named “myCustomer“.

 

angular.module('myApp', [])
.controller('MyController', ['$scope', function($scope) {
  $scope.customer = {
    name: 'Anil',
    address: 'Hyderabad'
  };
}])
.directive('myCustomer', function() {
  return {
    template: 'Name: {{customer.name}} Address: {{customer.address}}'
  };
});

<div ng-app="myApp" ng-controller="Controller">
  <div my-customer></div>
</div>

Syntax of a Directive

angular.module('myApp', [])
.directive('myDirective', function() {
  return {
    restrict: 'E',
    transclude: true,
    templateUrl: 'src/app/component/landing/landing.directive.html',
    scope:{
        'name': '=',
        'address': '@address',
        'onClick': '&onClick'
     },
     compile: function(tElement, tAttrs, transclude){
     },
     link: function(scope, iElement, iAttrs, controllerAs, transcludeFn){
     },
     controller: function($scope, $rootScope){
     }
  };
});

Properties wise details

  1. templateUrl can also be a function which returns the URL of an HTML template to be loaded and used for the directive. AngularJS will call the templateUrl function with two parameters: the element that the directive was called on, and an attr object associated with that element.
  2. The restrict option is typically set to:
    • 'A' – only matches attribute name
    • 'E' – only matches element name
    • 'C' – only matches class name
    • 'M' – only matches comment
    These restrictions can all be combined as needed:
    • 'AEC' – matches either attribute or element or class name
  3. scope: What we want to be able to do is separate the scope inside a directive from the scope outside, and then map the outer scope to a directive’s inner scope. We can do this by creating what we call an isolate scope. To do this, we can use a directive’s scope option.
angular.module('myApp', [])
.directive('myDirective', function() {
  return {
    restrict: 'E',
    scope: {
      customerInfo: '=info'
    },
    templateUrl: 'my-customer-iso.html'
  };
});
angular.module('myApp', [])
.directive('myDirective', function() {
  return {
    restrict: 'E',
    scope: {
      customerInfo: '=info'
    },
    templateUrl: 'my-customer-iso.html'
  };
});
The scope option is an object that contains a property for each isolate scope binding. In this case it has just one property:
  • Its name (customerInfo) corresponds to the directive’s isolate scope property, customerInfo.
  • Its value (=info) tells $compile to bind to the info attribute.
  • (=info) binds both way between two components(controller to directive and vice-versa).
  • (@info) binds one way from controller to directive.
  • (&info) binds one way from directive to controller.

4. Link

Directives that want to modify the DOM typically use the link option to register DOM listeners well as update the DOM. It is executed after the template has been cloned and is where directive logic will be put.
link takes a function with the following signature, function link(scope, element, attrs, controller, transcludeFn) { ... }, where:
  • scope is an AngularJS scope object.
  • element is the jqLite-wrapped element that this directive matches.
  • attrs is a hash object with key-value pairs of normalized attribute names and their corresponding attribute values.
  • controller is the directive’s required controller instance(s) or its own controller (if any). The exact value depends on the directive’s require property.
  • transcludeFn is a transclude linking function pre-bound to the correct transclusion scope.
myApp.directive('myDirective', function() {
  return {
    restrict: 'E',
    link: function(scope, element, attr){
          element.append("<strong>"+attr.title+"</strong>");
    }
  };
});
<div ng-app="myApp">
  <my-directive title="Hello Friends"></my-directive>
</div>

5. Compile

function compile(tElement, tAttrs, transclude) { ... }
The compile function deals with transforming the template DOM. Since most directives do not do template transformation, it is not used often. The compile function takes the following arguments:
  • tElement – template element – The element where the directive has been declared. It is safe to do template transformation on the element and child elements only.
  • tAttrs – template attributes – Normalized list of attributes declared on this element shared between all directive compile functions.
  • transclude – [DEPRECATED!] A transclude linking function: function(scope, cloneLinkingFn)

 

6. Controller

Controller constructor function. The controller is instantiated before the pre-linking phase and can be accessed by other directives (seerequire attribute). This allows the directives to communicate with each other and augment each other’s behavior. The controller is injectable (and supports bracket notation) with the following locals:

  • $scope – Current scope associated with the element
  • $element – Current element
  • $attrs – Current attributes object for the element
angular.module('docsTabsExample', [])
.directive('myTabs', function() {
  return {
    restrict: 'E',
    transclude: true,
    scope: {},
    controller: ['$scope', function MyTabsController($scope) {
      var panes = $scope.panes = [];

      $scope.select = function(pane) {
        angular.forEach(panes, function(pane) {
          pane.selected = false;
        });
        pane.selected = true;
      };

      this.addPane = function(pane) {
        if (panes.length === 0) {
          $scope.select(pane);
        }
        panes.push(pane);
      };
    }],
    templateUrl: 'my-tabs.html'
  };
})
.directive('myPane', function() {
  return {
    require: '^^myTabs',
    restrict: 'E',
    transclude: true,
    scope: {
      title: '@'
    },
    link: function(scope, element, attrs, tabsCtrl) {
      tabsCtrl.addPane(scope);
    },
    templateUrl: 'my-pane.html'
  };
});

<my-tabs>
  <my-pane title="Hello">
    <p>Lorem ipsum dolor sit amet</p>
  </my-pane>
  <my-pane title="World">
    <em>Mauris elementum elementum enim at suscipit.</em>
    <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
  </my-pane>
</my-tabs>
<div class="tabbable">
  <ul class="nav nav-tabs">
    <li ng-repeat="pane in panes" ng-class="{active:pane.selected}">
      <a href="" ng-click="select(pane)">{{pane.title}}</a>
    </li>
  </ul>
  <div class="tab-content" ng-transclude></div>
</div>

<div class="tab-pane" ng-show="selected">
  <h4>{{title}}</h4>
  <div ng-transclude></div>
</div>

7. Transclude

What does this transclude option do, exactly? transclude makes the contents of a directive with this option have access to the scope outside of the directive rather than inside.
To illustrate this, see the example below. Notice that we’ve added a link function in script.js that redefines name as Jeff. What do you think the {{name}} binding will resolve to now?

 

angular.module('myApp', [])
.controller('MyController', ['$scope', function($scope) {
  $scope.name = 'Tobias';
}])
.directive('myDialog', function() {
  return {
    restrict: 'E',
    transclude: true,
    scope: {},
    templateUrl: 'my-dialog.html',
    link: function(scope) {
      scope.name = 'Jeff';
    }
  };
});
<div ng-controller="MyController">
  <my-dialog>Check out the contents, {{name}}!</my-dialog>
</div>
Ordinarily, we would expect that {{name}} would be Jeff. However, we see in this example that the {{name}} binding is still Tobias.
The transclude option changes the way scopes are nested. It makes it so that the contents of a transcluded directive have whatever scope is outside the directive, rather than whatever scope is on the inside. In doing so, it gives the contents access to the outside scope.
Note that if the directive did not create its own scope, then scope in scope.name = 'Jeff' would reference the outside scope and we would see Jeff in the output.

Advantages

Directives are useful in creating repeatable and independent code. They modularize the code by clubbing requirement-specific behavioral functions in one place, rather than creating some objects in the central controller and manipulating them using multiple JavaScript methods. Such a modular code will have multiple directives that handle their own functionalities and data, and work in isolation from other directives. As an added benefit, the HTML page and Angular scripts becomes less messy and more organized.

Conclusion

In this post we learned about Custom Directives in AngularJS through a brief introduction and examples of their usages and their advantages. With Directives, you can create custom HTML tags, attributes, or classes to implement required functionality over an HTML section. This then becomes an independent and reusable component that can be embedded in the same or a different HTML page.