Tiago
Tiago

Reputation: 4470

AngularJS: Show loading spinner on ajax request only after some time has elapsed

In my Angular app, I have implemented this directive (code below) that basically allows me to show an element of my choosing whenever Angular detects an ajax request.

However, for slightly better usability, I would like to show the spinner only after some time has passed (say, 100 or 200 miliseconds) since the beginning of the request, to avoid those unnecessary split-second displays on every single request.

What would be the best way to implement such a thing? I'm having trouble getting setTimeout to play nicely within the if block because the element will never get hidden again, even if I no longer have a pending request.

.directive('loading',   ['$http' ,function ($http)
{
    return {
        restrict: 'A',
        link: function (scope, elm, attrs)
        {
            scope.isLoading = function () {
                return $http.pendingRequests.length > 0;
            };

            scope.$watch(scope.isLoading, function (v)
            {
                if(v){
                    elm.show();
                } else {
                    elm.hide();
                }
            });
        }
    };
}]);

Upvotes: 2

Views: 3653

Answers (2)

scniro
scniro

Reputation: 16979

Sounds like you can leverage interceptors and bind to a root variable instead of a directive to show your element for pending ajax requests (after the time threshold is met). Observe the following possibility...

app.factory('HttpInterceptor', ['$rootScope', '$q', '$timeout', function ($rootScope, $q, $timeout) {

    return {
        'request': function (config) {

            $timeout(function() {
                $rootScope.isLoading = true;    // loading after 200ms
            }, 200);

            return config || $q.when(config);   
        },
        'requestError': function (rejection) {
            /*...*/
            return $q.reject(rejection);
        },
        'response': function (response) {       

            $rootScope.isLoading = false;       // done loading

            return response || $q.when(response);
        },
        'responseError': function (rejection) {
            /*...*/
            return $q.reject(rejection);
        }
    };
}]);

// register interceptor
app.config(['$httpProvider', function ($httpProvider) {
    $httpProvider.interceptors.push('HttpInterceptor');
    /*...*/
}]);

<!-- plain element with binding -->
<div class="whatever" ng-show="isLoading"></div>

JSFiddle Link - working demo

Upvotes: 2

Daniel Beck
Daniel Beck

Reputation: 21475

For a single, globally-available loading indicator, an http interceptor is probably a better strategy. But assuming you want to attach this to individual elements separately, try something like this:

.directive('loading', ['$http', '$timeout', function($http, $timeout) {
    return {
        restrict: 'A',
        link: function(scope, elm, attrs) {
            scope.isLoading = function() {
                return $http.pendingRequests.length > 0;
            };

            if (scope.isLoading) {
                elm.hide(); // hide the loading indicator to begin with
                // wait 300ms before setting the watcher:
                $timeout(function() {
                    var watcher = scope.$watch(scope.isLoading, function(v) {
                        if (v) {
                            elm.show();
                        } else {
                            elm.hide();
                            watcher(); // don't forget to clear $watches when you don't need them anymore!
                        }
                    });
                }, 300);
            } else {
                // No pending requests on link; hide the element and stop
                elm.hide();

            }
        }
    };
}]);

(You should probably also include a $destroy block on the directive to call watcher(), in case the directive goes out of scope while http requests are still pending.)

Upvotes: 2

Related Questions