Creating custom directives in AngularJS

AngularJS directives are DOM elements with special markers that tell AngularJS to attach certain behaviour to the element. Commonly you’ll see these markers as an element tag (E) or element attribute (A). These special markers are also known as matching types.

Let’s take a look at a couple of examples:

Element (E) matching type:

In this example, an element tag my-directive is used to reference a directive:

<my-directive></mydirective>

Attribute (A) matching type:

In this example, the directive is referenced using an attribute my-directive on a <div>:

<div my-directive="true"></div>

There are 2 additional matching types you may also come across (but are less common): class names (C), and comments (M). I will not be covering these 2 types, so refer to the AngularJS directive guide for more information.

Defining a directive

A common use for directives is to provide a re-usable template, such as rendering a group of buttons. Let’s look at a simple example consisting of a single button with Bootstrap 4 button styling:

<button type="button" class="btn btn-primary">Click Me</button>

In AngularJS, a directive is defined as a function. This function should return an object containing some properties such as template and scope.

The first property we’ll examine is the template property which can contain HTML to be rendered by the directive. Here is a simple directive to render the button:

function() {
    return {
        template: '<button type="button" class="btn btn-primary">Click Me</button>'
    };
}

Before you can start referencing this directive in your AngularJS application, it must be registered.

Registering a directive

Directives are registered against a module using module.directive by passing the normalized (case-sensitive camelCase) directive name and your function. In the previous example of <my-directive> the normalized name would be myDirective.

Let’s take a look with another quick example. Here, our simple directive is registered against the module myApp using it’s normalized name myDirective:

var app = angular.module('myApp', []);
app.directive('myDirective', function() {
    return {
        template: '<button type="button" class="btn btn-primary">Click Me</button>'
    };
});

This directive can then be referenced in your HTML using my-directive (see below).

Referencing a directive

Directives in the DOM are referenced in lower-case format and may also use certain delimiters such as "-" to replace camelCase. This is required because HTML is case-insensitive. In our example, myDirective can be referenced as an element tag by using my-directive:

<my-directive></mydirective>

Restrict and matching types

When you define a directive it will by default support both E and A matching types. However, you can also specify which matching types your directive supports by using the restrict property.

Specifying restrict: 'EA' is the same as not specifying any types, since EA is the default value.

var app = angular.module('myApp', []);
app.directive('myDirective', function() {
    return {
        restrict: 'EA', // default restrict setting
        template: '<button type="button" class="btn btn-primary">Click Me</button>'
    };
});

To restrict the directive so it only supports element types, specify E:

var app = angular.module('myApp', []);
app.directive('myDirective', function() {
    return {
        restrict: 'E', // element only
        template: '<button type="button" class="btn btn-primary">Click Me</button>'
    };
});

The directive can then only be referenced as an element tag:

<my-directive></mydirective>    <!-- works -->
<div my-directive="true"></div> <!-- does not work -->

Similarily, to cause the directive to only support attribute referencing, use restrict: 'A'.

Scope inheritance

The scope of a directive is controlled with the scope property. Unless specified otherwise, the scope property will have a default value of false and share the scope of it’s parent. For example, if the parent is a controller, the directive will have access to the controller’s $scope variable:

In this example, the controller scope includes a variable btnLabel which is accessible by the directive using {{btnLabel}}:

var app = angular.module('myApp', []);
app.controller("myController", function($scope) {
    $scope.btnLabel = "Click Me";
});
app.directive('myDirective', function() {
    return {
        scope: false, // default scope setting - use parent scope
        template: '<button type="button" class="btn btn-primary">{{btnLabel}}</button>'
    };
});

Since the scope is shared, the directive may also modify $scope.btnLabel and these changes will be visible outside the directive. This can be illustrated by replacing the button with a text-box:

var app = angular.module('myApp', []);
app.controller("myController", function($scope) {
    $scope.myText = "Hello directive";
});
app.directive('myDirective', function() {
    return {
    scope: false, // default scope setting - use parent scope
        template: '<input type="text" ng-model="myText"/>'
    };
});

The value of myText displayed by the below span can be altered by the directive since the directive text-box shares the controller’s scope:

<div ng-app="myApp">
  <div ng-controller="myController">
    <span>{{myText}}</span>
    <my-directive></mydirective>
  </div>
</div>

Alternatively, using scope: true will cause the directive to have it’s own scope inherited from the parent. In the above example, the directive would inherit myText with a value of "Hello directive", but modifying the text-box value in the directive will no longer modify the controller’s myText.

The limitation of both scope: false and scope: true is that you can only reference the directive once within a controller. To overcome this, the directive scope must be isolated.

Isolating scope

The scope of a directive may be fully isolated from it’s parent by setting the directive’s scope property to an object.

var app = angular.module('myApp', []);
app.controller("myController", function($scope) {
    $scope.myText = "Hello directive";
});
app.directive('myDirective', function() {
    return {
        scope: {}, // isolated scope
        template: '<input type="text" ng-model="myText"/>' // myText will be null
    };
});

Isolating the directive scope allows a directive to be referenced multiple times within a controller.

<my-directive></mydirective>    <!-- works -->
<my-directive></mydirective>    <!-- also works -->

Isolated scope also provides the flexibility to define if individual properties of the parent $scope will be bound to the directive, whether they are simply copied (@) or are directly bound (=).

Let’s take a look at a final example using two $scope variables in the parent controller: myCopiedText and mySharedText. Note that we again use normalized names in the directive. The first variable myCopiedText is copied into the directive’s scope using @ and isolated from the controller scope. The second variable mySharedText is bound to the directive variable of the same name using =, similar to what happened for all variables with scope: false.

var app = angular.module('myApp', []);
app.controller("myController", function($scope) {
    $scope.myCopiedText = "Hello directive";
    $scope.mySharedText = "We share some things";
});
app.directive('myDirective', function() {
    return {
        scope: {
            myCopiedText: '@', // copied from controller scope but isolated
            mySharedText: '='  // bound to controller scope
        },
        template: '<input type="text" ng-model="myCopiedText"/>' + 
                  '<input type="text" ng-model="mySharedText"/>' 
    };
});

The controller variables are passed into the directive using attributes (note they are the lowercase, delimited version of the normalized variable name used in the directive scope object):

<div ng-app="myApp">
  <div ng-controller="myController">
    <span>{{myCopiedText}} {{mySharedText}}</span>
    <my-directive my-copied-text="myCopiedText" my-shared-text="mySharedText"></mydirective>
  </div>
</div>

Note above we are using the attribute my-copied-text which maps to the normalized directive variable myCopiedText. If the attribute were different, such as something-else we must specify that in the directive scope mapping, again with the normalized equivalent:

app.directive('myDirective', function() {
    return {
        scope: {
            myCopiedText: '@somethingElse', // map to something-else
            mySharedText: '='  // bound to controller scope
        },
        template: '<input type="text" ng-model="myCopiedText"/>' + 
                  '<input type="text" ng-model="mySharedText"/>' 
    };
});
<div ng-app="myApp">
  <div ng-controller="myController">
    <span>{{myCopiedText}} {{mySharedText}}</span>
    <my-directive something-else="myCopiedText" my-shared-text="mySharedText"></mydirective>
  </div>
</div>

Further reading

The AngularJS documentation provides a detailed explanation of directives and is a valuable resource for any AngularJS developer creating their own directives: https://docs.angularjs.org/guide/directive