Angular JS – Using Interceptors to handle Http errors

angular.module("myApp.directives", []);
angular.module("myApp.services", []);
angular.module("myApp", [
    "myApp.directives",
    "myApp.services"
])
.config(["$httpProvider", function ($httpProvider) {
    $httpProvider.interceptors.push("myInterceptor");
}]);

//------------
// DIRECTIVES
//------------

angular.module("myApp.directives").directive("myDirective", [function () {
    return {
        restrict: "E",
        replace: true,
        template:
            "<div>" +
                "<h2>myDirective</h2>" +
                "<h3 style='color:red'>{{error}}</h3>" +
            "</div>",
        controller: ["$scope", "myService", function ($scope, myService) {
            $scope.error = null;

            myService.callWorkingApi()
                .then(function () {
                    console.log("myDirective: calling working API success");
                })
                .catch(function () {
                    console.log("myDirective: calling working API failed");
                });

            myService.callBrokenApi()
                .then(function () {
                    console.log("myDirective: calling broken API success");
                })
                .catch(function () {
                    console.log("myDirective: calling broken API failed");
                });

            //Here we are catching up the
            //event raised by the interceptor
            $scope.$on("http-error", function (event, args) {
                $scope.error = args.status;
            });
        }]
    };
}]);

//----------
// SERVICES
//----------

angular.module("myApp.services").service("myService",
    ["$http", '$q', function ($http, $q) {
        var _self = this;

        _self.callWorkingApi = function () {
            return $q(function (resolve, reject) {
                $http.get("/json/data.json")
                    .then(function (result) {
                        resolve(result.data);
                    })
                    .catch(function (error) {
                        reject(error);
                    });
            });
        };

        _self.callBrokenApi = function () {
            return $q(function (resolve, reject) {
                $http.get("/json/wrong.json")
                    .then(function (result) {
                        resolve(result.data);
                    })
                    .catch(function (error) {
                        reject(error);
                    });
            });
        };
    }]);

//-----------
// FACTORIES
//-----------

angular.module("myApp").factory("myInterceptor",
    ["$q", "$rootScope", function ($q, $rootScope) {
        return {
            "response": function (response) {
                return response || $q.when(response);
            },
            "responseError": function (response) {
                if (!!response && !!response.config) {

                    //Here we can perform different
                    //actions based on the error code
                    console.log("myInterceptor:", response.config.url,
                        response.status, response.statusText);

                    //Here we are broadcasting an event
                    //for another directive to catch up and
                    //handle (to display a popup for instance)
                    $rootScope.$broadcast("http-error", response);
                }
                return $q.reject(response);
            }
        };
    }]);

And here is the output:

Angular JS – Decorators and Dynamic Templates to handle a multi-language scenario with $locale + Templates Pre-Caching

Let’s imagine a classic multi-language scenario where we want to have the same page in 2 (or many) different languages. What we normally can do is to setup a family of different URIs (“/it/my-page” and “/uk/my-page” for instance) whose output is the same, with the only difference of the title and the internationalization (i18n) script. Based on that we want to handle all the other differencies (in the data or in the HTML) with Angular and we want to do that in the most transparent way.

Here is an example of the output that we want to receive from the server for the IT and the UK page:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>La mia pagina IT</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
    <script src="i18n/angular-locale_it-it.js"></script>
    <script src="MyApp.js"></script>
</head>
<body>
    <div ng-app="myApp">
        <my-directive></my-directive>
    </div>
</body>
</html>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>My UK Page</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
    <script src="i18n/angular-locale_en-gb.js"></script>
    <script src="MyApp.js"></script>
</head>
<body>
    <div ng-app="myApp">
        <my-directive></my-directive>
    </div>
</body>
</html>

As we can see the only differences are in the title and the i18n script that we are injecting. The i18n scripts contain many information about the specific languages such as the days’ and months’ names, the currency, the date format and so on. They can of course include copies as well. The content of these scripts can be accessed via the $locale service. We can get these files by installing the whole i18n module (via bower or npm) or, if we are interested in just few of them, we can download them from the Github repository here: https://github.com/angular/bower-angular-i18n.

Here is our app’s code:

angular.module("myApp.directives", []);
angular.module("myApp", [
    "myApp.directives",
    "ngLocale"
]);

angular.module("myApp.directives").directive("myDirective", ["$locale", function ($locale) {
    return {
        restrict: "E",
        replace: true,
        template:
            "<div>" +
                "<h2>myDirective</h2>" +
                "<h3>{{labels.title}}</h3>" +
            "</div>",
        controller: ["$scope", function ($scope) {
            $scope.labels = $locale.labels;
        }]
    };
}]);

$locale.labels is a custom object that we created inside the i18n script for our copies.

Here is the output:

As expected we have the right translations.


Adding Dynamic Templates

To be able to use translated copies is a good beginning, and the i18n scripts can be used for any kind of string, not just copies, that could be different from country to country, including URIs and any other kind of configuration.

But this is often not enough. For instance some country could require a different HTML. Here is a situation where the Dynamic Templates are useful. In this example we want to use them to display the countries’ flags:

angular.module("myApp.directives").directive("myDirective", ["$locale", function ($locale) {
    return {
        restrict: "E",
        replace: true,
        template: function () {
            if ($locale.localeID == "en_GB") {
 
                //UK-specific
                return "<div>" +
                    "<h2>myDirective</h2>" +
                    "<img src='images/Flag_UK.png' width='200'></img>" +
                    "<h3>{{labels.title}}</h3>" +
                "</div>";
            } else {
 
                //IT-specific (default)
                return "<div>" +
                    "<h2>myDirective</h2>" +
                    "<img src='images/Flag_IT.png' width='200'></img>" +
                    "<h3>{{labels.title}}</h3>" +
                "</div>";
            }
        },
        controller: ["$scope", function ($scope) {
            $scope.labels = $locale.labels;
        }]
    };
}]);

Here is the output:

As expected the template is dynamically selected depending on the i18n localeID.


Adding Decorated Services

Another thing that is often different from country to country is data and logic. Since in a well designed application data is coming from services and logic is implemented in services (not in directives, that should just contain the view-model’s logic) it would be useful if we could get a country-specific instance (if exists) of the services we inject.

In this example we want to use a service to get the countries’ capital name. We’ll use Decorators to return the right instance depending on the i18n localeID. In this way the complexity coming from the multi-language scenario will be transparent for our directive’s logic.

angular.module("myApp.directives", []);
angular.module("myApp.services", []);
angular.module("myApp", [
    "myApp.directives",
    "myApp.services",
    "ngLocale"
])
.config(["$provide", function ($provide) {
 
    //If is there a locale-specific version it will
    //get that, otherwise it will get the default one
    $provide.decorator("capitalService", ["$delegate", "$injector", "$locale",
        function ($delegate, $injector, $locale) {
            if ($injector.has("capitalService_" + $locale.localeID)) {
                return $injector.get("capitalService_" + $locale.localeID);
            }
            return $delegate;
        }
    ]);
}]);


//----------
// SERVICES
//----------
 
//UK-specific
angular.module("myApp.services").service("capitalService_en_GB", [function () {
    var _self = this;
 
    _self.getCapital = function () {
        return "London";
    };
}]);
 
//IT-specific (default)
angular.module("myApp.services").service("capitalService", [function () {
    var _self = this;
 
    _self.getCapital = function () {
        return "Roma";
    };
}]);


//------------
// DIRECTIVES
//------------
 
angular.module("myApp.directives").directive("myDirective", ["$locale", function ($locale) {
    return {
        restrict: "E",
        replace: true,
        template: function () {
            if ($locale.localeID == "en_GB") {
 
                //UK-specific
                return "<div>" +
                    "<h2>myDirective</h2>" +
                    "<img src='images/Flag_UK.png' width='200'></img>" +
                    "<h3>{{labels.title}}</h3>" +
                    "<p>{{labels.capitalIs}} {{capital}}</p>" +
                "</div>";
            } else {
 
                //IT-specific (default)
                return "<div>" +
                    "<h2>myDirective</h2>" +
                    "<img src='images/Flag_IT.png' width='200'></img>" +
                    "<h3>{{labels.title}}</h3>" +
                    "<p>{{labels.capitalIs}} {{capital}}</p>" +
                "</div>";
            }
        },
        controller: ["$scope", "capitalService", function ($scope, capitalService) {
            $scope.labels = $locale.labels;
            $scope.capital = capitalService.getCapital();
        }]
    };
}]);

Here is the output:

As expected, depending on the i18n localeID, i’m getting the right instance of capitalService.


Scanning for Decorators

Decorators are great, but they have to be setup one by one. This is going to produce tons of parboiled code in an enterprise project, definitely not what we want to have to deal with. To solve this problem we would like to be able to auto-generate all of this configuration by scanning for the components and detecting which of them needs a decoration. In order for this to work we’ll follow the convention that we’ll append the i18n localeID to the components that are specific for a language.

The following algorithm works with both services and directives. In this example we’ll use it to decorate the capitalService as before and to replace the Dynamic Template with a decorated directive (decorating a directive should be avoided, only the template should dynamically change. If a directive’s logic needs to change from country to country the variable part should be moved out into a decorated service).

The algorithm has been implemented as an Angular provider, here is the code:

angular.module("myApp.directives", []);
angular.module("myApp.services", []);
angular.module("myApp", [
    "myApp.directives",
    "myApp.services",
    "ngLocale"
])
 
//For providers we must add "Provider" at the end!
.config(["decoratorsProvider", function (decoratorsProvider) {
     decoratorsProvider.bootstrapDecorators(["myApp.directives", "myApp.services"], ["en_GB"]);
}]);
 
 
//------------
// DIRECTIVES
//------------
 
//UK-specific
angular.module("myApp.directives").directive("myDirective_en_GB", ["$locale", function ($locale) {
    return {
        restrict: "E",
        replace: true,
        template:
            "<div>" +
                "<h2>myDirective_en_GB</h2>" +
                "<img src='images/Flag_UK.png' width='200'></img>" +
                "<h3>{{labels.title}}</h3>" +
                "<p>{{labels.capitalIs}} {{capital}}</p>" +
            "</div>",
        controller: ["$scope", "capitalService", function ($scope, capitalService) {
            $scope.labels = $locale.labels;
            $scope.capital = capitalService.getCapital();
        }]
    };
}]);
 
//IT-specific (default)
angular.module("myApp.directives").directive("myDirective", ["$locale", function ($locale) {
    return {
        restrict: "E",
        replace: true,
        template:
            "<div>" +
                "<h2>myDirective</h2>" +
                "<img src='images/Flag_IT.png' width='200'></img>" +
                "<h3>{{labels.title}}</h3>" +
                "<p>{{labels.capitalIs}} {{capital}}</p>" +
            "</div>",
        controller: ["$scope", "capitalService", function ($scope, capitalService) {
            $scope.labels = $locale.labels;
            $scope.capital = capitalService.getCapital();
        }]
    };
}]);
 
 
//----------
// SERVICES
//----------
 
//UK-specific
angular.module("myApp.services").service("capitalService_en_GB", [function () {
    var _self = this;
 
    _self.getCapital = function () {
        return "London";
    };
}]);
 
//IT-specific (default)
angular.module("myApp.services").service("capitalService", [function () {
    var _self = this;
 
    _self.getCapital = function () {
        return "Roma";
    };
}]);
 
 
//-----------
// PROVIDERS
//-----------
 
angular.module("myApp").provider("decorators", ["$provide", function ($provide) {
    var _self = this;
 
    _self.bootstrapDecorators = function (modulesNames, localeIDs) {
        scanDecorators().forEach(createDecorator);
 
        function scanDecorators() {
            var decorators = [];
            modulesNames.forEach(function (moduleName) {
                var module = angular.module(moduleName);
                module._invokeQueue.forEach(function (element) {
                    var elementType = element[1];
                    var elementName = element[2][0];
 
                    localeIDs.forEach(function (localeID) {
                        if (elementName.endsWith("_" + localeID)) {
                            var originalName = elementName.replace("_" + localeID, "");
                            if (!decorators.some(function (decorator) {
                                return decorator.originalName == originalName;
                            })) {
                                decorators.push({
                                    originalName: originalName,
                                    elementType: elementType
                                });
                            }
                        }
                    });
                });
            });
            return decorators;
        };
 
        function createDecorator(decorator) {
 
            //For directives we must add "Directive" at the end!
            var originalName = decorator.elementType == "directive"
                ? decorator.originalName + "Directive"
                : decorator.originalName;
 
            $provide.decorator(originalName, ["$delegate", "$injector", "$locale",
                function ($delegate, $injector, $locale) {
                    var decoratedName = decorator.originalName + "_" + $locale.localeID;
 
                    //For directives we must add "Directive" at the end!
                    decoratedName = decorator.elementType == "directive"
                        ? decoratedName + "Directive"
                        : decoratedName;
 
                    if ($injector.has(decoratedName)) {
                        return $injector.get(decoratedName);
                    }
                    return $delegate;
                }
            ]);
        }
    };
 
    _self.$get = angular.noop;
}]);

Here is the output:


Detecting Dynamic Templates

Now that we are able to automatically setup decorators based on a naming convention it would be cool to be able to do the same with Dynamic Templates. In this example we’ll use a service to automatically get the localized version of a template based on the current localeID. In order for this to work we need to have all the templates available into the Angular template cache, so we’ll be able to scan for them. Downloading all the templates in advance is anyway a good practice, since it gives a more reactive user experience.

To simplify the example we removed the services and the decorators, so we’ll be able to better focus on the new concept. Here is the code:

angular.module("myApp.templates", []);
angular.module("myApp.directives", []);
angular.module("myApp.services", []);
angular.module("myApp", [
    "myApp.templates",
    "myApp.directives",
    "myApp.services",
    "ngLocale"
]);
 
 
//-----------
// TEMPLATES
//-----------
 
//We pre-load all the templates into the template cache. It's possible
//to auto-generate this from the real .html files, for instance via Grunt.
angular.module("myApp.templates").run(["$templateCache", function ($templateCache) {
 
    //UK-specific
    $templateCache.put("/myApp/templates/myDirective_en_GB.html",
        "<div>" +
            "<h2>myDirective</h2>" +
            "<img src='images/Flag_UK.png' width='200'></img>" +
            "<h3>{{labels.title}}</h3>" +
        "</div>");
 
    //IT-specific (default)
    $templateCache.put("/myApp/templates/myDirective.html",
        "<div>" +
            "<h2>myDirective</h2>" +
            "<img src='images/Flag_IT.png' width='200'></img>" +
            "<h3>{{labels.title}}</h3>" +
        "</div>");
}]);
 
 
//------------
// DIRECTIVES
//------------
 
angular.module("myApp.directives").directive("myDirective",
    ["$locale", "templatesManager", function ($locale, templatesManager) {
        return {
            restrict: "E",
            replace: true,
            templateUrl: templatesManager.getLocalizedTemplate("/myApp/templates/myDirective.html"),
            controller: ["$scope", function ($scope) {
                $scope.labels = $locale.labels;
            }]
        };
    }]);
 
 
//----------
// SERVICES
//----------
 
angular.module("myApp.services").service("templatesManager",
    ["$templateCache", "$locale", function ($templateCache, $locale) {
        var _self = this;
 
        _self.getLocalizedTemplate = function (templateName) {
            var localizedName = templateName.replace(".html", "_" + $locale.localeID + ".html");
 
            if (!!$templateCache.get(localizedName)) {
                return localizedName;
            } else {
                return templateName;
            }
        };
    }]);

Angular JS – Transclude and Require

Transclude allows us to define a directive’s content, or part of it, directly from the content we put inside it. This is a very flexible way to compose an interface by directly putting directives one inside the other. The place in the template where to place the inner content can be defined with the ng-transclude directive.

However the Transclude’s default behavior doesn’t always work as expected, so in this example we’ll add some magic in order to make it work in the right way.

<div ng-app="myApp">
    <my-container current-color="red">
        <!--I can have bound HTML-->
        <p>{{description}}</p>
 
        <!--I can have bound directives-->
        <my-content color="red" current-color="currentColor" next-color="blue"></my-content>
        <my-content color="blue" current-color="currentColor" next-color="green"></my-content>
        <my-content color="green" current-color="currentColor" next-color=""></my-content>
    </my-container>
</div>

angular.module("myApp.directives", []);
angular.module("myApp", [
    "myApp.directives"
]);
 
angular.module("myApp.directives").directive("myContainer", function () {
    return {
        restrict: "E",
        replace: true,
        transclude: true,
        scope: {
            currentColor: "@"
        },
        template:
            "<div>" +
                "<h3>{{title}}</h3>" +
                "<div id='transclude'></div>" +
                "<h3>{{footer}}</h3>" +
            "</div>",
        controller: ["$scope", function ($scope) {
            $scope.title = "This is the title";
            $scope.description = "Click on the colors!";
            $scope.footer = "This is the footer";
        }],
        link: function ($scope, elem, attrs, ctrl, transclude) {
            /*
            http://angular-tips.com/blog/2014/03/transclusion-and-scopes
            By default, the transcluded content is not bound to the directive's
            isolate scope, as one could think, but is instead bound to the outer
            controller's one.
 
            ---------------------------        -------------
            | Outer controller        | =====> | Directive |
            |                         |        -------------
            | (the main in this case) |        -----------------------
            |                         | -----> | Transcluded content |
            ---------------------------        -----------------------
 
            =====> = Isolated scope
            -----> = Non-isolated scope
 
            Because of this both $scope.title and $scope.footer would be rendered,
            since they are included in the template, while $scope.description
            wouldn't, because is referred in the transcluded content.
 
            The transclude() function allows us to change this behavior by setting
            what scope the transcluded content should to be compiled with. After
            the content has been compiled we can manually insert it where we want.
 
            IMPORTANT
            Since we override the default behavior in this way we don't have to use
            the ng-transclude directive to specify where the transcluded content
            should be placed. Since that directive automatically inject the
            content as it's generated in the default way, if we do so we'll end
            up having it rendered twice.
            */
            transclude($scope, function (clone) {
                elem.find("#transclude").append(clone);
            });
        }
    };
});
 
angular.module("myApp.directives").directive("myContent", function () {
    return {
        restrict: "E",
        replace: true,
        require: "^myContainer", // <-- Must be insert within a myContainer controller
        scope: {
            color: "@",
            currentColor: "=",
            nextColor: "@"
        },
        template:
            "<div style='background-color:{{color}}'>" +
                "<h3>{{title}}</h3>" +
                "<div ng-if='currentColor == color'>" +
                    "<button ng-click='onNext()'>Next</button>" +
                "</div>" +
            "</div>",
        controller: ["$scope", function ($scope) {
            $scope.title = "This is the content " + $scope.color;
            $scope.onNext = function () {
                $scope.currentColor = $scope.nextColor;
 
                //We can access to the parent's controller
                $scope.$parent.title = $scope.color;
            };
        }]
    };
});


Referring the $parent from the view

As we saw before we can user $scope.$parent to refer the parent’s scope (this is more safe if we use the “require” option to enforce our component to be used with the right parent). However there is some consideration to do when accessing the $parent from the view. The $parent keyword is managed by angular and isn’t always referring the scope we suppose. This is because several angular directives, such as ng-if and ng-repeat create their own child scope, so if we refer to the $parent from inside one of them we’ll just get their outer scope and not the scope of our component’s parent. And easy way to be sure about the parent we are referring to is to map a reference in our scope:

$scope.parent = $scope.$parent;

Angular JS – HttpService to cache $http calls between different apps

angular.module("common.services")
    .service("httpCachingService", ["$http", '$q', "$window",
        function ($http, $q, $window) {
            var _self = this;

            var _ns = $window.httpCachingService = $window.httpCachingService || {
                    items: []
                };

            _self.get = function (uri) {
                return $q(function (resolve, reject) {
                    var filteredItems = _ns.items.filter(function (item) {
                        return item.uri == uri;
                    });

                    var item = filteredItems.length > 0 ? filteredItems[0] : null;

                    if (!!item) {
                        if (item.ready) {
                            resolve(item);
                        } else {
                            item.listeners.push(function () {
                                resolve(item);
                            });
                        }
                    } else {
                        item = {
                            uri: uri,
                            ready: false,
                            data: null,
                            listeners: []
                        };

                        _ns.items.push(item);

                        $http.get(uri)
                            .then(function (result) {
                                item.data = result.data;
                                item.ready = true;

                                item.listeners.forEach(function (listener) {
                                    listener();
                                });

                                item.listeners = [];
                                resolve(item);
                            })
                            .catch(function (error) {
                                reject(error);
                            });
                    }
                });
            };

            _self.invalidate = function (uri) {
                _ns.items = _ns.items.filter(function (item) {
                    return (!!uri && item.uri != uri) || !item.ready;
                });
            };
        }]);

Angular JS – InteropService to broadcast events between different apps

angular.module("myApp.services")
    .service("interopService", ["$q", function ($q) {
        var _self = this;

        _self.broadcastEventToApp = function (appSelector, event, args) {
            return $q(function (resolve, reject) {
                var app = $(appSelector), injector;
                if (app.length > 0 && app.hasClass("ng-scope")
                    && !!(injector = app.injector())) {

                    var $rootScope = injector.get("$rootScope");
                    safeApply($rootScope, function () {
                        $rootScope.$broadcast(event, args);
                        resolve();
                    });
                } else {
                    reject();
                }
            });
        };

        _self.broadcastEvent = function (event, args) {
            var apps = $(".ng-scope").filter(function (i, e) {
                return $(e).parents(".ng-scope").length == 0;
            });

            apps.each(function () {
                _self.broadcastEventToApp(this, event, args);
            });
        };

        function safeApply($rootScope, callback) {
            var phase = $rootScope.$$phase;
            if (phase == '$apply' || phase == '$digest') {
                callback();
            } else {
                $rootScope.$apply(callback);
            }
        }
    }]);

Angular JS – Testing a controller with its view, as for a directive with its template

var $scope, target;

inject(function ($rootScope, $controller, $compile, $templateCache) {
    $scope = $rootScope.$new();

    $controller("modalUsersController", {
        "$scope": $scope,
        "$locale": {labels: labelsMock},
        "$uibModalInstance": uibModalInstanceMock,
        "usersService": usersServiceMock
    });

    target = $compile($templateCache.get("modal-views/users.html"))($scope);
    $scope.$digest();
});

Angular JS – i18n Directive + Dynamic Templates

angular.module("myApp.directives").directive("i18n", function () {
    return {
        restrict: "E",
        replace: true,
        scope: {
            key: "@"
        },
        template: function (elem) {
            if (elem.attr("html") == "true") {
                return "<span ng-bind-html='value'></span>";
            } else {
                return "<span>{{value}}</span>";
            }
        },
        controller: ["$locale", "$scope", function ($locale, $scope) {
            if ($scope.key == "CURRENCY_SYM") {
                $scope.value = $locale.NUMBER_FORMATS.CURRENCY_SYM
            } else {
                $scope.value = $locale.labels[$scope.key];
            }
        }]
    }
});

It’s possible to do the same with the templateUrl property, in order to load a different external template depending on some condition (the $locale language for instance).

Angular JS – Pagination Directive

Given:

pageInfo: {
    page = zero-based page index
    pageCount = number of pages
    hasPrev = boolean
    hasNext = boolean
};

angular.module("myApp.directives").directive("pagination", function () {
    return {
        restrict: "E",
        replace: true,
        scope: {
            pageInfo: "=",
            onPageChange: "="
        },
        templateUrl: "/apps/myApp/templates/paginationDirective.html",
        controller: ["$scope", function($scope) {
            var range = 3;
 
            $scope.paginationItems = [];
            $scope.$watch('pageInfo', calculatePaginationItems, true);
 
            $scope.isActive = function(paginationItem) {
                return paginationItem == $scope.pageInfo.page + 1;
            };
 
            function calculatePaginationItems() {
                if (!!$scope.pageInfo) {
                    var curPage = Math.max(Math.min($scope.pageInfo.page - range,
                        $scope.pageInfo.pageCount - (range * 2 + 1)), 0);
 
                    for (var n = 0; n < (range * 2 + 1); n++) {
                        if (curPage == $scope.pageInfo.pageCount) {
                            break;
                        }
                        $scope.paginationItems.push(++curPage);
                    }
                }
            }
        }]
    };
});

<ul class="pagination pagination-sm no-margin pull-right">
    <li class="paginate_button previous" ng-class="{'disabled': !pageInfo.hasPrev}">
        <span role="button" ng-click="onPageChange(1)">
            <i18n key="pagination.first" />
        </span>
    </li>
    <li class="paginate_button previous" ng-class="{'disabled': !pageInfo.hasPrev}">
        <span role="button" ng-click="onPageChange(pageInfo.page)">
            <i18n key="pagination.prev" />
        </span>
    </li>

    <li class="paginate_button" ng-repeat="paginationItem in paginationItems" ng-class="{'active': isActive(paginationItem)}">
        <span role="button" ng-click="onPageChange(paginationItem)">
            {{paginationItem}}
        </span>
    </li>

    <li class="paginate_button previous" ng-class="{'disabled': !pageInfo.hasNext}">
        <span role="button" ng-click="onPageChange(pageInfo.page + 2)">
            <i18n key="pagination.next" />
        </span>
    </li>
    <li class="paginate_button previous" ng-class="{'disabled': !pageInfo.hasNext}">
        <span role="button" ng-click="onPageChange(pageInfo.pageCount)">
            <i18n key="pagination.last" /> ({{pageInfo.pageCount}})
        </span>
    </li>
</ul>

Angular Service with $http, $q and testing (test fixtures)

angular.module("myApp.services")
    .service("usersService", ["$http", '$q',
        function ($http, $q) {
            var _self = this;

            _self.getUserDetails = function () {
                return $q(function (resolve, reject) {
                    $http.get('/api/client/me/details')
                        .then(function (result) {
                            resolve(result.data);
                        })
                        .catch(function (error) {
                            reject(error);
                        });
                });
            };

            _self.setUserDetails = function (firstName, lastName) {
                return $q(function (resolve, reject) {
                    $http.post('/api/client/me/details', {
                        firstName: firstName,
                        lastName: lastName
                    })
                        .then(function (result) {
                            resolve(result.data);
                        })
                        .catch(function (error) {
                            reject(error);
                        });
                });
            };
        }]);

<hr />

describe("an usersService", function () {
    var target;

    var httpMock;
    var qMock;

    beforeEach(function () {
        httpMock = fixturesProvider.getSuccessHttpMock("api response");
        qMock = fixturesProvider.getQMock();

        module("myApp.services", function ($provide) {
            $provide.value("$http", httpMock);
            $provide.value("$q", qMock);
        });

        inject(function ($injector) {
            target = $injector.get("usersService");
        });
    });

    describe("#getUserDetails", function () {
        it("it should call the api", function () {
            spyOn(httpMock, "get").and.callThrough();
            target.getUserDetails();
            expect(httpMock.get).toHaveBeenCalledWith("/api/client/me/details")
        });

        it("it should return the result", function () {
            var result = target.getUserDetails();
            expect(result.resolved).toBeTruthy();
            expect(result.value).toBe("api response");
        });
    });

    describe("#setUserDetails", function () {
        it("it should call the api", function () {
            spyOn(httpMock, "post").and.callThrough();
            target.setUserDetails("John", "Doe");
            expect(httpMock.post).toHaveBeenCalledWith("/api/client/me/details", {
                firstName: "John",
                lastName: "Doe"
            })
        });

        it("it should return the result", function () {
            var result = target.setUserDetails("John", "Doe");
            expect(result.resolved).toBeTruthy();
            expect(result.value).toBe("api response");
        });
    });

    describe("when the api call fails", function () {
        beforeEach(function () {
            httpMock.get = fixturesProvider.getFailingHttpMethodMock(500);
            httpMock.post = fixturesProvider.getFailingHttpMethodMock(500);
        });

        describe("#getUserDetails", function () {
            it("it should return the error", function () {
                var result = target.getUserDetails();
                expect(result.resolved).toBeFalsy();
                expect(result.value.status).toBe(500);
            });
        });

        describe("#setUserDetails", function () {
            it("it should return the error", function () {
                var result = target.setUserDetails("John", "Doe");
                expect(result.resolved).toBeFalsy();
                expect(result.value.status).toBe(500);
            });
        });
    });
});

Angular JS – Test Fixtures

var fixturesProvider = new function () {
    var _this = this;

    //---------------
    // promises
    //---------------

    _this.getSuccessPromiseMock = function (result) {
        return {
            then: function (callback) {
                callback(result);
                return {
                    catch: function () {
                    }
                };
            }
        };
    };

    _this.getFailingPromiseMock = function (error) {
        return {
            then: function () {
                return {
                    catch: function (callback) {
                        callback(error);
                    }
                }
            }
        };
    };

    //---------------
    // $http
    //---------------

    _this.getHttpResultMock = function (result) {
        return {
            data: result
        };
    };

    _this.getHttpErrorMock = function (status) {
        return {
            status: status
        };
    };

    _this.getSuccessHttpMethodMock = function (result) {
        return function () {
            return _this.getSuccessPromiseMock(_this.getHttpResultMock(result))
        };
    };

    _this.getFailingHttpMethodMock = function (status) {
        return function () {
            return _this.getFailingPromiseMock(_this.getHttpErrorMock(status))
        };
    };

    _this.getSuccessHttpMock = function (result) {
        return {
            get: _this.getSuccessHttpMethodMock(result),
            post: _this.getSuccessHttpMethodMock(result)
        };
    };

    _this.getFailingHttpMock = function (status) {
        return {
            get: _this.getFailingHttpMethodMock(status),
            post: _this.getFailingHttpMethodMock(status)
        };
    };

    //---------------
    // services
    //---------------

    _this.getSuccessApiMethodMock = function (result) {
        return function () {
            return _this.getSuccessPromiseMock(result);
        };
    };

    _this.getFailingApiMethodMock = function (status) {
        return _this.getFailingHttpMethodMock(status);
    };

    //---------------
    // $q
    //---------------

    _this.getQMock = function () {
        return function (callback) {
            var result = null;

            callback(function (resolveValue) {
                result = {
                    resolved: true,
                    value: resolveValue
                };
            }, function (rejectValue) {
                result = {
                    resolved: false,
                    value: rejectValue
                };
            });

            return result;
        };
    };

    //---------------
    // modals
    //---------------

    _this.getUibModalMock = function () {
        return {
            open: function () {
                return {
                    result: {
                        then: function (callback) {
                            callback();
                        }
                    }
                };
            }
        };
    };

    _this.getUibModalInstanceMock = function () {
        return {
            dismiss: function () {
            },
            close: function () {
            }
        };
    };

    //---------------
    // $window
    //---------------

    _this.getWindowMock = function () {
        return {
            location: {
                href: ""
            }
        };
    };

    //---------------
    // $filter
    //---------------

    _this.getFilterMock = function () {
        return function () {
            return function () {
                return Array.prototype.join.call(arguments, "-");
            };
        };
    }
}();