Mastering AngularJS Directives

5 (1 reviews total)
By Josh Kurz
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. The Tools of the Trade

About this book

AngularJS offers many features that allow the creation of rich, compelling web applications. Directives are by far the most important feature to incorporate into any skill set. This book starts off by teaching basic and advanced techniques for the art of directive writing. The different techniques are taught by a series of examples that showcase when and why certain directives should be created, based on given use cases. It moves on to explain more about how to harness the potential of AngularJS, by incorporating external libraries, optimizing code, and using brand new functions such as animations.

Finally, the book includes an overview of the techniques and best practices that AngularJS developers need to keep in mind while developing their own applications. The overall goal of this book is to teach different aspects of directive writing that can be consumed by all levels.

Publication date:
June 2014
Publisher
Packt
Pages
210
ISBN
9781783981588

 

Chapter 1. The Tools of the Trade

The leading edge of web development moves quicker than most mortal humans can keep up with. There are so many different techniques to learn and harness that it can sometimes seem overwhelming. Thankfully, there is a wonderful JavaScript framework called AngularJS that helps us mere mortals become something greater.

AngularJS offers many different facets of technology that can be used to accomplish different tasks efficiently and effectively. There is no specific implementation that AngularJS is built from that is more powerful than a directive. A directive may be defined as an official or authoritative instruction. This is a modern nontechnical term for a directive. In AngularJS, directives still follow this definition; however, a more technical description could be a set of instructions, the ultimate goal of which is to read or write HTML.

Directives can be used to solve many different use cases related to Document Object Model (DOM). Directives allow developers to create new HTML elements that can do almost anything inside AngularJS. Teaching the browser new functionality to provide DOM manipulation, creation, and event detection is a new idea that is just becoming popular.

AngularJS takes a different approach to how JavaScript and HTML5 work in unison with each other. There is no longer a need to constantly traverse the DOM tree for every bit of functionality that needs to be created. Directives allow for a declarative approach, which separates DOM manipulation logic and business logic. This separation means that our new applications are more readable, testable, and perform better.

 

Introduction to directives


When first learning about AngularJS, directives can create a magical illusion that hides the logic associated with the view's actions. This hidden logic is by design and is the reason AngularJS is so popular. The gory details of every directive are not important to some developers, but these details are the lifeblood of custom directives.

A directive is essentially just a JavaScript factory function that is defined inside of a given AngularJS module. The function returns an object that holds a set of instructions for the AngularJS HTML compiler. This object can either be a function that is run once the element is linked to the scope (link function) or a JSON representation of more advanced instructions that ultimately should also contain a link function. Returning a JSON instruction representation is referred to as returning a definition object and is the community-preferred method of writing directives.

Definition objects can have a finite number of options, made available by the AngularJS public directive API. Let's break down a simple definition object as follows:

var definitionObject = {
   restrict: 'EA',
   link: function(scope, element, attrs){
     element.text('Hello Directive World');
   }
};

Tip

Downloading the example code

You can download the example code files for all Packt books that you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you. The code can also be found at https://github.com/joshkurz/Black-Belt-AngularJS-Directives/tree/master/chapters.

This definition object has two properties that instruct the AngularJS compiler to do specific tasks. These instructions state that the directive can only be created on an element or attribute, and once it's found, to set its text to 'Hello World'. The specific syntax needed to add this directive to the AngularJS compiler is as follows:

app.directive('bbHelloWorld', function(){
  return definitionObject;
});

Tip

The directive is named bbHelloWorld because we are namespacing all of our directives with bb. This is a community-preferred method of directive writing because creating reusable code that does not interfere with other applications is the goal.

Now that our directive has been created, we can use it in any HTML template that is part of our ng application. To call this directive, we write something like the following lines of code:

<bb-hello-world></bb-hello-world>

The output will be as follows:

Hello Directive World

This was an example of the most basic type of directive. There is nothing wrong with creating simple directives that accomplish basic tasks, but AngularJS allows for all levels of directives to be written. The complete Definition Object API should be understood to create more advanced directives.

Directive Definition Object API

The compiler is given instructions from the directive's definition object. These instructions can be integers, strings, booleans, or JavaScript objects. Their purpose is to give the developer many different options of control when initializing and dynamically manipulating DOM according to the given model.

Priority

The priority integer is used when multiple directives are set on the same element. AngularJS collects all of the known directives on an element by any of the defined restrict properties and runs each directive's compile, prelink, and postlink functions in a given order. The order is specified by priority. Lower priority compile and prelink functions are run last; however, the postlink function is the opposite. The default value is zero. Negative values are allowed in case directives need to be compiled after default directives.

Terminal

The terminal field is a Boolean whose default value is false. The terminal value of a directive applies to lower priority directives defined on the same element and all of their child directives. Setting a terminal field to true states that the applicable directives will not compile during the initial directive collection. The initial collection can be run in AngularJS during the initial bootstrap or a manual $compile function call.

An HTML example looks like the following:

<div directive-one directive-two directive-three>
  <directive-four></directive-four>
</div>

The following code snippet shows how the directive definition of a terminal directive looks:

app.directive('directiveTwo', function() {
    return {
        priority: 10,
        terminal: true,
        link: function(scope,element,attrs) {
            //link logic
        }
    }
});

If directiveOne and directiveThree have a priority lower than 10, then they will not run on this given div element. The directiveFour directive will not run because it is a child of the terminal directive. Just because these elements have not been run during the initial collection phase does not mean that they cannot be run at a later time, depending upon other directive definition objects that could have been set.

Tip

The terminal option is used in the core library by a few different directives. The most notable are ngRepeat, ngIf, and ngInclude.

Scope

A scope is a JavaScript object that is created by AngularJS during the initial bootstrap. This initial scope is referred to as the $rootScope directive and can be created in two different ways. The ngApp directive or angular.bootstrap can be called on an element. Either way, the result is the same.

Once the application has been bootstrapped and a $rootScope directive has been created, subsequent child directives are in charge of creating new types of scopes. Together, these new scopes create a hierarchy of communication channels. There are specific rules for how each channel is allowed to communicate with each other.

The following two different types of scopes denote the communication rules that are observed by the scope hierarchy:

  • The child scope

  • The isolate scope

These two scope objects have different types of communication abilities. Child scopes are created using normal JavaScript prototypal inheritance, which means they inherit their parents' attributes but have their own context. Isolate scopes create a separate context that only has a root scope as a commonality between itself and its defining scope.

The scope is what drives a directive's ability to keep its view in sync with its associated data models. The scope can hold any number of data models and can either watch their values for changes or just read from them. There are three different options available when creating new scopes via a directive definition object: defining scopes, child scopes, and isolate scopes. Let's go over an example of a simple app and its scope hierarchy in the following diagram:

There are two directives present in the demo application represented by the preceding diagram. Each cylinder is a directive that either creates its own scope or uses its defining scope. For example, the directiveA cylinder creates scopeA and directiveB creates either scopeB or uses its defining scope, that is, scopeA. Let's say that directiveA creates a child scope called scopeA. Depending upon the scope definition of directiveB, three options are available for the scope it creates. The directiveB cylinder could just use its defining scope. A new child scope could be made, which would create prototypal inheritance from scopeA. Lastly, an isolate scope could be created, which would prevent directiveA from accidentally reading or writing to scopeA.

There is a specific syntax that is used to create the following three possible options for the scope of a directive:

  • False/default: This is to use the defining/containing scope as the directives own scope

  • True: This is to create a new scope, which prototypically inherits from the defining/containing scope

  • Object hash: This is to create an isolate scope (the hash defines the details of the scope)

Two directives that do not create new scopes are ngShow and ngHide. These directives perform tasks declared on the element's attributes depending upon the defined scope. Since the directive is not actually ever going to change the model itself, it is safe for it to exist on the defining scope. Directives that alter the model in some way should either be given a child scope or an isolate scope.

Child scopes are useful when creating directives that live around other directives of their kind. This is because child scopes are not accessible to sibling elements; so by writing to a child scope, the directive is ensuring that none of its siblings will have any of their data overwritten. Child scopes create a prototypal inheritance model. There are some nuances to dealing with child scopes that are covered in detail in Chapter 5, Communication between Directives.

Isolate scopes are probably the most widely used scope option in directives in the open source community. This is because of the different options that are available with it. These options allow for a very unique and special functionality. There are three options available when defining an isolate scope variable. These options are the values of the isolate scopes definitions. Isolate scopes allow for special types of data binding.

Tip

Isolate scopes are defined by three available values that perform different types of data binding.

The following code snippets shows scope variables:

   // String representation of a defining scope's variables
Javascript: Scope: {'name': '@'}
HTML: <div bb-stop-watch name="{{localName}}"></div>

// An expression executed on the defining scope
Javascript: Scope: {'name' : '&'}
HTML: <div bb-stop-watch name="newName = localName + ' ha ha'"></div>

   // Two Way Data Binding
   JavaScript: Scope: {'name': '='}
HTML: <div bb-stop-watch name="localName"></div>

All of these defined local scope variables are called scope.name, which are passed by a directive attribute called name as well. The attribute could be called something other than name, with a syntax that specifies the attributes names. The following code snippet is the same example, just with different attribute's names that define the value:

   // String representation of a defining scope's variables
JavaScript: Scope: {'name': '@theName'}
HTML: <div bb-stop-watch the-name="{{localName}}"></div>

// An expression executed on the defining scope
JavaScript: Scope: {'name' : '&theName'}
HTML: <div bb-stop-watch the-name= "newName = localName + ' ha ha'"></div>

   // Two Way Data Binding
   JavaScript: Scope: {'name': '=theName'}
HTML: <div bb-stop-watch the-name="localName"></div>

Each of these variables performs specific tasks as denoted by the comments above the scope definitions in the preceding code snippet. The first represents a string that is interpolated from the defining scope. The second represents an expression that returns some variable to be set as the local scope attribute definition. The expression used in the preceding example is simple, but these can be defined on any scope and can be as complex as needed.

Tip

Expressions that are set inside of curly brackets in an AngularJS application will be evaluated on every digest. This means that the developer should be careful to not set expensive expressions that could cause the digest cycle to take longer than 50 ms.

The third variable is used to create two-way data binding between the parent and its isolate scopes. This means that a watch is set on the variable automatically, and once one of the variables changes, all of the variables that reference each other will change as well. This is one of the most common AngularJS tactics and also one of the most spectacular.

The following is another example of three different input directives that utilize each one of the methods:

  */ HTML Templates */
<div bb-string term="{{term}}"></div>
<div bb-expression term="theTerm = term + ' AngularJS Directives'"></div>
   <div bb-two-way term="term"></div>

 
*/ Demo Javascript Module */ 
angular.module('demoApp', [])
.controller('demoCtrl', function($scope){
    $scope.term = 'How To Master';
})
.directive('bbString', function(){
    return {
        scope: { term: '@'},
        template: '<input ng-model="term">'
    }
})
.directive('bbExpression', function(){
    return {
        scope: { term: '&'},
        template: '<input ng-model="term">',
        link: function(scope, element, attrs){
            scope.term = scope.term();
        }
    }
})
.directive('bbTwoWay', function(){
    return {
        scope: { term: '='},
        template: '<input ng-model="term">'
    }
});

Take a look at the following screenshot that shows the differences between the different directives:

Tip

Typing into the first or second directives will not alter the parent $scope.term variable, but typing into the third input will alter the first directive's model and its parent directives. A live example is available at http://jsfiddle.net/joshkurz/x22y2/.

There is a fourth type of scope that can be used inside of a directive. This scope is only created when the directive is utilizing transclusion. The scope created is a child of the definition scope, and when inserted into the directive element, it becomes a sibling of the directive scope, whatever it may be.

Controller

The controller definition option creates mostly all the controllers created in an AngularJS application. This is not a very common insight, but it is true. Even the AngulaJS routing functions that associate a controller with a specific URL, wrap their associated template with ngController and feed the data used in the route definition of this directive.

A controller is a constructor that creates a new context (that is… this) that can define its own variables and functions every time its own constructor function is called. This constructor function can be called in various ways. Some common ways are to have a controller associated with a route, using an ngController directive directly, or create a custom directive that properly uses the named controller option to initialize a new instance of a controller.

The controller option is a string or inline function. If the value is a string, then it maps to a controller constructor function set on a module that the directive is tied too. Controllers and directives work together to keep the view aligned with the model.

The main purpose of having directives utilize their own private controllers is for interdirective communication. This is most commonly a requirement with reusable directives that work independently of each other, but depend on shared resources that instruct state changes. A directive can require any amount of controllers necessary to perform its tasks, whatever they may be.

A qualifier for a controller option is a set of directives that will always have a parent-child relationship and have the need to communicate with each other. The parent directive should have the main controller defined on its definition object. The child directive should require this controller.

Let's build a simple bbStopLight directive that is broken down by two individual directives that share a parent-child relationship. The parent bbStopLightContainer directive is used to contain all of the child bbStopLight directives. The bbStopLightContainer directive is in control of which bbStopLight will be active. This information needs to be communicated to all the associated directives.

For the following example, we only show the parent bbStopLightContainer directive; the child bbStopLight directive will be discussed at a later time:

// The container for the stopLight's
angular.module('TrafficLight')
 .directive('bbStopLightContainer', [ function() {
      return {
        controller: 'bbStopLightCtrl',
        scope: {options: '='}
       };
      }])
.controller('bbStopLightCtrl', function($scope,$interval){
          

         
          // setting options to the function's context
          this.options = $scope.options;
    
          this.setNextState = function(){
          state = $scope.options.state;
          // setting next state logic
        };
    
        $interval(this.setNextState,this.options.interval);  
    });

Note

If the value of the controller value defined on the definition object is an @ sign, then the controller name will be the directive's name, for example, stop-light="stopLightCtrl". This is how ngController works.

Directive controllers are meant to create a medium for communication. This medium is used by the directive controllers to pass objects between related directives. Some nonintuitive behaviors of the directive's controllers are that the only objects accessible in the actual directives via the require field are defined on the function's context. This means that setting functions and attributes on the controller's scope object will not allow for its objects to be shared in directives that require it. The controller's scope is specific to the controller itself and is not shared between directives.

The bbStopLightContainer directive can always refer to what its current state is. Now that we have a parent container directive that will control the information for all our stoplights, it's time to create a bbStopLight directive and show how it works in collaboration with this new controller.

Require

The require field is a string or an array of strings. Each string represents a directive that provides a controller. The require field is essentially a key that helps AngularJS map the controller we want to pass into the link function. This is done during the pre and post stages of the link functions, which means the controllers are available in both as the fourth parameter. The controller that is passed into a directive during the link function is a representation of either a controller on a parent directive or the current directive. This is why directives that use require must have some type of relationship with the directive that they require a controller from.

The following four different options are available when initializing the require field on a definition object:

  • require: 'directiveName' – This option searches for a directive on the current element; if not found, it throws an error

  • require: '?directiveName' – If a directive is not present on an element, this option passes a null value to the link function

  • require: '^directiveName' – This option searches for a directive on an element's parent directives, and if not found, throws an error

  • require: '^?directiveName' – This option searches the parent directives for the directive and passes a null value to the link function if not found

Once the controller is passed to the directive, it can be read from, or written to, and this is true for any other directive that requires that controller. This gives direct access to the directives that require it and to other directives that also require the same instance of this controller. Remember that the shared controller is an instance of the controller that is created on the directive that calls the controller constructor. This opens up many options for child directives that perform specific tasks, depending on the state of the parent directive and its controller.

Let's allow the bbStoplight directive to communicate with its parent controller (bbStopLightContainer) by adding the require field to its definition object with the help of the following code snippet:

    // now bbStopLight requires the bbStopLightContainers Controller
angular.module('TrafficLight')
  .directive('bbStopLight', function(svgService) {
    return {
        require: '^bbStopLightContainer',
        scope: {},
        link: function(scope,element,attrs,stopLightCtrl) {
            // the logic that determines what to do with the 
            // linked stop light element
        }
    };
});

Now, we can access the bbStopLightCtrl directive's objects that are publicly exposed, which means we can set our own options for each specific stoplight and turn it on or off. To do this, we could observe the values for a change on the bbStopLightCtrl directive and update the bbStopLight directive accordingly.

ControllerAs

The controllerAs field is a string that represents an alternative way to reference the directive's controller from the template. Once the controllerAs field has been set to a name, the function context (this) turns into the string representation of the controllerAs value.

It is the same as using $scope.controllerAsValue = this and being able to reference it inside of the HTML or template.

Restrict

The Restrict field is a character value that represents how the directive is defined in the DOM. AngularJS has a built-in compile function that is run on initialization and at the developer's choice. The $compile service is its public access point. This compile function collects directives and parses the DOM tree recursively, looking for directives by matching each element's nodeType to the list of directives attached to the defined app's modules. There are four different types of representations a directive can have, and all of them are denoted by the Restrict option listed as follows:

  • Restrict: 'A' – This option represents the directive that is an attribute of the element (default), which implies that Angular is looking for <div my-directive></div >

  • Restrict: 'E' – This option represents the directive that is an element, which implies that Angular is looking for <my-directive></my-directive>

  • Restrict: 'C' – This option represents the directive that is a class definition, which implies that Angular is looking for <div class="my-directive"></div>

  • Restrict: 'M' – This option represents the directive that is a comment, which implies that Angular is looking for <!-- directive: my-directive attrs-->

Any of the options of the restrictions mentioned in the preceding list can be combined together to create a directive that can have multiple options. Cross-browser compatibility is the biggest reason to go with the A restriction most of the time. IE 8 and 9 require special shivs to work with element directives, and there are some issues that IE has with reading comment directives as well. Class directives work across browsers, and when used properly, can be useful. The comment directive can have special use cases, such as when compiling a directive and creating a compiled comment directive, which are not seen by the user.

Template

The template option is a string or a function that returns a string. The string represents the HTML that the directives inject into the DOM once fully compiled and linked. Templates are very useful and help keep HTML source files clean and readable. Directives can be created inside of other directive templates; they allow for nested dynamic directive creation possibilities.

Templates have access to the directive's scope during the linking phase of a directive. This allows for any scope variable or function to be added to the template and utilized in the same fashion as any other HTML markup inside of an AngularJS application. The only stipulation is that the developer has to make sure that the objects being used in the template are actually available in the directive's scope.

Tip

Being able to determine what will be active on the directive's scope during runtime depends upon how the scope is defined in the definition object and where it lives in the DOM tree.

When the template value is a function, the two parameters available during runtime are tElem and tAttrs, which are the element and attributes that the directive has access to during the compile phase. However, the values of the attributes are pre-interpolated, so they must be hardcoded values in HTML so that significant value can be derived from them. This allows the developer to request dynamic templates with the $http service depending on the attributes set on the element and the values that are taken from the app's current state.

Tip

Inside the template function, any variable that has been Dependency Injected into the directive's functional context will be available and can be utilized to determine conditions.

The following is an example of an Animated Menu directive that utilizes the template function just to showcase its syntax:

angular.module('Menus', [])
  .directive('bbAnimatedMenu', [function(){
    return{
      restrict: 'EA',
      replace: true,
      template: function(tElem, tAttrs){
        return '<div class="animated-menu animated-menu- vertical animated-menu-left">' +
          '{{hello}}'  +
    '</div>';
        },
        link: function(scope, elem, attrs){
          scope.showMenu = function() {};
        }
     };
}]) 

.controller('menuCtrl', function($scope){
    $scope.hello = 'Hello';
    $scope.hello2 = ' World';
})

// This directive is Called in HTML with this syntax
<div bb-animated-menu test="{{hello}}"><div>

Inside the template function, the tElem and tAttrs objects are exactly equal to what they are in the static DOM. This is because the template function is run before the compile phase even happens, and there has been no interpolation on the hello attribute. The final result in the menu will be a div parameter, with the correlating CSS classes and some text that reads Hello World. An example of this function is available at http://jsfiddle.net/joshkurz/qJfa4/3/.

Note

It is apparent that this function could be cleaned up to some extent, and that is exactly what the templateUrl field is for.

TemplateUrl

The TemplateUrl option is a string or a function that returns a string, which maps to a template located in templateCache or needs to be requested via HTTP. This is the field that should be utilized instead of the template when writing directives in production. Utilizing the templateUrl field allows directives to be much more readable, and the same goes for the templates themselves.

Once the template is fetched, the exact same rules apply as the template field. The templateUrl function utilizes the same tElem and tAttrs objects as the template option. The benefit to using templateUrl is the added readability.

Using templateUrl as a function is very important and allows for directives to be able to use dynamic templates based off of attribute values. This is a very declarative approach to programming and writing directives. The benefits are grand and allow for a flexible directive that can be used in many different ways.

Replace

The replace field is a Boolean that has a default value of false. If replace is set to true, then the element will replace the defining element in the DOM during the compile phase. This is useful when the defining directive is present just for syntactical purposes and the template holds all of the real DOM that the view should hold. If replace is false, then the template element will just be appended as a child of the defining element. false is the default value of replace.

Transclude

The Transclude option is a Boolean or a string that has a default value of false. If transclude is set to true, then AngularJS will copy the element's child elements from the DOM before compiling the template to store a link function for later use. If transclude is set to 'element', then AngularJS will compile the entire element and store its link function for later use.

There are a few other directive options that work well with transclusion. Earlier, we alluded to a terminal being useful when transclusion is used by directives. This is because any directive that uses the terminal option would not allow its children or other directives with a lower priority to be compiled. The transclude function forces them to be compiled, but their link functions are used at a specified time rather than upon directive initialization. Transclude also works with the replace option in a similar manner. If the defining directive uses replace, then its original contents will be lost without transclusion.

The transclude process takes place during the directive's compile phase. When compiling a node, if a transclusion option has been set, then either that node's specified elements or its child elements will be passed into another compile function call. This compile function returns a separate link function that is passed into the directives link function as the fifth parameter. The link function is prebound to a new transclusion scope, which is a child scope of the defining scope. Even though the transclude function is prebound to the correct scope, it can be overwritten when the transclude function is called. A normal use case of passing a different scope is to create a new child scope from the directive's scope and pass that new scope in.

Let's add some transclusion to the animated-menu directive and see how we can access its values, as shown in the following code snippet:

app.directive('bbAnimatedMenu', function(){
  return{
    restrict: 'EA',
    replace: true,
    transclude: true,
    templateUrl: 'animatedMenu.tpl.html',
    link: function(scope, elem, attrs, nullCtrl, transcludeFn){
      //setting a variable that represents the cloned element,
      //which is where the original contents of this element were
      //before the compile function ran and generated the new
      //templated element.
      var clonedElement = transcludeFn(function(clone){
        return clone;
      });
      elem.append(clonedElement);
      scope.showMenu = function() {};
    }
  };
});

Now that we are transcluding the menu, it can semantically create content that was relevant to the defining scope. This allows for the fulfillment of requirements that call for generating the DOM based upon $parent level scope objects that are not accessible inside of the directive through suggested means. This also keeps original bindings intact through prototypal inheritance. Transcluding this way is also great for readability, and it keeps the templates limited to content related to specific directives. A live demo is available at http://jsfiddle.net/joshkurz/qJfa4/4/.

There is also a directive called ngTransclude that allows the developer to insert the transcluded DOM into a specified place in the template of a directive. The ngTransclude directive is useful when a simple clone of the original contents is needed inside a new templated element.

A simple example of ng-transclude being used on a directive is shown in the following code snippet:

angular.module('DemoExamples').directive ('demoTransclusion', function(){
    return{
        restrict: 'EA',
              transclude: true,
        template: '<div class="container">' +
                  '<div ng-transclude><div>' +
                  '</div>',
        // simplified for readability
    };
}]);

Now, the compiled and linked DOM will be inserted into the div element where the ng-transclude attribute is set. This is great for clean, easy cloning when there are no additional changes that need to be made to the original contents.

Compile

The Compile option is a function that returns either an object or a single post-link function. If the returned item is an object, then it can contain two fields, which are either pre or post. The purpose of the compile function is to offer optimization techniques that can be run before the directive's DOM is linked to a scope. The compile function does not have access to any scope. This means that any attribute that is read during the compile phase will be the pre-interpolated value.

Note

The life cycle of the compile function depends on its definition, but the order in which it is called depends on the priority set by the definition object. If a directive has a higher priority, then its compile function will be run before those that have a lower compile value and that are set on the same element.

This directive option is not to be confused with the $compile service that AngularJS offers in its API as a Dependency Injected variable. The difference is that this compile function does not traverse the given element to register any associated directives. The compile option's main purpose it to return a variable link function that defines how the directive should be linked to the DOM and scope.

The compile function is the basis of all core and custom directives. Normally, when creating directives, there is no need to utilize this field because its returned value is exactly the same as the link function would be.

An example compile function for the animated menu is shown in the following code snippet:

compile: function(tElem, tAttrs){
     
          return function(scope, elem, attrs, nullCtrl, transcludeFn){
            var clonedElement = transcludeFn(function(clone){
              return clone;
            });
            elem.append(clonedElement);
            scope.showMenu = function() {};
          }

}

Note

The tElem and tAttrs objects are template values that are preinterpolated. Only template alterations should be made inside the context of the compile functions. DOM manipulation, event registration, and compiling should be done in the returned link function.

The compile function returns the exact same link function that the animated menu used in the previous transclude example. The difference is that now the link function has access to the templated attributes and elements. This opens up many options for different use cases, such as directives with variable link functions, because the template or the application state could determine which link function should be returned.

Another example of the compile function utilizing the pre and post objects is shown in the following code snippet:

compile: function(tElem, tAttrs){
           
    return {
        pre: function(scope, elem, attrs, controller, transcludeFn){
          scope.showMenu = function() {
            elem.toggleClass('animated-menu-push-toright' );
          };
        },
        post: function(scope,elem,attrs,controller,transcludeFn){
         var clonedElement = transcludeFn(function(clone){
            return clone;
          });
          elem.append(clonedElement);

          scope.showMenu();
        }
    }
}

The pre and post object fields, used in this version of the animated menu's compile function, both return link functions. The difference is that the pre function will always be run before any post link function that is defined on the animatedMenu directive. It is also not safe to perform DOM manipulation in the pre link function because the post link function will fail to locate specific elements.

Another important factor about the compile function is how it performs some of its optimization techniques. Any directive that uses transclude, such as ngRepeat, will compile directives only once. The link function will be run many times, which proves that placing logic in the compile phase, that does not need scope interaction, is faster.

Link

The link definition option can either be a function or an object. If the link definition is a function, then it is considered a postlink function. If the link definition is an object, then it can contain pre or post object keys that map to individual link functions that are run in a synchronous order. The post link function is the only place where DOM manipulation, which depends on scope variables, should occur in an AngularJS application. This is so all of the elements can be precompiled and their scope linked to DOM data. Doing so will ensure that all elements are available at the correct time and that the specific directive logic has been performed as intended.

The link function is synonymous with the compile function's returned object. Most of the examples that we have used in this chapter so far contain link functions. This is because most directives only contain link function definitions. The link function is the last function to be called from a directive's definition. If the element contains multiple directives, then the link function is considered a composite link function that holds the data set by all of the other directives on the given element and its child elements.

Directives are collected recursively in AngularJS. Their link functions are run in the opposite order that they are compiled in. AngularJS traverses the DOM, starting at the root, and compiles every directive it finds. Then, as AngularJS comes back up the tree, each post link function is run. The parent directive will only run its post link function once all of the child directives have already done so. If a directive was compiled first, then its DOM and scope will be linked last.

Almost all of a directive's logic will be placed inside of the link function. This is because it is the safest place to perform DOM manipulation as we can ensure that all of the directive's child elements are compiled and linked. It is common to find directives with fat link functions. Although this is not a wonderful technique, it is used in the community often.

Tip

The bbStopLight directive creates an SVG element based on the width and height of the defining element and watches the parent container for state changes.

Let's take a look at a detailed version of the bbStopLight link function in the following code snippet:

link: function(scope,element,attrs,stopLightCtrl) {
         
            var context = element[0].getContext('2d');
            
            scope.options =  angular.extend({ 
              attrsState: attrs.state,
              height: element[0].height,
              width: element[0].width
            },stopLightCtrl.options);

            function getStopLightState(){
              return stopLightCtrl.options.state;
            }
            
            svgService.setUpStopLight(context,scope.options);
          
            scope.$watch(getStopLightState, function(newV,oldV){
              if(newV !== oldV){
                svgService.changeColor(context,scope.options.attrsState,newV);
              }
            });
}

The link function for the bbStopLight directive consists of a couple of different functions. First, a context variable is set as a canvas element. This will be used as a parameter in the coming functions, so we can perform all of the SVG-related tasks we need to. Note that the specific isolated scope has an object value set to it, which are the options for the parent container and some extended information that is specific to an instance of the bbStopLight directive.

Once we have the context and the options, we can initialize the SVG circle that represents the bbStopLight directive. The svgService has been injected to the context of this directive so that we can call its specific SVG functions. This is so we can keep the cyclomatic complexity down inside of the link function.

Tip

Keeping the cyclomatic complexity low in link functions is especially important and easily achievable by utilizing services and factories. This also opens up more room to test individual functions that may not have been testable if left as private functions in the link function's context.

Now that we have our bbStopLight directive set, we need to set its color. The color should be #ccc or the color of the current state of the container only if it matches the isolated bbStopLight. This is done by setting up a watch on the scope and calling getStopLightState on every digest. During the first digest, this function will run because the comparison of newV and oldV will be true because oldV will equal undefined on the first digest. Once svgService.changeColor runs, our bbStopLight directive will be the color that it is supposed to be. Then, every time the interval fires in the controller of bbStopLigthContainer, this watch will be fired and the svgService.changeColor method will be run, subsequently changing the color of bbStopLight to its correct state.

The following output shows the change in the color of the traffic light:

Tip

The link functions are where the bulk of the logic for a specific directive gets located. The reason for this is that in the post link function, all of the compiled information about a directive and its child elements is known.

Wrapping up definition objects

The definition object is a set of instructions used when AngularJS compiles and links the DOM against a specified scope. The whole purpose of a definition object is to separate out the logic behind building DOM structures that live in the AngularJS digest cycle. Once completely linked, these DOM structures offer a versatile and dynamic solution to manipulating the view.

Each individual definition field has a distinct purpose and subsequently should be used if the requirements for the given directive call for it. This is not a commonly used set of definition object fields. The use of each field depends widely on individual implementation. Because of this, it is good to know when and where to use each definition field and how to use the fields in correlation with each other.

 

Summary


We have gone over every definition object option available in detail and the method to utilize these on directives for different purposes. We have successfully created a traffic light that consists of two directives, a directive controller, and a service. We also created an animated menu that utilizes different aspects of the compile definition field to create its link function that is needed to accomplish its tasks.

Another focus of this chapter, was on how and when to use individual definition fields according to specific requirements for individual directives. In every case, the definition fields needed can vary, but there are some very similar concepts that can be reused through almost every directive.

DOM manipulation, which depends on scope variables or child elements, should only be done inside the post-linking function returned by the compile definition. This is to ensure that all of the directive's elements and child elements are compiled, evaluated, and interpolated against the given directive's scope and transcluded scope.

The templates used in a directive can be very complex and should always be put into templateUrl for the best readability possible. The readability is advantageous for both the directive and the template. Also, one of the major advantages of using a templateUrl option is that we can create a declarative directive that uses dynamic templates.

Transclusion should be performed when the contents of a DOM element need to be cloned and attached to the newly linked element. It is possible to transclude just the directive's child elements or the entire directive itself. Transclusion works great with other options such as terminal or replace.

The scope field is very important and should be used correctly. There are three different types of scopes that a directive can have. These scopes have specific purposes and their own set of benefits. The most robust scope is the isolate scope option that allows for the directive to use two-way data.

These directive definition object options offer different ways of accomplishing simple to very advanced tasks. The key to controlling how and when to use these options correctly lays in knowing them in detail and practicing their utilization.

In the next chapter, we will build our first directive. This directive will utilize many of the options we have described in this chapter. We will also go over some different types of testing techniques.

About the Author

  • Josh Kurz

    Josh Kurz is a client-side technician who constantly pushes the realms of frontend technologies by mixing new-age theories and proven Computer Science concepts. He has successfully shown that AngularJS can be used to create some of the fastest, most usable data visualization applications while working at Turner. He also has a true passion for open source code and believes it is one of the reasons for his success. Currently, outside of work, he is practicing to become a black belt in Jiu Jitsu.

    Browse publications by this author

Latest Reviews

(1 reviews total)
Excellent
Book Title
Access this book, plus 7,500 other titles for FREE
Access now