Thursday 16 January 2014

AngularJS Loading Widget

UPDATE: Does not work in 1.3.0. Use Interceptors instead. See Angular Request Count & Loading Widget (ng 1.3.0).

When using angular JS, it would be nice to inform the user that some content is being loaded or that there are some pending requests. I found some solutions of how to show this information to user globally. But in most of these cases the code they used wasn’t clean.

One of the problem was using $rootScope to $broadcast an event informing that the loading has started. Imagine the tons of listeners being notified.

Another problem was the use of Request/Response interceptors. Sometimes this was a unreliable solution for a reason I was unable to find.

The third problem was usage of pendingRequests property of $http service. The documentation states clearly that it should be used for debugging purposes only.

Therefore, I decided writing my very first provider to cope with these problems. I register this provider during config phase of the application as a Request/Response transformer. Every single request/response goes through this function.

This is how my requestNotification provider looks like:

   1:  angular.module('myApp.services').provider(
   2:  'requestNotification', function() {
   3:      // This is where we keep subscribed listeners
   4:      var onRequestStartedListeners = [];
   5:      var onRequestEndedListeners = [];
   6:   
   7:      // This is a utility to easily increment the request count
   8:      var count = 0;
   9:      var requestCounter = {
  10:          increment: function() {
  11:              count++;
  12:          },
  13:          decrement: function() {
  14:              if (count > 0)
  15:                  count--;
  16:          },
  17:          getCount: function() {
  18:              return count;
  19:          }
  20:      };
  21:      // Subscribe to be notified when request starts
  22:      this.subscribeOnRequestStarted = function(listener) {
  23:          onRequestStartedListeners.push(listener);
  24:      };
  25:   
  26:      // Tell the provider, that the request has started.
  27:      this.fireRequestStarted = function(request) {
  28:          // Increment the request count
  29:          requestCounter.increment();               
  30:          //run each subscribed listener
  31:          angular.forEach(onRequestStartedListeners, function(listener) { 
  32:              // call the listener with request argument
  33:              listener(request);
  34:          });                
  35:          return request;
  36:      };
  37:   
  38:      // this is a complete analogy to the Request START
  39:      this.subscribeOnRequestEnded = function(listener) {
  40:          onRequestEndedListeners.push(listener);
  41:      };
  42:   
  43:   
  44:      this.fireRequestEnded = function() {
  45:          requestCounter.decrement();
  46:          var passedArgs = arguments;
  47:          angular.forEach(onRequestEndedListeners, function(listener) {
  48:              listener.apply(this, passedArgs);
  49:          });
  50:          return arguments[0];
  51:      };
  52:   
  53:      this.getRequestCount = requestCounter.getCount;
  54:   
  55:      //This will be returned as a service
  56:      this.$get = function() {
  57:          var that = this;
  58:          // just pass all the functions
  59:          return {
  60:              subscribeOnRequestStarted: that.subscribeOnRequestStarted,
  61:              subscribeOnRequestEnded: that.subscribeOnRequestEnded,
  62:              fireRequestEnded: that.fireRequestEnded,
  63:              fireRequestStarted: that.fireRequestStarted,
  64:              getRequestCount: that.getRequestCount
  65:          };
  66:      };
  67:  }
  68:  );



The provider methods (this.someFunction) can be accessed during the configuration phase of the application. So I add the request/response transformers:


   1:  angular.module('myApp', [
   2:      'ngRoute',
   3:      'myApp.filters',
   4:      'myApp.services',
   5:      'myApp.directives',
   6:      'myApp.controllers'
   7:  ])
   8:          .config(function(
   9:          $httpProvider,
  10:          requestNotificationProvider) {
  11:              $httpProvider
  12:                      .defaults
  13:                      .transformRequest
  14:                      .push(function(data) {
  15:                          requestNotificationProvider
  16:                                  .fireRequestStarted(data);
  17:                          return data;
  18:                      });
  19:   
  20:              $httpProvider
  21:                      .defaults
  22:                      .transformResponse
  23:                      .push(function(data) {
  24:                          requestNotificationProvider
  25:                                  .fireRequestEnded(data);
  26:                          return data;
  27:                      });
  28:          });

Finally, I create a directive which listens onRequestStarted/Ended. It shows/hides the element with the directive accordingly. You can place a nice animated gif to indicate a pending request.


   1:  angular.module('myApp.directives')
   2:    .directive('loadingWidget', function (requestNotification) {
   3:      return {
   4:          restrict: "AC",
   5:          link: function (scope, element) {
   6:              // hide the element initially
   7:              element.hide();
   8:   
   9:              //subscribe to listen when a request starts
  10:              requestNotification
  11:                      .subscribeOnRequestStarted(function() {
  12:                  // show the spinner!
  13:                  element.show();
  14:              });
  15:   
  16:              requestNotification
  17:                      .subscribeOnRequestEnded(function() {
  18:                  // hide the spinner if there are no more pending requests
  19:                  if (requestNotification.getRequestCount() === 0)
  20:                      element.hide();
  21:              });
  22:          }
  23:      };
  24:    });

Note that requestNotificationProvider has become a service to use by the directive, thus its injected as requestNotification. We can access all the methods of the object returned by the $get function.


Demo


See the attached JSFiddle for a demonstration of how it works.


Feel free to use the code in your projects. If you liked this post, like & share it. Or give me a Flattr.


And how do you do it? See a bug in my code? Do you have a different approach? Leave a comment!

No comments:

Post a Comment