2

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();
                }
            });
        }
    };
}]);
Community
  • 1
  • 1
Tiago
  • 4,233
  • 13
  • 46
  • 70
  • Remember that Angular comes with it's own `$timeout` functionality, which in most cases is what you should use instead of `setTimeout`. My understanding is that `$timeout` waits until the digest loop has completed before running, whereat `setTimeout` is not aware of the digest loop. – random_user_name Jan 08 '16 at 14:32
  • Were you able to solve this? Was my answer able to point you in the right direction? Please share your findings when you can – scniro Jan 11 '16 at 14:37
  • @scniro Will do. Haven't had the chance to actually test it because some other things came up but I didn't forget about it. It does seem to be the right approach and might actually end up solving some other issues with the way I'm currently handling requests in my project. Thanks a lot! – Tiago Jan 11 '16 at 14:42
  • @Tiago cool, good to hear. Just keep me posted with your findings – scniro Jan 11 '16 at 16:51
  • how about you checkout my factory [angular-httpshooter](https://www.npmjs.com/package/angular-httpshooter), it gives you better control for showing spinners or freezing ui, even blocking multiple calls – Siddharth Jan 08 '17 at 07:39

2 Answers2

2

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.)

Daniel Beck
  • 20,653
  • 5
  • 38
  • 53
2

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

scniro
  • 16,844
  • 8
  • 62
  • 106