AngularJS Abstractions: Directives

Directives are one of the lynchpins that make AngularJS work, and also make the framework nice to work with.

Directives are the attributes Angular searches for in the markup when the DOM is loaded, attributes like ng-app, ng-controller, and ng-click. You can also use directives to create custom elements, like <my-widget>, but doing so depends on how much value you place in having your raw source validated by an HTML validator.

If validation is high on your list, there are HTML 5 compliant approaches to using directives, including data- prefixing attribute directives (use data-ng-controller instead of ng-controller, for example, and Angular still works).

What Are They Really?

From an abstraction perspective, directives form an anti-corruption layer between a model and a view. Directives are where you can listen for changes in a model value and then go manipulate the DOM. Directives allow models to change the content and presentation of a view without the model getting bogged down by the DOM manipulation code. The model stays clean, testable, and plain.

Here’s an example of a custom “myShow” directive(note that AngularJS already has an ngShow directive to do this job, but this is an easy example to understand).

The purpose of the myShow attribute is to show or hide a DOM element based on the truthiness of a model value.

First, the markup:

<div data-ng-controller="TestController">
     
    <div data-my-show="model.visible">
        {{model.message}}
    </div>
             
    <button data-ng-click="model.toggleVisible()">Click me!</button>
 
</div>

Notice the data-my-show attribute on the first inner div. The attribute value is set to “model.visible”.

Here is the controller and model for the scenario:

(function () {
 
    var model = {
        visible: false,
        message: "Surprise!",
        toggleVisible: function () {
            this.visible = !this.visible;
        }
    };
 
    var TestController = function ($scope) {
        $scope.model = model;
    };
 
    TestController.$inject = ["$scope"];
 
    angular.module("testApp")
           .controller("TestController", TestController);
 
}());

All that’s needed now is the code for the my-show directive:

(function () {
 
    var myShow = function() {
        return {
            restrict: "A",
            link: function(scope, element, attrs) {
                scope.$watch(attrs.myShow, function(value) {
                    element.css("display", value ? "" : "none");
                });
            }
        };
    };
 
    angular.module("testApp")
           .directive("myShow", myShow);
}());

The naming conventions applied by AngularJS allow the directive registered as “myShow” to be applied in markup as data-my-show, as well as my-show and a few other variations. 

The directive itself is setup by a function that returns a “directive definition object” (let’s call it the DDO). The DDO has to have a few members in place for the directive to work, but this sample only uses a couple members: restrict and link.

The restrict property specifies where the directive is valid (E for element, A for attribute, C for class, M for comment). Since the default for restrict is “A”, this property could be omitted.

The link function is responsible for making things happen by setting up watches to listen for changes in the model.  Angular invokes the link function and gives it the model (scope), the element (effectively a jQuery wrapped reference to the DOM element), and an attrs parameter containing the attributes of the element. Asking for attrs.myShow will return “model.visible”, since that is the attribute value specified in the markup.

The link function in this example uses scope.$watch to listen for changes in the specified model value. $watch is a good topic for a future post, but for now we can say if the model changes it’s visible property from false to true, the framework invokes the listener function passed as the second parameter to $watch.

The listener function in this sample sets the display property of the element to an empty string or “none” to toggle visibility of the element. The value parameter in the function is the recently changed model value, the value the directive set up to $watch.

Where Are We?

This sample doesn’t show the full capabilities of a directive, which can also load additional markup from a string or a URL, as well as other nifty features.  We’ll cycle back to more advanced features in a future post.

Although custom directives can require  more code in a project compared to using DOM manipulation inside a controller, the advantage of having a directive in place is in keeping the models and controllers simple. Plus, once written, most directives can be parameterized and reused in different places and multiple applications.