Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a loading indicator for every asynchronous action (using $q) in an angularjs-app

I know there are several approaches to loading-indicators in angular js (this one, for example: https://gist.github.com/maikeldaloo/5140733).

But they either have to be configured for every single call, or - if they act globally, as I want - just apply to http-requests, but not to $q-promises being used in services.

The global loading indicators, I've seen so far, work with

$httpProvider.responseInterceptors.push(interceptor);

Is there something similiar for $q, like a $qProvider.reponseInterceptors? And if not, what would be the most convenient way to implement such a functionality? Is it possible to use a decorator-pattern of some kind for example?

like image 925
hugo der hungrige Avatar asked Jul 05 '13 18:07

hugo der hungrige


4 Answers

Although I find it very complicated, unnecessary and probably broken, you could decorate $q and override its defer function.

Every time someone asks for a new defer() it runs your own version which also increments a counter. Before handing out the defer object, you register a finally callback (Angular 1.2.0 only but always may fit, too) to decrement the counter.

Finally, you add a watch to $rootScope to monitor when this counter is greater than 0 (faster than having pendingPromisses in $rootScope and bind like ng-show="pendingPromisses > 0").

app.config(function($provide) {
    $provide.decorator('$q', ['$delegate', '$rootScope', function($delegate, $rootScope) {
      var pendingPromisses = 0;
      $rootScope.$watch(
        function() { return pendingPromisses > 0; }, 
        function(loading) { $rootScope.loading = loading; }
      );
      var $q = $delegate;
      var origDefer = $q.defer;
      $q.defer = function() {
        var defer = origDefer();
        pendingPromisses++;
        defer.promise.finally(function() {
          pendingPromisses--;
        });
        return defer;
      };
      return $q;
    }]);
});

Then, view bound to a scope that inherits from $rootScope can have:

<span ng-show="loading">Loading, please wait</span>

(this won't work in directives with isolate scopes)

See it live here.

like image 152
Kos Prov Avatar answered Nov 16 '22 20:11

Kos Prov


There is a good example in the official documentation working for the current stable 1.2.0.

http://docs.angularjs.org/api/ng.$http (top quarter of the page, search for Interceptors)

My extraction of these documentation lead me to this solution:

angular.module('RequestInterceptor', [])
  .config(function ($httpProvider) {
    $httpProvider.interceptors.push('requestInterceptor');
  })
  .factory('requestInterceptor', function ($q, $rootScope) {
    $rootScope.pendingRequests = 0;
    return {
           'request': function (config) {
                $rootScope.pendingRequests++;
                return config || $q.when(config);
            },

            'requestError': function(rejection) {
                $rootScope.pendingRequests--;
                return $q.reject(rejection);
            },

            'response': function(response) {
                $rootScope.pendingRequests--;
                return response || $q.when(response);
            },

            'responseError': function(rejection) {
                $rootScope.pendingRequests--;
                return $q.reject(rejection);
            }
        }
    });

You might then use pendingRequests>0 in an ng-show expression.

like image 25
angabriel Avatar answered Nov 16 '22 20:11

angabriel


Since requested by the OP, this is based on the method we are using for the app we are currently working on. This method does NOT change the behaviour of $q, rather adds a very simple API to handle promises that need some kind of visual indication. Although this needs modification in every place it is used, it is only a one-liner.

Usage

There is a service, say ajaxIndicator, that knows how to update a portion of the UI. Whenever a promise-like object needs to provide indication until the promise is resolved we use:

// $http example:
var promise = $http.get(...);
ajaxIndicator.indicate(promise); // <--- this line needs to be added

If you do not want to keep a reference to the promise:

// $http example without keeping the reference:
ajaxIndicator.indicate($http.get(...));

Or with a resource:

var rc = $resource(...);
...
$scope.obj = rc.get(...);
ajaxIndicator.indicate($scope.obj);

(NOTE: For Angular 1.2 this would need tweeking, as there is no $then() on the resource object.)

Now in the root template, you will have to bind the indicator to $rootScope.ajaxActive, e.g.:

<div class="ajax-indicator" ng-show="ajaxActive"></div>

Implementation

(Modified from our source.) WARNING: This implementation does not take into account nested calls! (Our requirements called for UI blocking, so we do not expect nested calls; if interested I could try to enhance this code.)

app.service("ajaxIndicator", ["$rootScope"], function($rootScope) {
    "use strict";

    $rootScope.ajaxActive = false;

    function indicate(promise) {
        if( !$rootScope.ajaxActive ) {
            $rootScope.ajaxActive = true;
            $rootScope.$broadcast("ajax.active"); // OPTIONAL
            if( typeof(promise) === "object" && promise !== null ) {
                if( typeof(promise.always) === "function" ) promise.always(finished);
                else if( typeof(promise.then) === "function" ) promise.then(finished,finished);
                else if( typeof(promise.$then) === "function" ) promise.$then(finished,finished);
            }
        }
    }

    function finished() {
        $rootScope.ajaxActive = false;
    }

    return {
        indicate: indicate,
        finished: finished
    };
});
like image 5
Nikos Paraskevopoulos Avatar answered Nov 16 '22 21:11

Nikos Paraskevopoulos


I had the same big question few weeks ago and I happen to make some directives to represent the loading state on the action buttons and ng-repeat content loading.

I just spent some time and pushed it on github: https://github.com/ocolot/angularjs_loading_buttons

I hope it helps.

like image 1
ocolot Avatar answered Nov 16 '22 21:11

ocolot