Credit to devdocs.io

angularjs 1.4

angularjs

Improve this DocCreating Custom Directives

Note: this guide is targeted towards developers who are already familiar with AngularJS basics. If you're just getting started, we recommend the tutorial first. If you're looking for the directives API, we recently moved it to $compile.

This document explains when you'd want to create your own directives in your AngularJS app, and how to implement them.

What are Directives?

At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS's 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.

Angular comes with a set of these directives built-in, like ngBind, ngModel, and ngClass. Much like you create controllers and services, you can create your own directives for Angular to use. When Angular bootstraps your application, the HTML compiler traverses the DOM matching directives against the DOM elements.

What does it mean to "compile" an HTML template? For AngularJS, "compilation" means attaching directives to the HTML to make it interactive. The reason we use the term "compile" is that the recursive process of attaching directives mirrors the process of compiling source code in compiled programming languages.

Matching Directives

Before we can write a directive, we need to know how Angular's HTML compiler determines when to use a given directive.

Similar to the terminology used when an element matches a selector, we say an element matches a directive when the directive is part of its declaration.

In the following example, we say that the <input> element matches the ngModel directive

<input ng-model="foo">

The following <input> element also matches ngModel:

<input data-ng-model="foo">

And the following element matches the person directive:

<person>{{name}}</person>

Normalization

Angular 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:

Best Practice: Prefer using the dash-delimited format (e.g. ng-bind for ngBind). If you want to use an HTML validating tool, you can instead use the data-prefixed version (e.g. data-ng-bind for ngBind). The other forms shown above are accepted for legacy reasons but we advise you to avoid them.

Directive types

$compile can match directives based on element names, attributes, class names, as well as comments.

All of the Angular-provided directives match attribute name, tag name, comments, or class name. The following demonstrates the various ways a directive (myDir in this case) 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>
Best Practice: Prefer using directives via tag name and attributes over comment and class names. Doing so generally makes it easier to determine what directives a given element matches.
Best Practice: Comment directives were commonly used in places where the DOM API limits the ability to create directives that spanned multiple elements (e.g. inside <table> elements). AngularJS 1.2 introduces ng-repeat-start and ng-repeat-end as a better solution to this problem. Developers are encouraged to use this over custom comment directives when possible.

Creating Directives

First let's talk about the API for registering directives. Much like controllers, directives are registered on modules. To register a directive, you use the module.directive API. module.directive takes the normalized directive name followed by a factory function. This factory function should return an object with the different options to tell $compile how the directive should behave when matched.

The factory function is invoked only once when the compiler matches the directive for the first time. You can perform any initialization work here. The function is invoked using $injector.invoke which makes it injectable just like a controller.

Best Practice: Prefer using the definition object over returning a function.

We'll go over a few common examples of directives, then dive deep into the different options and compilation process.

Best Practice: In order to avoid collisions with some future standard, it's best to prefix your own directive names. For instance, if you created a <carousel> directive, it would be problematic if HTML7 introduced the same element. A two or three letter prefix (e.g. btfCarousel) works well. Similarly, do not prefix your own directives with ng or they might conflict with directives included in a future version of Angular.

For the following examples, we'll use the prefix my (e.g. myCustomer).

Template-expanding directive

Let's say you have a chunk of your template that represents a customer's information. This template is repeated many times in your code. When you change it in one place, you have to change it in several others. This is a good opportunity to use a directive to simplify your template.

Let's create a directive that simply replaces its contents with a static template:

Notice that we have bindings in this directive. After $compile compiles and links <div my-customer></div>, it will try to match directives on the element's children. This means you can compose directives of other directives. We'll see how to do that in an example below.

In the example above we in-lined the value of the template option, but this will become annoying as the size of your template grows.

Best Practice: Unless your template is very small, it's typically better to break it apart into its own HTML file and load it with the templateUrl option.

If you are familiar with ngInclude, templateUrl works just like it. Here's the same example using templateUrl instead:

templateUrl can also be a function which returns the URL of an HTML template to be loaded and used for the directive. Angular will call the templateUrl function with two parameters: the element that the directive was called on, and an attr object associated with that element.

Note: You do not currently have the ability to access scope variables from the templateUrl function, since the template is requested before the scope is initialized.
Note: When you create a directive, it is restricted to attribute and elements only by default. In order to create directives that are triggered by class name, you need to use the restrict option.

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

Let's change our directive to use restrict: 'E':

For more on the restrict property, see the API docs.

When should I use an attribute versus an element? Use an element when you are creating a component that is in control of the template. The common case for this is when you are creating a Domain-Specific Language for parts of your template. Use an attribute when you are decorating an existing element with new functionality.

Using an element for the myCustomer directive is clearly the right choice because you're not decorating an element with some "customer" behavior; you're defining the core behavior of the element as a customer component.

Isolating the Scope of a Directive

Our myCustomer directive above is great, but it has a fatal flaw. We can only use it once within a given scope.

In its current implementation, we'd need to create a different controller each time in order to re-use such a directive:

This is clearly not a great solution.

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:

Looking at index.html, the first <my-customer> element binds the info attribute to naomi, which we have exposed on our controller's scope. The second binds info to igor.

Let's take a closer look at the scope option:

//...
scope: {
  customerInfo: '=info'
},
//...

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.
Note: These =attr attributes in the scope option of directives are normalized just like directive names. To bind to the attribute in <div bind-to-this="thing">, you'd specify a binding of =bindToThis.

For cases where the attribute name is the same as the value you want to bind to inside the directive's scope, you can use this shorthand syntax:

...
scope: {
  // same as '=customer'
  customer: '='
},
...

Besides making it possible to bind different data to the scope inside a directive, using an isolated scope has another effect.

We can show this by adding another property, vojta, to our scope and trying to access it from within our directive's template:

Notice that {{vojta.name}} and {{vojta.address}} are empty, meaning they are undefined. Although we defined vojta in the controller, it's not available within the directive.

As the name suggests, the isolate scope of the directive isolates everything except models that you've explicitly added to the scope: {} hash object. This is helpful when building reusable components because it prevents a component from changing your model state except for the models that you explicitly pass in.

Note: Normally, a scope prototypically inherits from its parent. An isolated scope does not. See the "Directive Definition Object - scope" section for more information about isolate scopes.
Best Practice: Use the scope option to create isolate scopes when making components that you want to reuse throughout your app.

Creating a Directive that Manipulates the DOM

In this example we will build a directive that displays the current time. Once a second, it updates the DOM to reflect the current time.

Directives that want to modify the DOM typically use the link option to register DOM listeners as 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 Angular 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.
For more details on the <