AngularJS, code.explode, Coding

Using Response Interceptors to Show and Hide a Loading Widget: Redux

In my previous post Using Response Interceptors to Show and Hide a Loading Widget I showed how to display a loading widget whenever an Ajax request started and hide it when all the requests completed by using a $http resource interceptor.

Unfortunately, I violated one of the core tenets of AngularJS’ best practices by modifying the DOM outside of a directive. I want to thank everyone who brought that to my attention and provided examples on how to clean up the code.

This post will walk through the revised code to show how to do it properly. I will also provide a second solution, that I think is even better structured, that uses a Publish/Subscribe pattern to encapsulate the whole messaging solution and keep the publishers and subscribers from having to know anything about the notification mechanism.

Using $rootScope to Send Notifications

The first solution I’m going to cover, uses the $rootScope $broadcast method to broadcast to all children in the scope tree. This way, any controllers or directives further down the scope that are interested in the messages can set up scope $on methods for the event message and can react to it.

Below is the revised code from the Sample01 solution:

 

'use strict';

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

app.constant('_START_REQUEST_', '_START_REQUEST_');
app.constant('_END_REQUEST_', '_END_REQUEST_');

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

            function success(response) {
                // get $http via $injector because of circular dependency problem
                $http = $http || $injector.get('$http');
                // don't send notification until all requests are complete
                if ($http.pendingRequests.length < 1) {
                    // get $rootScope via $injector because of circular dependency problem
                    rootScope = rootScope || $injector.get('$rootScope');
                    // send a notification requests are complete
                    rootScope.$broadcast(_END_REQUEST_);
                }
                return response;
            }

            function error(response) {
                // get $http via $injector because of circular dependency problem
                $http = $http || $injector.get('$http');
                // don't send notification until all requests are complete
                if ($http.pendingRequests.length < 1) {
                    // get $rootScope via $injector because of circular dependency problem
                    rootScope = rootScope || $injector.get('$rootScope');
                    // send a notification requests are complete
                    rootScope.$broadcast(_END_REQUEST_);
                }
                return $q.reject(response);
            }

            return function (promise) {
                // get $rootScope via $injector because of circular dependency problem
                rootScope = rootScope || $injector.get('$rootScope');
                // send notification a request has started
                rootScope.$broadcast(_START_REQUEST_);
                return promise.then(success, error);
            }
        }];

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

app.directive('loadingWidget', ['_START_REQUEST_', '_END_REQUEST_', function (_START_REQUEST_, _END_REQUEST_) {
    return {
        restrict: "A",
        link: function (scope, element) {
            // hide the element initially
            element.hide();

            scope.$on(_START_REQUEST_, function () {
                // got the request start notification, show the element
                element.show();
            });

            scope.$on(_END_REQUEST_, function () {
                // got the request end notification, hide the element
                element.hide();
            });
        }
    };
}]);

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

app.controller('myController', ['$scope', 'YeastResource', function ($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 first change is that I've added two constants on lines 6 and 7, these will be used as the event messages sent by the http resource interceptor.

On line 9, I add the two constants as dependencies to the config function so they can be used by the interceptor.

On line 12, I define a variable to hold a reference to the $rootScope, which will be used to broadcast the notification messages. You might wonder why we didn't put the $rootScope as a parameter to the config function, this is because at the time the config function is called the $rootScope doesn't exist. Later when we need the $rootScope we get it by making a call to $injector.get(). You can see this on lines 20, 33 and 40.

Next we define the success and error handlers. Both methods work similar, we use $injector.get() to get a reference to the $http service and check to see if all the requests have completed and if so we use the reference to the $rootScope to broadcast the _END_REQUEST_ notification. You can see this on lines 18 thru 23 and on lines 31 thru 36.

Finally, we use the reference to the $rootScope to broadcast the _START_REQUEST_ in the final method of the resource interceptor on lines 41 thru 44.

The Loading Widget Directive

Instead of having to put logic into a controller to show or hide the loading widget each time a notification is received, I've created a directive that handles showing and hiding any element that it is applied to. Let's discuss the code below:

 

app.directive('loadingWidget', ['_START_REQUEST_', '_END_REQUEST_', function (_START_REQUEST_, _END_REQUEST_) {
    return {
        restrict: "A",
        link: function (scope, element) {
            // hide the element initially
            element.hide();

            scope.$on(_START_REQUEST_, function () {
                // got the request start notification, show the element
                element.show();
            });

            scope.$on(_END_REQUEST_, function () {
                // got the request end notification, hide the element
                element.hide();
            });
        }
    };
}]);

I again add dependencies for the _START_REQUEST_ and _END_REQUEST_ constants to the directive, this way the directive can add message handlers for each notification message. The _START_REQUEST_ handler is defined on lines 59 thru 62 and the _END_REQUEST_ handler is defined on lines 64 thru 67. Each handler shows or hides the attached element based on notification message. By default the directive hides the attached element until it receives the _START_REQUEST_ notification message.

HTML Changes

The only change made to the original index.html file occurs on the div element that contains the loading widget at lines 25 thru 31, where we added the new directive to the div element at line 25.

 

    <div id="loadingWidget" class="row-fluid ui-corner-all" style="padding: 0 .7em;" loading-widget >
        <div class="loadingContent">
            <p>
                <img alt="Loading  Content" src="images/ajax-loader.gif" /> Loading
            </p>
        </div>
    </div>

 

Now that is pretty much all you need to do get the loading widget to display and hide whenever an $http request begins or ends.

Applying the Publish/Subscribe Pattern to the Loading Widget

Our solution works great, but there are some issues with the solution as it now sits.

Both the $http resource interceptor and the directive have to have knowledge of the notification message in order to communicate with each other. This means that the modularity of the solution is too tightly coupled to be reused in other scenarios where we might need it.

If we wanted to display the widget whenever we had a lengthy computation execute, we would have to couple the computation service to the same two notification messages in order to make it work. But what if we wanted to move the computation engine to another application that used different notification messages? We'd have to change the dependencies we inject into the service or make some code changes to wire up the service with the new notification code.

That's not a good thing. What we really want are code components with high cohesion and high modularity, and with minimal coupling between them. To achieve this we need to use a different mechanism that can abstract out the specifics around the notification messages and provide a standard interface that can be implemented for any type of notifications that we might want to implement.

We can use the Publish/Subscribe Pattern to encapsulate the notification details and instead provide a standard interface that any component can use to show the widget as needed.

The implementation of the Publish/Subscribe pattern I built my service on was created for AngularJS by Eric Burley, @eburley, his original post can be found at angularjs.org watch, on pub sub, and you., I just wrapped the implementation into a service that could be injected into the publishers and subscribers.

So let's take a look at the new code from Sample02 solution:

 

'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 notificationChannel;

            function success(response) {
                // get $http via $injector because of circular dependency problem
                $http = $http || $injector.get('$http');
                // don't send notification until all requests are complete
                if ($http.pendingRequests.length < 1) {
                    // get requestNotificationChannel via $injector because of circular dependency problem
                    notificationChannel = notificationChannel || $injector.get('requestNotificationChannel');
                    // send a notification requests are complete
                    notificationChannel.requestEnded();
                }
                return response;
            }

            function error(response) {
                // get $http via $injector because of circular dependency problem
                $http = $http || $injector.get('$http');
                // don't send notification until all requests are complete
                if ($http.pendingRequests.length < 1) {
                    // get requestNotificationChannel via $injector because of circular dependency problem
                    notificationChannel = notificationChannel || $injector.get('requestNotificationChannel');
                    // send a notification requests are complete
                    notificationChannel.requestEnded();
                }
                return $q.reject(response);
            }

            return function (promise) {
                // get requestNotificationChannel via $injector because of circular dependency problem
                notificationChannel = notificationChannel || $injector.get('requestNotificationChannel');
                // send a notification requests are complete
                notificationChannel.requestStarted();
                return promise.then(success, error);
            }
        }];

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

app.factory('requestNotificationChannel', ['$rootScope', function($rootScope){
    // private notification messages
    var _START_REQUEST_ = '_START_REQUEST_';
    var _END_REQUEST_ = '_END_REQUEST_';

    // publish start request notification
    var requestStarted = function() {
        $rootScope.$broadcast(_START_REQUEST_);
    };
    // publish end request notification
    var requestEnded = function() {
        $rootScope.$broadcast(_END_REQUEST_);
    };
    // subscribe to start request notification
    var onRequestStarted = function($scope, handler){
        $scope.$on(_START_REQUEST_, function(event){
            handler();
        });
    };
    // subscribe to end request notification
    var onRequestEnded = function($scope, handler){
        $scope.$on(_END_REQUEST_, function(event){
            handler();
        });
    };

    return {
        requestStarted:  requestStarted,
        requestEnded: requestEnded,
        onRequestStarted: onRequestStarted,
        onRequestEnded: onRequestEnded
    };
}]);

app.directive('loadingWidget', ['requestNotificationChannel', function (requestNotificationChannel) {
    return {
        restrict: "A",
        link: function (scope, element) {
            // hide the element initially
            element.hide();

            var startRequestHandler = function() {
                // got the request start notification, show the element
                element.show();
            };

            var endRequestHandler = function() {
                // got the request start notification, show the element
                element.hide();
            };

            requestNotificationChannel.onRequestStarted(scope, startRequestHandler);

            requestNotificationChannel.onRequestEnded(scope, endRequestHandler);
        }
    };
}]);

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

app.controller('myController', ['$scope', 'YeastResource', function ($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();
}]);

 

First off, We've removed the constant declarations and the dependencies on them across the code.

$http Resource Interceptor Changes

Starting on line 9, I've changed the variable that referenced the $rootScope instance to now hold a new reference to the Publish/Subscribe implementation called notificationChannel. Then I changed the Success, Error, and $http Resource Interceptor functions to request an instance of the new notification service on lines 17, 30, and 39.

The Success and Error methods now call the requestEnded() method on the new notification service instead of broadcasting a notification message, (lines 19 and 32). The $http Resource Interceptor function now calls the requestStarted() method instead of broadcasting the notification message on line 41.

The Publish/Subscribe Service

Starting on line 49, I define the a new service that will implement the Publish/Subscribe Pattern. To hide the details of the implementation I'm using a revealing module pattern to return back an object with only four public methods; requestStarted, requestEnded, onRequestStarted and onRequestEnded. The rest of the details and the broadcasting of messages are all hidden from the consumers of the service.

We now define the notification start and end messages on lines 51 and 52. The requestStarted and requestEnded methods broadcast the notification messages and the onRequestStarted and onRequestEnded methods setup the broadcast watchers on the scope that is passed into them. The onRequestStarted and onRequestEnded methods also require that a handler function be passed into the method so we can call it whenever the notification message is received.

At first glance it might look that the new service should have an array of listeners, but since each listener passes in it's own scope, the service can use it to perform the watch for the notification message and the anonymous function passed to the watch calls the listener's request handler eliminating the need for an array to hold all of the request handlers.

loadingWidget Directive Changes

The directive now has a dependency on the new Publish/Subscribe Service, requestNotificationChannel. And instead of implementing watches for the notification messages, the directive now has defined two new request handlers; startRequestHandler and endRequestHandler, which now handle showing and hiding the element the directive is attached two. Finally, the directive registers the request handlers with the Publish/Subscribe Service by passing in the directives scope and the appropriate request handler.

The Wrap Up

As I said at the beginning of this post, my original post covered how to use $http request interceptor to display and hide a loading widget. However, there were flaws in the original code. In this article I provided an updated example of the solution that follows the Best Practices of AngularJS. I then provided a second, better solution which uses the Publish/Subscribe Pattern to encapsulate the details of the messaging and improve the overall modularity of the solution.

I've revised the code and split both examples into two different examples; Sample01 and Sample02. I've also provided a stand alone module that can be used in your applications. You can find all of the source code on my GitHub site.

Standard
Loading Facebook Comments ...

One thought on “Using Response Interceptors to Show and Hide a Loading Widget: Redux

  1. Pingback: Angularjs found great solution to display AJAX spinner loading widget | Lemoncode

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