AngularJS Custom Directives

by Nicholas Boll

Who Am I?

  • @NicholasBoll
  • nicholas.boll@gmail.com
  • I'm a Software Engineer at LogRhythm
  • I've been using AngularJS for about 6 months

What are directives?

If you use Angular, you already use Directives (ex: ng-model)

{{ message }}

Why make your own?

Angular gives you many powerful directives

jQuery, right?

Directives are:

  • A way to expand HTML to be more declarative
  • Components or behavior modifiers
  • How AngularJS allows you to hook into the DOM
  • Complicated

Creating a directive:

Config object

restrict

"E"
Element
"A"
Attribute
"C"
Class
"M"
Comment

WTF?

CSS classes? Comments?!

Comment directives

Mostly used internal for directives like ng-repeat

This is because the Browser parses HTML into DOM and then Angular gets to it

Ex: my-directive creates a td

Class directives

I don't suggest using classes as directives. CSS classes are style-hooks and not obvious that behavior is being added.

Note:

IE8 can't parse Element directives without the help of the document.createElement hack. Stick with attribute directives for IE8 support.

priority

Number

Tells the compiler what order to process directives

Higher numbers get processed first

  • {{ movie }}

terminal

Boolean

True means no further compiling will be done of lower priority or child directives

Binding: {{ message }}
Non-binding: {{ message }}

scope

  • Boolean or Object hash
  • Set to true for a new scope
  • Set to Object hash for an isolate scope
  • Scopes have prototypical inheritance

New Scope

Normal scope: {{ message }}, {{ foo }}
New scope: {{ message }}, {{ foo }}

Isolate Scope

=
Two-way binding between parent and local scope (sharing)
@
One-way (parent to local) binding by interpolation. Ex: hello {{name}}
&
Provides a way to execute an expression in the context of a parent scope.
Parent: {{ message }}
One way: {{ oneWay }}
Two way: {{ twoWay }}

controller

  • Actually more like a Directive Controller
  • Should not contain DOM
  • Should contain business logic and writing to local scope
  • Should be unit-testable without DOM
  • Defines an API for directive communication

require

  • String or String[]
  • Require a directive controller from the current or parent element
  • (no prefix) is current element, ^ is parent and ? means optional
  • Angular will pass required controllers as forth argument to linking function

template

  • String or Function(tElement, tAttr)
  • Replaces contents of directive with HTML
  • Migrates attributes and classes from old element

templateUrl

  • String
  • Like template, but loaded from URL
  • Loads asynchronously, suspends compile/link stages
  • Use for dev, $templateCache for prod and testing

replace

  • Boolean
  • true to replace the current element with template (must be 1 root element in template)
  • false to replace contents of current element
I will be replaced I will also be replaced

transclude

  • Boolean
  • Typically used with ng-transclude and an isolate scope
  • Makes it possible to make a widget with an isolate scope, but have transcluded content bound to parent scope

What?

Think of a widget where your template is a picture frame and ng-transclude is the place where the picture goes

This content is transcluded like a picture!

{{ message }}

link

  • Function (scope, iElement, iAttributes, controller, transcludeFn)
  • Object { preLink: Function, postLink: Function}
  • If function, it is a postLink function
  • Use if no compile function is needed (most of the time)
  • controller is a reference or array of references to required controllers (including controller of the directive if applicable)
  • If transclude is true, transcludeFn will be a pre-bound transclude function for cloning (ng-repeat)

preLink

  • Rarely used in practice - actually similar to controller
  • Used when you need to change a scope, but not DOM
  • Angular uses for form and ngInit

postLink

  • Can add DOM listeners and manipulate DOM
  • Most directive code will go here

compile

  • Function (tElement, tAttributes)
  • Used to transform the template DOM
  • Returns a link function or object
  • Notice there is no scope

compile vs link

Think of underscore or Handlebar templates: you compile a string into a template function, then call the template function with data.

Handlebars:

var template = Handlebars.compile('
{{ title }}
'); var html = template({ title: 'My Title' });

Angular:

scope.title = 'My Title';
var linkFn = $compile('
{{ title }}
'); linkFn(scope);

compile vs link

compile is added for performance. It is only run once while link is run for each clone

General Rules

  • Should be easy to use
  • Should be modular and reusable by reducing concern
  • Should only read scope. Controllers write scope
  • Limit functionality to UI and UI behavior
  • Controllers for inter-directive communication (doesn't mess with scope)
  • Remember to clean up: scope.$on('$destroy', ...

API Considerations: Components

  • Components with config (Imperitive)
  • Components with transclude (Declaritive)

Impertitive Component: ng-grid

Declaritive Component: angular-table

Imperative

  • Imperative is nice if you want to control the template
  • Makes for simple examples
  • All functionality/use cases need to be supported in code and configured
  • Much more isolated functionality

Declarative

  • Declarative is nice if you want components to be customized
  • More daunting API to get started
  • Gives consumers of your directive API building blocks to create apps
  • Gives consumers a lot of power and flexibility through transclusion

Tooltips example

Plain HTML

Hover over me!

Angular Tooltip

Hover over me!
angular.module('presentation').directive('nbTooltip', function ($document, $compile) {
  return {
    scope: true,
    link: function (scope, element, attrs) {
      var tooltip = null;
      var tooltipLinker = $compile('
{{ text }}
'); function showTooltip (event) { tooltip = tooltipLinker(scope); scope.text = attrs.nbTooltip; scope.$digest(); tooltip.css({ top: event.y + 5 + 'px', left: event.x + 5 + 'px' }); $document.find('body').append(tooltip); } function hideTooltip (event) { tooltip.remove(); } element.on('mouseover', showTooltip); element.on('mouseout', hideTooltip); scope.$on('$destroy', function () { element.off('mouseover', showTooltip); element.off('mouseout', hideTooltip); }); } }; });

Tooltip example

  • Shows use of separate compile and link steps by using the $compile service (used when compiling bootstrapped apps)
  • Shows how to add Angular compiled DOM after app is bootstrapped
  • Shows cleanup through $destroy event

Example Directive

The directive used to power the examples in this presentation

angular.module('directives.example', []).directive('nbExample', function ($compile) {
  return {

    // create isolate scopes for examples
    scope: {},

    // Only match to element or attribute
    restrict: 'EA',

    // don't process any other directive
    terminal: true,

    // priority higher than other directives
    priority: 1000,

    // compile function - this will return a linking function
    compile: function (templateElement, templateAttributes) {

      /* compile the template. This will return a function that will create
       * real DOM within a scope - kind of like underscore templates return
       * a function that will operate on data
       * We need more control for this template than providing a template
       * attribute to the directive
       */
      var template = $compile('

Example:

Output:

'); // Get the contents of the directive as a string for later use var example = templateElement.html(); // Remove leading whitespaces and replace with a single return per line var leadingWhitespace = example.match(/^\s+/)[0]; example = example.replace(new RegExp(leadingWhitespace, 'g'), "\n").replace("\n", ''); templateElement.empty(); // return the linking function return function link (scope, element, attrs) { // evaluate the template using a scope var html = angular.element(template(scope)); html.find('code').addClass(attrs.language).text(example); html.find('div').append($compile(example)(scope)); element.append(html); } } }; });

Questions?