Deep Dive

into

Custom Directives


Alternate Title:
I've always wanted to know what "transclusion" means


Links for Later


AngularJS Developer Guide:


Some useful directive libraries:


AngularUI:

AngularStrap:

Bad Directive Joke


What did the directive say after getting cut out of its parents' will?

"I'm isolated"



Prerequisites



Familiarity with these Angular terms:

  • Scope
  • Controller
  • Dependency Injection

Angular's Built-in Directives

  • ng-show
  • ng-hide
  • ng-click
  • ng-class
  • ng-style
  • ng-switch
  • ng-repeat
  • ng-animate
  • And many more...

Naming Your Directives


  • Use a unique prefix
    • Avoids clashing with others
    • Easier for readers to identify
  • Don't use "ng-" for your custom prefix
  • Common convention: two letters
    • The AngularUI project uses "ui-"
    • At HireVue, we use "hv-"
    • For this talk, I'm using "my-"
  • 1296 prefixes ought to be enough for anyone ;-)

ng-show

Usage:
 <div ng-show="showThis">...</div>
Code:
directive('ngShow', ['$animate', function($animate) {
  return function(scope, element, attr) {
    scope.$watch(attr.ngShow, function(value){
      $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element,
                                                              'ng-hide');
    });
  };
}]);

Naming convention:

   HTML:          ng-show
   JavaScript:   ngShow

ng-class

Usage:

 <div ng-class="myClass()">...</div>

Code: (modified slightly for brevity and clarity)

directive('ngClass', function() {
  return {
    restrict: 'A',
    link: function(scope, element, attr) {
      var oldVal;

      scope.$watch(attr.ngClass, ngClassWatchAction, true);

      attr.$observe('class', function(value) {
        ngClassWatchAction(scope.$eval(attr.ngClass));
      });

      function ngClassWatchAction(newVal) {
        if (selector === true || scope.$index % 2 === selector) {
          var newClasses = flattenClasses(newVal || '');
          if(!oldVal) {
            attr.$addClass(newClasses);
          } else if(!equals(newVal,oldVal)) {
            attr.$updateClass(newClasses, flattenClasses(oldVal));
          }
        }
        oldVal = copy(newVal);
      }

      function flattenClasses(classVal) {
        // snipped for brevity
      }
    }
  };
});

When to use directives?


  • If you want a reusable HTML component
  • <my-widget>
  • If you want reusable HTML behavior
  •  <div ng-click="..."> 
  • If you want to wrap a jQuery plugin
  •  <div ui-date></div>
  • Almost any time you need to interface with the DOM

Getting Started

Create a module for your directives:
angular.module('MyDirectives', []);
Load the module in your app:
angular.module('MyApp', ['MyDirectives']);
Register your directive:
angular.module('MyDirectives').
directive('myDirective', function() {
  // TODO Make this do something
});
Use your directive in your HTML:
 <div my-directive>...</div>



Your First Directive


How about a directive that extends a text <input> to automatically highlight its text when the user focuses it?

 <input type="text" select-all-on-focus />


Your First Directive

angular.module('MyDirectives').
directive('selectAllOnFocus', function() {
  return {
    restrict: 'A', // more on this soon
    link: function(scope, element) {
      element.mouseup(function(event) {
        event.preventDefault();
      });
      element.focus(function() {
          element.select();
      });
    }}});

Note: The link() function arguments are positional, not injectable.

Let's use it


<input type="text" ng-model="name" select-all-on-focus />
<input type="text" ng-model="age" select-all-on-focus />


Quiz:


How many times will the link() function be called?

What about the directive() function?

Bonus points: What about the compile() function?
(not shown here)

Restrict

Attributes only:
 restrict: 'A'
 <div my-directive></div>
      When to use: Behavioral HTML, like ng-click
Elements only:
 restrict: 'E'
 <my-directive></my-directive>
      When to use: Component HTML, like a custom widget

Weird ones:  (rarely used in practice)
  'C' for classes and 'M' for comments
  Combine them:
 restrict: 'AE'

Simple Widget Example


Let's make a simple widget:
<my-social-buttons></my-social-buttons>

We want this to expand to:
<div>
  <a href="x"><i class="fa fa-facebook"></i></a>
  <a href="y"><i class="fa fa-twitter"></i></a>
  <a href="z"><i class="fa fa-google-plus"></i></a>
</div>


Simple Widget Example



directive('mySocialButtons', function() {
 return {
  restrict: 'E',
  template:
   '<div>' +
   '  <a href="x"><i class="fa fa-facebook"></i></a>' +
   '  <a href="y"><i class="fa fa-twitter"></i></a>' +
   '  <a href="z"><i class="fa fa-google-plus"></i></a>' +
   '</div>'
  }
});

New Concept: Templates

Templates

Two options for specifying the template:

  • template Define the HTML in JavaScript
  • templateUrl Use a URL to an HTML partial

Tip: You can avoid the round trip to the server with:
<script type="text/ng-template"
        id="/partials/my-social-buttons.html">
 <div>
   <a href="x"><i class="fa fa-facebook"></i></a>
   <a href="y"><i class="fa fa-twitter"></i></a>
   <a href="z"><i class="fa fa-google-plus"></i></a>
 </div>
</script>

How Angular downloads templates:
 $http.get(templateUrl, {cache: $templateCache})

Interactive Widget Example


Let's make a custom search box

 <my-searchbox
  search-text="theSearchText"
  placeholder="Please search now"
  ></my-searchbox>

We want it to appear like this in the browser


Custom Widget Example


angular.module('MyDirectives').
directive('mySearchbox', function() {
  return {
    restrict: 'E',
    scope: {
      searchText: '=',
      placeholder: '@',
      usedLucky: '='
    },
    template:
      '<div>' +
      ' <input type="text" ng-model="tempSearchText" />' +
      ' <button ng-click="searchClicked()">' + 
      '   Search' +
      ' </button>' +
      ' <button ng-click="luckyClicked()">' + 
      '   I\'m feeling lucky' +
      ' </button>' +
      '</div>'
  }
});

New concept: Isolate Scope

Isolate Scope

By default, a directive does not have its own scope. 

Example: ng-click

But you can give your directive its own scope like this:

scope: {
  someParameter: '='
}

Or like this:
scope: true

Important: It does not inherit from the enclosing scope.

Isolate Scope


Why can it not inherit from the enclosing scope?

  • To keep a clean interface between the directive and the caller
  • To prevent the directive from (easily) accessing the caller's scope
  • To encourage directive reuse
  • To discourage guilty knowledge about the directive's environment
  • To make the directive testable in isolation

Scope Parameters

Each scope parameter can be passed through HTML attributes:
scope: {
  param1: '=', // two-way binding    (reference)
  param2: '@', // one-way expression (top down)
  param3: '&'  // one-way behavior   (bottom up)
}
Examples:
<div my-directive
   param1="someVariable"
   param2="My name is {{theName}}, and I am {{theAge+10}}."
   param3="doSomething(theName)"
>




Finishing the Custom Widget


directive('mySearchbox', function() {
  return {
    restrict: 'E',
    scope: {
      searchText: '=',
      placeholder: '@',
      usedLucky: '='
    },
    template: /* snip */,
    link: function(scope, element, attrs) {
      scope.searchClicked = function() {
        scope.searchText = scope.tempSearchText;
        scope.usedLucky = false;
      }
      scope.luckyClicked = function() {
        scope.searchText = scope.tempSearchText;
        scope.usedLucky = true;
      }
    }
  }
});

HTML Attributes





There are other ways to pass data to your directives...

attrs

The attrs object gives read/write access to HTML attributes:
directive('myDirective', function() {
  return {
    link: function (scope, element, attrs) {
      var options = scope.$eval(attrs.myDirective)
Usage:
function MyController($scope) {
  $scope.someVariable = 42
<div my-directive="{foo: 1, bar: someVariable}">
options will contain this JavaScript object:
{foo: 1, bar: 42}

attrs


Use attrs.$set() to change HTML attribute values

Use $attrs.observe() to be notified
when HTML attributes change

Compile and Link


Compile
Convert an HTML string into an Angular template.

Link
Insert an Angular template into the DOM with a scope as context.


    Compile and Link

    Handlebars uses a similar concept to Angular's:

    var template = Handlebars.compile('<div>...</div>');
    var html     = template({foo: 1, bar: 2});

    In this context, template is a lot like a link function.

    The difference:

    Handlebars' link function returns a string.

    Angular's link function creates DOM at a specified location.

    Compile and Link


    Directives that use compile:

    • ng-repeat
    • ng-if
    • ng-switch

    When should I use it?

    • When you need to add/remove DOM elements after link time
    • When you need to reuse a template multiple times

    Compile Example

    Lazy loading expensive DOM

    <div my-lazy-load="readyToShow">
       <!-- Expensive DOM Here -->
    </div>

    How to do this?

    Compile Example

    Lazy loading expensive DOM
    directive('myLazyLoad', function() {
     return {
      transclude: 'element',
      priority: 1200, // changed needed for 1.2
      terminal: true,
      restrict: 'A',
      compile: function(element, attr, linker) {
       return function (scope, iterStartElement, attr) {
        var hasBeenShown = false;
        var unwatchFn = scope.$watch(attr.myLazyLoad, function(value){
          if (value && !hasBeenShown) {
            hasBeenShown = true;
            linker(scope, function (clone) {
             iterStartElement.after(clone);
            });
            unwatchFn();
           }
          });
        }
       }
    });
    New concepts: terminal and priority

    Transclusion


    Think of transcludable directives as a picture frame

    Transclusion

    Lets the caller specify the inner content of your directive

    Tell Angular where to put the caller's content with ng-transclude:

    directive('myDialog', function() {
    return { restrict: 'E', transclude: true, template: '<div class="modal">' + ' <div class="modal-body" ng-transclude></div>' + '</div>' } });


    Transclusion


    This:
    <my-dialog>
     <h1>Some Custom Title</h1>
     <p>Some custom dialog content goes here</p>
    </my-dialog>

    Expands to this:
    <div class="modal">
     <div class="modal-body">
      <h1>Some Custom Title</h1>
      <p>Some custom dialog content goes here</p>
     </div>
    </div>
    


    Built-in Directives that Transclude


    • ng-repeat
    • ng-switch
    • ng-if

    Stuff We Didn't Talk About

    • Multiple directives on the same element
    • How directives drive scope creation
    • transclude:'element'
    • Element directives for standard attributes
      • e.g., "placeholder"

    Deep Dive into custom Directives

    By djsmith

    Deep Dive into custom Directives

    • 14,250