How to Write Custom AngularJS Directives

Much of Angular’s built-in functionality is provided by modular slices of code called directives. You can write custom directives to perform form validation, to minimize code repetition, to attach events to elements, to inject markup into templates, and more. Directives are so powerful that their usefulness is limited only by how well you understand them. Get more out of Angular by learning how to write custom directives today.

Matching Directives

As a page is loading, Angular’s HTML compiler ($compile) parses the DOM looking for elements or attributes that match any directive names. There are a couple naming guidelines you should follow to ensure your own directives can be matched.

In JavaScript directives should be given camel case names (“myDirective”). Angular will match against a dash-delimited name (“my-directive”) when traversing the DOM. It is best practice to prefix the names of your custom directives to ensure they won’t conflict with directives or tags released in future versions of Angular or HTML. In particular, avoid names beginning with “ng.”

Three Types of Functions

Directives are like onions. They may contain three nested functions, colloquially referred to here as the Init, Compile, and Link functions. These functions represent three sequential steps in Angular’s page load process:

  1. The module specified by ng-app is loaded and the application injector is created.
  2. The DOM inside ng-app, also called a template, is compiled.
  3. The template is rendered visually.

An outline of a directive with all three functions would look like this:

myApp.directive('myDirective', function Init() {
  //Code that runs once when app is initialized     
  return {
    compile: function Compile(templateElement, templateAttributes) {
      //Code that runs once for each matched element as the template is compiling
      return function Link(scope, linkElement, linkAttributes, controller) {
        //Code that runs once for each matched element after it is rendered
      };
    }
  };
});

A more common format includes a Link function and options:

myApp.directive('myDirective', function() {
  return {
    restrict: 'A',
    // More options here
    link: function (scope, linkElement, linkAttributes, controller) {
      // Link Function
    }
  };
});

Restrict tells $compile what types of objects in the DOM to match against. Default is “AE,” or attributes and element names. Keep in mind that custom elements will cause HTML5 validation to fail, and that legacy browsers like IE 8 do not support them at all.

It is common for directives to contain just a Link function, so a shorthand exists:

myApp.directive('myDirective', function () {

  return function (scope, linkElement, linkAttributes, controller) {
    // Link Function
  };
});

Link Function

Your Angular scope is the first parameter to this function. In fact, this is the only function from which you may get or set the values of scope objects. For this reason, it is the most widely used directive function.

The second parameter is the jQuery object representing the element to which the directive is attached.

The third parameter is an object containing all the element’s attributes. Access attributes by indexing with name:

linkAttributes['attributeName']  // ex. var title = linkAttributes['title']

The fourth parameter is the directive’s own Angular controller, or an injected controller instance depending on the require option. Directives can declare their own controllers with the controller option, and these controllers can be referenced by other directives using require. With this technique you can create directives that communicate.

There are two varieties of the Link function: post-link and pre-link. Pre-link functions differ from post-link only in that they are guaranteed to run before any of the element’s post-link functions. By default, a link function is post-link.

Getting and Setting Scope Members

If the value of an attribute is not a mere string, but an object or some JavaScript statements, use the parse service to resolve it.

<div my-directive="myFamily.pets"></div>
var myFamily = { pets: ['Lilith', 'Rogue'], adults: […] }; // Inside controller
var petsGetter = $parse(linkAttributes['myDirective']);    // Inside MyDirective

Parse always returns a function. Call that function with scope as a parameter to receive the original object or statement(s) you fed to the directive.

var pets = petsGetter(scope);   // pets = ['Lilith', 'Rogue']

If the original argument was an object, you can also save its value back to the scope in a similar fashion:

pets.push('River');

var petsSetter = petsGetter.assign;
petsSetter(scope, pets); // $scope.pets = ['Lilith', 'Rogue', 'River']

Compile Function

The Compile function runs before any templates are cloned, which means the DOM is still in its original state. If you need to modify the DOM before directives like ng-repeat have spawned its children, use this function.

Attaching Event Listeners

Directives should be used to add custom behavior for events like blur, focus, click, and keyup. Attach event listeners to elements or $document inside the Link function.

This example allows a click or enter key action to work on any element. This is useful if you want to attach ng-click behavior to a non-traditional input element—and have keyboard interaction work as usual. Be sure to include tabindex=”0″ so the element is added to the natural tab order.

We will use the parse service to resolve the JavaScript snippet that will be invoked when our event is triggered.

<th enter-or-click="sortBy('firstName')" tabindex="0">First Name</th>
myApp.directive('enterOrClick', function ($parse) {
  return function (scope, linkElement, linkAttributes) {

    var funcGetter = $parse(linkAttributes ['enterOrClick']);

    var handler = function (event) {
      scope.$apply(function () {
        funcGetter(scope, { $event: event });
      });
    };

    //Call function if user clicked or hit enter
    linkElement.bind('click', handler);
    linkElement.bind('keyup', function (event) { 
      if (event.keyCode == 13) handler(event); 
    });
  };
});

Expanding the Template

It is a good idea to store oft-repeated markup in a single place. Directives make it easy to reference that markup and render it. Isolate the scope of the directive by specifying the scope option. This allows you to reuse the directive on the same page for different objects.

<div user-profile="bob"></div>
<div user-profile="eve"></div>
$scope.bob = { name: 'Bob', loginName: 'bobby' };   // Inside controller
$scope.eve = { name: 'Eve', loginName: 'eevee' };
myApp.directive('userProfile', function() {
  return {
    scope: {
      person: '='
    },
    template: 'Name: {{person.name}} Login: {{person.loginName}}'
  };
});

The equals sign tells $compile to bind person to the object passed to the directive. You may specify a different element attribute by adding its name after the equal sign (ex. ‘=data’).

Ideally you should store orphaned markup in its own html file:

myApp.directive('userProfile', function() {
  return {
    scope: {
      person: '='
    },
    templateUrl: '/Content/userProfile.html'
  };
});

Transclude Option

To transclude means to include the content of one item inside another item by reference. Sometimes you want a directive to add markup so that it wraps other arbitrary markup. You may also want a directive to have the same scope as the controller outside it. Both can be achieved with the transclude option.

<div fancy-wrapper>                  // Original markup
  <span>Welcome, {{name}}!</span>
</div>   
<div class="fancy-wrapper" ng-transclude></div>  // fancy-wrapper.html
$scope.name = 'Will';                            // In controller
myApp.directive(fancyWrapper, function() {
  return {
    transclude: true,
    templateUrl: '/Content/fancy-wrapper.html'
  };
});
<div fancy-wrapper>                              // Final markup
  <div class="fancy-wrapper" ng-transclude> 
    <span>Welcome, Will!</span>
  </div>
</div>

Custom Validation Tests

Although Angular comes pre-equipped with a number of powerful form validation tools, you may need to write a directive to add complicated tests. Need to validate an input against a custom check, like a regular expression? Write a directive in this format: 

<input type="text" name="email" ng-model="email" email-regex required />
<span ng-show="myForm.email.$error.emailregex">Please enter a valid email</span>
myApp.directive('emailRegex', function () {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function (scope, linkElement, linkAttributes, controller) {
    
      var regex = /^[a-zA-Z0-9-_]+[a-zA-Z0-9-_.]*(.[a-zA-Z0-9-_]+)*([a-zA-Z0-9]+)@[a-zA-Z]+[0-9a-zA-Z-.]*[0-9a-zA-Z]+.[a-zA-Z]{2,5}$/;
    
      function checkValidity(value) {
        var valid = true;
      
        // This directive should consider an empty value to be valid
        // because the required test, if specified, occurs separately
        if (value != null && value.length > 0) {
          valid = regex.test(value);
        }

        return valid;
      }

      controller.$parsers.unshift(function (value) {
        // View to model
        var valid = checkValidity(value);

        // Note "emailregex" instead of "emailRegex" - should be all lowercase
        // This allows "$error.emailregex" to work in the DOM
        controller.$setValidity('emailregex', valid);

        // Only return the value to the model if it's valid
        return valid ? value : undefined;
      });

      controller.$formatters.unshift(function (value) {
        // Model to view
        controller.$setValidity('emailregex', checkValidity(value));

        // Return the value regardless of validity,
        // otherwise nothing will appear on the DOM
        return value;
      });
    }  
  };
});

The require option tells $compile that this directive has a dependency on another directive (ngModel). Angular will automatically inject it.

Formatters and Parsers

Formatters and parsers are attached to the controller and perform similar tasks. Formatters change how model values will appear in the view, whereas parsers change how view values will be saved back to the model. This means you can use them to transform the value so it is different between the view and the model (ex. display a phone number on the view like (xxx) xxx-xxxx, but for the model remove all non-numerical characters).

Both a formatter and a parser need to be added to our validation directive so the value given by the user can be tested on both ends.

(Note that if an element is invalid, its model value should be undefined.)


Found this blog post useful? Make yourself comfortable and check out our blog home page to explore other technologies we use on a daily basis and the fixes we’ve solved in our day to day work. To make your life easier, subscribe to our blog to get instant updates sent straight to your inbox:

{{cta(‘33985992-7ced-4ebd-b7c8-4fcb88ae3da4’)}}

Leave a Comment

Easy Dynamics Login