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.

AngularJS Abstractions: Services

At first glance, services in AngularJS are a catch-all destination for any code that doesn’t fall into one of the other primary abstractions in the framework. If it’s not a controller, filter, directive, or model,  it must be a service. One reason to think to think this way is because one can interpret the word “service” to mean anything in software. However, services in Angular have capabilities and behaviors that make them well suited for specific jobs.

Services As Application Helpers

Controllers, applications, and even other services can take a dependency on 0 or more services, meaning services can be a convenient location for code that is used in different places throughout an application. For example, if two controllers both require some algorithmic logic to make a decision, it would be better to place the logic inside a service that both controllers can use than have the code duplicated in two different controllers.

Services As Singletons

AngularJS will manage services to make sure there is only one instance of a service per application. This makes services a convenient storage location for data that needs to stick around (controllers and their models can come and go as views are loaded and unloaded in the DOM).

Services As Communication Hubs

AngularJS provides a framework to achieve a separation of concerns and a the various components have no knowledge of each other. Since services can be injected into almost anything (controllers, directives, filters, and even other services), a service can play the role of a mediator or event aggregator for other components to communicate in a loosely coupled manner.

Services As Injectable Dependencies

Perhaps the biggest reason to put code into a service is because services are injected into other components, meaning components maintain a loose coupling and any particular service can be replaced during a unit test. AngularJS itself provides many services that fall into this category. There is the $http service (for network communication),  the $window service (a wrapper for the global window object), the $log service, and others.

What Isn’t A Service?

There is plenty of vanilla JavaScript code you can write for an AngularJS application that doesn’t have to live inside an AngularJS abstraction. Model and view model definitions, for example, could live outside of any controller or service. Controllers could instantiate models, obviously, and services could hold references and vice versa. My candidates for services generally fall into one of the previous 4 categories, and as models are easy to test without test doubles, they don’t need to be injected and hence don’t need to be considered a service themselves, although it is certainly reasonable to make a service responsible for fetching a model from some unknown data source.

How To Create A Service?

One of the confusing areas of AngularJS revolves around the APIs for  creating a service, since there are no fewer than 6 ways to register a service and the obvious API choice, a method named service, probably shouldn’t be your first choice. The one that can adapt to just about any scenario is the factory method, which when used on a module object looks like something that would define a factory function for the module, but in fact is defining a factory function for a service (another unfortunate point of confusion).

As an example, here is a service to wrap the geolocation APIs of  the browser:

(function () {
 
    var geo = function ($q, $rootScope) {
 
        var apply = function () {
            $rootScope.$apply();
        };
 
        var locate = function () {
            var defer = $q.defer(); 
            navigator.geolocation.getCurrentPosition(
                function (position) {
                    defer.resolve(position);
                    apply();
                },
                function(error) {
                    defer.reject(error);
                    apply();
                });
            return defer.promise;
        };
 
        return {
            locate: locate
        };
    };
 
    geo.$inject = ["$q", "$rootScope"];
 
    angular.module("testApp")
           .factory("geo", geo);
 
}());

The factory method takes a function that AngularJS will invoke to create the service instance. This geo service makes use of the $q service provided by Angular to create promises for the asynchronous delivery of latitude and longitude. Somewhere else in the app, a controller can take a dependency on the geo service and use it to find the user’s position.

(function () {
 
    var TestController = function ($scope, geo) {
 
        geo.locate()
           .then(function (position) {
                $scope.position = position;
            });
    };
 
    TestController.$inject = ["$scope", "geo"];
 
    angular.module("testApp")
           .controller("TestController", TestController);
 
}());

Summary

Services are an important part of any AngularJS application. The built-in services in the framework make code more testable, and custom services can provide an abstraction to make code easier to maintain.

AngularJS Tests With An HTTP Mock

 Let’s look at a controller that likes to communicate over the network.

(function (module) {
 
    var MoviesController = function ($scope, $http) {
 
        $http.get("/api/movies")
            .then(function (result) {
                $scope.movies = result.data;
            });
    };
 
    module.controller("MoviesController",
        ["$scope", "$http", MoviesController]);
 
}(angular.module("myApp")));

If you want a unit test to execute without network communication there are a couple options. You could refactor the controller to use a custom service to fetch movies instead of using $http directly. Then in a unit test it would be easy to provide a fake service that returns pre-canned responses.

Another option is to use angular-mocks. Angular mocks includes a programmable fake $httpBackend that replaces the real $httpBackend service. It is important to note that $httpBackend is not the same as the $http service. The $http service uses $httpBackend to send HTTP messages. In a unit test, we’ll use the real $http service, but a fake $httpBackend programmed to respond in a specific way.

Here is one approach:

describe("myApp", function () {
 
    beforeEach(module('myApp'));
 
    describe("MoviesController", function () {
 
        var scope, httpBackend;
        beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http) {
            scope = $rootScope.$new();
            httpBackend = $httpBackend;
            httpBackend.when("GET", "/api/movies").respond([{}, {}, {}]);
            $controller('MoviesController', {
                $scope: scope,
                $http: $http
            });
        }));
 
        it("should have 3 movies", function () {
            httpBackend.flush();
            expect(scope.movies.length).toBe(3);
        });
    });
});

In the above test, $httpBackend is programmed (with the when method) to respond to a GET request with three objects (empty objects, since the controller in this example never uses the data, but only assigns the response to the model).

For the data to arrive in the model, the test needs to call the flush method. Flush will respond to all requests with the programmed responses. The flush method will also verify there are no outstanding expectations. Writing tests with expectations is a little bit different.

describe("myApp", function () {
 
    beforeEach(module('myApp'));
 
    describe("MoviesController", function () {
 
        var scope, httpBackend, http, controller;
        beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http) {
            scope = $rootScope.$new();
            httpBackend = $httpBackend;
            http = $http;
            controller = $controller;
            httpBackend.when("GET", "/api/movies").respond([{}, {}, {}]);
        }));
       
        it('should GET movies', function () {
            httpBackend.expectGET('/api/movies');
            controller('MoviesController', {
                $scope: scope,
                $http: http
            });
            httpBackend.flush();
        });
    });
});

The expectation in the above code is registered with the expectGET method. When the test calls flushflush will fail the test if the controller did not make all of the expect calls. Of the two tests in this post, I prefer the first style. Testing with expectations is the same as interaction testing with mocks, which I’ve learned to shy away from because interaction testing tends to be brittle.