AngularJS, code.explode

Using Response Interceptors to Show and Hide a Loading Widget

AngularJS provides extension points which allow you to intercept responses to http requests before they are handed over to the application code. You can use these extension points to provide global exception handling, authentication, or any other type of pre-processing you might need in your app. In this article I’m going to show you how you can use a response interceptor to turn a loading widget on and off as your http requests are made and completed.

The App Overview

I’ve created a simple app that displays a list of different yeast strains used in home brewing. I’ve stored the list of yeast strains in a MongoDB collection out on mongolabs.com and I am using the Promise-aware MongoLab $resource for AngularJS by Pawel Kozlowski out on GitHub at https://github.com/pkozlowski-opensource/angularjs-mongolab-promise to retrieve the collection.

I’ve also added a simple filter that allows us to page through the data to make the data a little easier to deal with.

If you look at code it’s pretty straight forward, the controller makes a call to the data resource which in turn queries the mongolab collection that we assign to an internal array that is used in the ng-repeat to populate the page.

'use strict';

// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['mongolabResourceHttp', 'data.services', ]);

app.filter('startFrom', function () {
        return function (input, start) {
            start = +start; //parse to int
            return input.slice(start);
        }
    });

function myController($scope, YeastResource) {
    $scope.yeasts = [];
    $scope.currentPage = 0;
    $scope.pageSize = 10;

    $scope.numberOfPages = function () {
        return Math.ceil($scope.yeasts.length / $scope.pageSize);
    };

    $scope.init = function() {
        YeastResource.query({}, {sort: {Type: 1, Name: 1}}).then(function(yeast){
            $scope.yeasts = yeast;
        });
    };

    $scope.init();
}

Below is the HTML Mark up used to display the data on the page. It’s pretty straight forward as well. The only thing to note is how the buttons are used to change the currentPage scope variable to page through the data.

<div class="container-fluid" ng-controller="myController">
    <div class="row-fluid">
        <h2>HTTP Interceptor Example</h2>
    </div>
    <div class="row-fluid" >
        <div class="span1">Name</div>
        <div class="span1">Type</div>
        <div class="span1">Form</div>
        <div class="span1">Laboratory</div>
        <div class="span3">Best For</div>
        <div class="span5">Notes</div>
    </div>
    <div class="row-fluid" ng-repeat="yeast in yeasts | startFrom:currentPage*pageSize | limitTo:pageSize">
        <div class="span1">{{yeast.Name}}</div>
        <div class="span1">{{yeast.Type}}</div>
        <div class="span1">{{yeast.Form}}</div>
        <div class="span1">{{yeast.Laboratory}}</div>
        <div class="span3">{{yeast.BestFor}}</div>
        <div class="span5">{{yeast.Notes}}</div>
    </div>
    <hr />
    <div class="well">
        <button ng-disabled="currentPage == 0" ng-click="currentPage=0">
            First
        </button>
        <button ng-disabled="currentPage == 0" ng-click="currentPage=currentPage-1">
            Previous
        </button>
        <span ng-bind="currentPage+1">1</span>/<span ng-bind="numberOfPages()">Loading..</span>
        <button ng-disabled="currentPage >= yeasts.length/pageSize - 1" ng-click="currentPage=currentPage+1">
            Next
        </button>
        <button ng-disabled="currentPage >= yeasts.length/pageSize - 1" ng-click="currentPage=numberOfPages()-1">
            Last
        </button>
    </div>
</div>

Adding a Loading Widget

First let’s define our loading widget. We’ll add a DIV to the HTML Mark up and set the id to loadingWidget so we can show and hide it in our response interceptor. All the loading widget does is wrap an image tag that contains our animated gif and the word Loading. By default we use an in-line style to set the DIV to display: none so it will not be visible until we set it visible.

<div class="container-fluid" ng-controller="myController">
    <div id="loadingWidget" class="row-fluid ui-corner-all" style="padding: 0 .7em; display: none;">
        <div class="loadingContent">
            <p>
                <img alt="Loading  Content" src="images/ajax-loader.gif" /> Loading
            </p>
        </div>
    </div>
    <div class="row-fluid">
        <h2>HTTP Interceptor Example</h2>
    </div>
    <div class="row-fluid" >
        <div class="span1">Name</div>
        <div class="span1">Type</div>
        <div class="span1">Form</div>
        <div class="span1">Laboratory</div>
        <div class="span3">Best For</div>
        <div class="span5">Notes</div>
    </div>
    <div class="row-fluid" ng-repeat="yeast in yeasts | startFrom:currentPage*pageSize | limitTo:pageSize">
        <div class="span1">{{yeast.Name}}</div>
        <div class="span1">{{yeast.Type}}</div>
        <div class="span1">{{yeast.Form}}</div>
        <div class="span1">{{yeast.Laboratory}}</div>
        <div class="span3">{{yeast.BestFor}}</div>
        <div class="span5">{{yeast.Notes}}</div>
    </div>
    <hr />
    <div class="well">
        <button ng-disabled="currentPage == 0" ng-click="currentPage=0">
            First
        </button>
        <button ng-disabled="currentPage == 0" ng-click="currentPage=currentPage-1">
            Previous
        </button>
        <span ng-bind="currentPage+1">1</span>/<span ng-bind="numberOfPages()">Loading..</span>
        <button ng-disabled="currentPage >= yeasts.length/pageSize - 1" ng-click="currentPage=currentPage+1">
            Next
        </button>
        <button ng-disabled="currentPage >= yeasts.length/pageSize - 1" ng-click="currentPage=numberOfPages()-1">
            Last
        </button>
    </div>
</div>

Next let’s add a response interceptor to show and hide a loading widget while our query is being made against mongolab.com.

To do this we’ll add a configuration item to our app.js file that will define the response interceptor.

app.config(['$httpProvider', function ($httpProvider) {
    var $http,
        interceptor = ['$q', '$injector', function ($q, $injector) {
            var error;

            function success(response) {
                // get $http via $injector because of circular dependency problem
                $http = $http || $injector.get('$http');
                if($http.pendingRequests.length < 1) {
                    $('#loadingWidget').hide();
                }
                return response;
            }

            function error(response) {
                // get $http via $injector because of circular dependency problem
                $http = $http || $injector.get('$http');
                if($http.pendingRequests.length < 1) {
                    $('#loadingWidget').hide();
                }
                return $q.reject(response);
            }

            return function (promise) {
                $('#loadingWidget').show();
                return promise.then(success, error);
            }
        }];

    $httpProvider.responseInterceptors.push(interceptor);
}]);

The interceptor will be called prior to any application code, so we need to provide functions to intercept the success and error callbacks along with a function that is used to insert our success and error functions into the call hierarchy of the promise returned from the call to $http.

We also need to push our interceptor onto the array of response interceptors maintained by the $httpProvider so each call to the $http service calls our code.

Both the success and error functions are very similar, each one gets the $http service via the $injector service to avoid circular dependencies and then checks to see if the number of pending requests is less than one, if so the code hides the loadingWidget element. The Success method then returns the response and the Error method calls reject with the response to indicate the call failed.

The third method shows the loadingWidget and returns the promise with the Success and Error methods inserted into the response chain.

Now every time a call to the $http service is made our interceptor will be called and will display the loadingWidget and hide it accordingly.

Below is the full code of our app.js file:

'use strict';

// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['mongolabResourceHttp', 'data.services', ]);

app.config(['$httpProvider', function ($httpProvider) {
    var $http,
        interceptor = ['$q', '$injector', function ($q, $injector) {
            var error;

            function success(response) {
                // get $http via $injector because of circular dependency problem
                $http = $http || $injector.get('$http');
                if($http.pendingRequests.length < 1) {
                    $('#loadingWidget').hide();
                }
                return response;
            }

            function error(response) {
                // get $http via $injector because of circular dependency problem
                $http = $http || $injector.get('$http');
                if($http.pendingRequests.length < 1) {
                    $('#loadingWidget').hide();
                }
                return $q.reject(response);
            }

            return function (promise) {
                $('#loadingWidget').show();
                return promise.then(success, error);
            }
        }];

    $httpProvider.responseInterceptors.push(interceptor);
}]);

app.filter('startFrom', function () {
        return function (input, start) {
            start = +start; //parse to int
            return input.slice(start);
        }
    });

function myController($scope, YeastResource) {
    $scope.yeasts = [];
    $scope.currentPage = 0;
    $scope.pageSize = 10;

    $scope.numberOfPages = function () {
        return Math.ceil($scope.yeasts.length / $scope.pageSize);
    };

    $scope.init = function() {
        YeastResource.query({}, {sort: {Type: 1, Name: 1}}).then(function(yeast){
            $scope.yeasts = yeast;
        });
    };

    $scope.init();
}

The Wrap Up

This article shows a simple way to use response interceptors to display and hide a loading widget each time the $http service requests data from a server. It also showed how you can plug your code into the $http service pipeline to provide cross cutting functionality for your AngularJS Apps. To learn more about response interceptors check out the AngularJS $http service API documentation at http://docs.angularjs.org/api/ng.$http

Hopefully, this article helps you to see some of the extensibility points provided by the AngularJS library and helps you think of ways you can code for cross cutting concerns using such extension points.

Code for this example can be found on GitHub at https://github.com/lavinjj/angularjs-spinner

As always, drop me a comment on other AngularJS topics you’d like to see more tutorials on.

 

 

Edit:  I have provided an updated version of the above example that works better. Please see my new post Using Response Interceptors to Show and Hide a Loading Widget: Redux

Standard
Loading Facebook Comments ...

5 thoughts on “Using Response Interceptors to Show and Hide a Loading Widget

  1. Pingback: angularjs 学习大礼包大赠送【权威英文版】! _ Angular _ 小G的大Q

  2. Pingback: Angularjs Found great solution to display AJAX spinner loading widget | Lemoncode

  3. Pingback: Show a html page while another page loads in background | DiscVentionsTech

  4. Pingback: How could you become zero to hero in AngularJS? | Milap Bhojak

Got a comment? I'd love to hear it!