236

I am using the $http service of AngularJS to make an Ajax request.

How can a spinner GIF (or another type of busy indicator) be shown while the Ajax request is executing?

I don't see anything like an ajaxstartevent in the AngularJS documentation.

DavidRR
  • 18,291
  • 25
  • 109
  • 191
Ajay Beniwal
  • 18,857
  • 9
  • 81
  • 99
  • 2
    If you want a simple spinner based on HTTP Interceptors, I have an angular module for that. It uses the popular Identified Sham spinner. Take a look: https://github.com/harinair/angular-sham-spinner – Hari Gangadharan Nov 16 '13 at 04:46
  • 1
    I wrote a plugin [angular-httpshooter](https://www.npmjs.com/package/angular-httpshooter), it releases an event with config data just before the call shooting and release another just after recieving resposne, you can write global loaders catching those events – Siddharth Jan 08 '17 at 07:37

27 Answers27

472

This really depends on your specific use case, but a simple way would follow a pattern like this:

.controller('MainCtrl', function ( $scope, myService ) {
  $scope.loading = true;
  myService.get().then( function ( response ) {
    $scope.items = response.data;
  }, function ( response ) {
    // TODO: handle the error somehow
  }).finally(function() {
    // called no matter success or failure
    $scope.loading = false;
  });
});

And then react to it in your template:

<div class="spinner" ng-show="loading"></div>
<div ng-repeat="item in items>{{item.name}}</div>
Tom
  • 7,640
  • 1
  • 23
  • 47
Josh David Miller
  • 120,525
  • 16
  • 127
  • 95
  • 225
    If you have multiple ajax requests at the same time, you can declare `loading` as an integer `$scope.loading = 0;`, when a request starts you do `$scope.loading++;`, and when it ends you do `$scope.loading--;`. And there is nothing to change in the template. So the spinner is displayed when there is at least one request in progress, and is hidden the rest of the time – JMaylin Oct 17 '13 at 15:43
  • 16
    You probably should set loading to false in the error fallback function as well. – sebnukem Feb 06 '14 at 21:18
  • 3
    @sebnukem Right you are. This was a pretty high-level example, but I went ahead and added it anyway for clarity. Thanks for the feedback! – Josh David Miller Feb 06 '14 at 22:30
  • 1
    @JMaylin - you can then use $watch on $scope.loading do do other things – Jason Apr 09 '14 at 01:14
  • 1
    Much more clean and Angular-ish way of doing it! – Mads K Jun 21 '14 at 16:21
  • 5
    An even cleaner way, instead of putting `$scope.loading = false` in both the success and the fail callback, is to put it in the `.finally()`. It would look like: `mySvc.get().then(success, fail).finally(function() { $scope.loading = false; });` – Tom Dec 11 '14 at 19:34
  • 1
    @Tom You're right, but this answer was written before any of the promise sugar was added to `$q`. Feel free to give it a little edit. – Josh David Miller Dec 11 '14 at 21:33
  • As a comment on @JMaylin comment. The question is, why we need to handle multiple ajax requests while our spinner already has a css style to disable all links on our page ? Thanks. and, if we have changed the css style to allow us to interact with the web page. what is the usage of the spinner at this time ? – user1510006 Apr 24 '15 at 01:23
  • 1
    The reason I had to use this as opposed to a directive is that the directives typically tap into the `$http` service and _any_ http call loads the spinner overlay. The problem is if there are background operations occurring async unrelated to the focused operation, the overlay could unnecessarily remain on the screen longer than it would appear it should. I had to make the code more isolated to the _specific_ http call being made like in the controller sample above. – atconway Sep 23 '15 at 22:22
  • 1
    Nice solution, I am using exactly the same approach. However, I have found that I have a small problem. My spinner is an animated GIF and the first time the page is loaded, the browser will fetch the GIF image only after the Ajax request has finished. The end result is that for the first time the spinner is frozen :) Do you have any idea how can I avoid this ? – Dan Corneanu Dec 09 '15 at 02:35
  • 1
    @DanCorneanu I don't recommend using an animated gif for UI stuff unless you have a compelling reason to do so. It would be much better to use a CSS-based solution. However, you can preload the image if you need to. But I'm not sure I fully understand the issue you're calling out. – Josh David Miller Dec 18 '15 at 04:41
  • sir i am using this but in my case on a class ng-hide class is applied on a class="spinner ng-hide" like this so loader is loaded please tell m anyone how to remove ng-hide class? – Kapil Soni Mar 06 '18 at 06:06
  • spinner class ? – Adit choudhary Aug 20 '18 at 09:10
88

Here are the current past AngularJS incantations:

angular.module('SharedServices', [])
    .config(function ($httpProvider) {
        $httpProvider.responseInterceptors.push('myHttpInterceptor');
        var spinnerFunction = function (data, headersGetter) {
            // todo start the spinner here
            //alert('start spinner');
            $('#mydiv').show();
            return data;
        };
        $httpProvider.defaults.transformRequest.push(spinnerFunction);
    })
// register the interceptor as a service, intercepts ALL angular ajax http calls
    .factory('myHttpInterceptor', function ($q, $window) {
        return function (promise) {
            return promise.then(function (response) {
                // do something on success
                // todo hide the spinner
                //alert('stop spinner');
                $('#mydiv').hide();
                return response;

            }, function (response) {
                // do something on error
                // todo hide the spinner
                //alert('stop spinner');
                $('#mydiv').hide();
                return $q.reject(response);
            });
        };
    });

//regular angular initialization continued below....
angular.module('myApp', [ 'myApp.directives', 'SharedServices']).
//.......

Here is the rest of it (HTML / CSS)....using

$('#mydiv').show(); 
$('#mydiv').hide(); 

to toggle it. NOTE: the above is used in the angular module at beginning of post

#mydiv {  
    position:absolute;
    top:0;
    left:0;
    width:100%;
    height:100%;
    z-index:1000;
    background-color:grey;
    opacity: .8;
 }

.ajax-loader {
    position: absolute;
    left: 50%;
    top: 50%;
    margin-left: -32px; /* -1 * image width / 2 */
    margin-top: -32px;  /* -1 * image height / 2 */
    display: block;     
}

<div id="mydiv">
    <img src="lib/jQuery/images/ajax-loader.gif" class="ajax-loader"/>
</div>
bulltorious
  • 7,769
  • 4
  • 49
  • 78
  • 19
    Note that you can use `angular.element('#mydiv').show()` instead of `$('#mydiv').show()` – JMaylin Oct 17 '13 at 15:34
  • 8
    this does not work if your page makes multiple ajax requests. It will hide the loading gif after the first requests ends. – Meeker Oct 22 '13 at 04:05
  • 38
    According to AngularJS’ best practices (which the accepted solution is violating) you shouldn't be modifying the DOM outside of an directive. Consider invoking show/hide on elements from within a directive. – Mariusz Jan 09 '14 at 01:42
  • +1 Perfect. I agree to Mariusz but somtimes we have to go against standards. – Amritpal singh Nov 04 '14 at 13:38
  • 1
    There is no real reason to 'go against standards' here. You could just as easily alter a value in a service that is watched by a directive if you are hell-bent on using an interceptor. – Dan Apr 14 '15 at 08:10
  • 7
    I'm not sure i know enough to comment... But wouldnt the right way to go about doing this be using the `ng-class` directive instead of using JQuery to show and hide elements? – James Heald Jan 11 '16 at 16:01
  • 3
    @JamesHeald is correct.. you should never really need to use jQuery for any dom manipulation in Angular. Look into: ng-class, ng-if, ng-hide, ng-show, etc.. there's a directive for nearly everything you'll ever need. – jKlaus Feb 08 '16 at 14:41
  • Why don't you just listen for `$routeChangeStart`, `$routeChangeSuccess`, and `$routeChangeError` and update UI only on those three parts? – Taku Aug 28 '16 at 07:01
  • I think the #mydiv position must be "fixed" instead of "absolute" so that when the page scrolls , the loader won't scroll along with the page – Sachin Murali G Sep 01 '16 at 16:24
  • This is not a good solution if you execute more than one request at a time. – Shannon Mar 26 '17 at 22:48
46

Here's a version using a directive and ng-hide.

This will show the loader during all calls via angular's $http service.

In the template:

<div class="loader" data-loading></div>

directive:

angular.module('app')
  .directive('loading', ['$http', function ($http) {
    return {
      restrict: 'A',
      link: function (scope, element, attrs) {
        scope.isLoading = function () {
          return $http.pendingRequests.length > 0;
        };
        scope.$watch(scope.isLoading, function (value) {
          if (value) {
            element.removeClass('ng-hide');
          } else {
            element.addClass('ng-hide');
          }
        });
      }
    };
}]);

by using the ng-hide class on the element, you can avoid jquery.


Customize: add an interceptor

If you create a loading-interceptor, you can show/hide the loader based on a condition.

directive:

var loadingDirective = function ($rootScope) {
  return function ($scope, element, attrs) {
      $scope.$on("loader_show", function () {
          return element.removeClass('ng-hide');
      });
      return $scope.$on("loader_hide", function () {
          return element.addClass('ng-hide');
      });
  };
};

interceptor:

  • for example: don't show spinner when response.background === true;
  • Intercept request and/or response to set $rootScope.$broadcast("loader_show"); or $rootScope.$broadcast("loader_hide");

more info on writing an interceptor

punkrockpolly
  • 9,270
  • 6
  • 36
  • 37
  • @punkrockpolly In my specific scenario, I have two indivual components calling a service. But this just works for one of them, Should I pass a parameter or something to make this reusable? – napstercake Sep 08 '15 at 16:24
  • @razorblade it will spin anytime you make a call via `$http` in any service. – punkrockpolly Sep 18 '15 at 19:26
31

If you are using ngResource, the $resolved attribute of an object is useful for loaders:

For a resource as follows:

var User = $resource('/user/:id', {id:'@id'});
var user = User.get({id: 1})

You can link a loader to the $resolved attribute of the resource object:

<div ng-hide="user.$resolved">Loading ...</div>
14

https://github.com/wongatech/angular-http-loader is a good project for this.

Example here http://wongatech.github.io/angular-http-loader/

The code below shows a template example/loader.tpl.html when a request is happening.

<div ng-http-loader template="example/loader.tpl.html"></div>
eddiec
  • 7,608
  • 5
  • 34
  • 36
13

Just discovered the angular-busy directive that shows a little loader depending on some async call.

For example, if you have to make a GET, reference the promise in your $scope,

$scope.req = $http.get('http://google.fr');

and call it like so :

<div cg-busy="req"></div>

Here is the GitHub.

You can also install it using bower (don't forget to update your project dependencies):

bower install angular-busy --save
Preview
  • 35,317
  • 10
  • 92
  • 112
  • I could not find how to "disable some element" in the documentation. Can you explain how ? For example, I want to disable a button while the spinner is shown. – c4k Apr 09 '15 at 15:07
  • *angular-busy* only put on your element a mask, I don't think you can disable a button as you want to do, but you can set the backdrop on it to with an empty template to give this impression. I'm not a native speaker, sorry for the confusion :) – Preview Apr 09 '15 at 15:28
  • Ok, I edited your answer to prevent someone else to have the same confusion. A French speaking English to another French is always confusing :) – c4k Apr 09 '15 at 16:59
5

For page loads and modals, the easiest way is to use the ng-show directive and use one of the scope data variables. Something like:

ng-show="angular.isUndefined(scope.data.someObject)".

Here, while someObject is undefined, the spinner will show. Once the service returns with data and someObject is populated, the spinner will return to its hidden state.

DavidRR
  • 18,291
  • 25
  • 109
  • 191
Arnold.Krumins
  • 1,055
  • 8
  • 8
5

If you're wrapping your api calls within a service/factory, then you can track the loading counter there (per answer and excellent simultaneous suggestion by @JMaylin), and reference the loading counter via a directive. Or any combination thereof.

API WRAPPER

yourModule
    .factory('yourApi', ['$http', function ($http) {
        var api = {}

        //#region ------------ spinner -------------

        // ajax loading counter
        api._loading = 0;

        /**
         * Toggle check
         */
        api.isOn = function () { return api._loading > 0; }

        /**
         * Based on a configuration setting to ignore the loading spinner, update the loading counter
         * (for multiple ajax calls at one time)
         */
        api.spinner = function(delta, config) {
            // if we haven't been told to ignore the spinner, change the loading counter
            // so we can show/hide the spinner

            if (NG.isUndefined(config.spin) || config.spin) api._loading += delta;

            // don't let runaway triggers break stuff...
            if (api._loading < 0) api._loading = 0;

            console.log('spinner:', api._loading, delta);
        }
        /**
         * Track an ajax load begin, if not specifically disallowed by request configuration
         */
        api.loadBegin = function(config) {
            api.spinner(1, config);
        }
        /**
         * Track an ajax load end, if not specifically disallowed by request configuration
         */
        api.loadEnd = function (config) {
            api.spinner(-1, config);
        }

        //#endregion ------------ spinner -------------

        var baseConfig = {
            method: 'post'
            // don't need to declare `spin` here
        }

        /**
         * $http wrapper to standardize all api calls
         * @param args stuff sent to request
         * @param config $http configuration, such as url, methods, etc
         */
        var callWrapper = function(args, config) {
            var p = angular.extend(baseConfig, config); // override defaults

            // fix for 'get' vs 'post' param attachment
            if (!angular.isUndefined(args)) p[p.method == 'get' ? 'params' : 'data'] = args;

            // trigger the spinner
            api.loadBegin(p);

            // make the call, and turn of the spinner on completion
            // note: may want to use `then`/`catch` instead since `finally` has delayed completion if down-chain returns more promises
            return $http(p)['finally'](function(response) {
                api.loadEnd(response.config);
                return response;
            });
        }

        api.DoSomething = function(args) {
            // yes spinner
            return callWrapper(args, { cache: true });
        }
        api.DoSomethingInBackground = function(args) {
            // no spinner
            return callWrapper(args, { cache: true, spin: false });
        }

        // expose
        return api;
    });

SPINNER DIRECTIVE

(function (NG) {
    var loaderTemplate = '<div class="ui active dimmer" data-ng-show="hasSpinner()"><div class="ui large loader"></div></div>';

    /**
     * Show/Hide spinner with ajax
     */
    function spinnerDirective($compile, api) {
        return {
            restrict: 'EA',
            link: function (scope, element) {
                // listen for api trigger
                scope.hasSpinner = api.isOn;

                // attach spinner html
                var spin = NG.element(loaderTemplate);
                $compile(spin)(scope); // bind+parse
                element.append(spin);
            }
        }
    }

    NG.module('yourModule')
        .directive('yourApiSpinner', ['$compile', 'yourApi', spinnerDirective]);
})(angular);

USAGE

<div ng-controller="myCtrl" your-api-spinner> ... </div>
Community
  • 1
  • 1
drzaus
  • 24,171
  • 16
  • 142
  • 201
3

This is the easiest way to add a spinner i guess:-

You can use ng-show with the div tag of any one of these beautiful spinners http://tobiasahlin.com/spinkit/ {{This is not my page}}

and then you can use this kind of logic

//ajax start
    $scope.finderloader=true;
    
          $http({
    method :"POST",
    url : "your URL",
  data: { //your data
     
     }
  }).then(function mySucces(response) {
    $scope.finderloader=false;
      $scope.search=false;          
    $scope.myData =response.data.records;
  });
     
    //ajax end 
    
<div ng-show="finderloader" class=spinner></div>
//add this in your HTML at right place
3
Based on Josh David Miller response:

  <body>
  <header>
  </header>
<div class="spinner" ng-show="loading">
  <div class="loader" ></div>
</div>

<div ng-view=""></div>

<footer>
</footer>

</body>

Add this css:

    .loader {
  border: 16px solid #f3f3f3;
  border-radius: 50%;
  border-top: 16px solid #3498db;
  border-bottom : 16px solid black;
  width: 80px;
  height: 80px;
  -webkit-animation: spin 2s linear infinite;
  animation: spin 2s linear infinite;
  position: absolute;
  top: 45%;
  left: 45%;
}

@-webkit-keyframes spin {
  0% { -webkit-transform: rotate(0deg); }
  100% { -webkit-transform: rotate(360deg); }
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}


.spinner{
  width: 100%;
height: 100%;
z-index: 10000;
position: absolute;
top: 0;
left: 0;
margin: 0 auto;
text-align: center;
vertical-align: middle;
background: white;
opacity: 0.6;
}

And just in your angular add:

$rootScope.loading = false; $rootScope.loading = true; -> when $http.get ends.

cabaji99
  • 1,305
  • 11
  • 9
2

Sharing my version of the great answer from @bulltorious, updated for newer angular builds (I used version 1.5.8 with this code), and also incorporated @JMaylin's idea of using a counter so as to be robust to multiple simultaneous requests, and the option to skip showing the animation for requests taking less than some minimum number of milliseconds:

var app = angular.module('myApp');
var BUSY_DELAY = 1000; // Will not show loading graphic until 1000ms have passed and we are still waiting for responses.

app.config(function ($httpProvider) {
  $httpProvider.interceptors.push('busyHttpInterceptor');
})
  .factory('busyHttpInterceptor', ['$q', '$timeout', function ($q, $timeout) {
    var counter = 0;
    return {
      request: function (config) {
        counter += 1;
        $timeout(
          function () {
            if (counter !== 0) {
              angular.element('#busy-overlay').show();
            }
          },
          BUSY_DELAY);
        return config;
      },
      response: function (response) {
        counter -= 1;
        if (counter === 0) {
          angular.element('#busy-overlay').hide();
        }
        return response;
      },
      requestError: function (rejection) {
        counter -= 1;
        if (counter === 0) {
          angular.element('#busy-overlay').hide();
        }
        return rejection;
      },
      responseError: function (rejection) {
        counter -= 1;
        if (counter === 0) {
          angular.element('#busy-overlay').hide();
        }
        return rejection;
      }
    }
  }]);
qwwqwwq
  • 6,999
  • 2
  • 26
  • 49
2

You can use angular interceptor to manage http request calls

  <div class="loader">
    <div id="loader"></div>
  </div>

<script>
    var app = angular.module("myApp", []);

    app.factory('httpRequestInterceptor', ['$rootScope', '$location', function ($rootScope, $location) {
        return {
            request: function ($config) {
                $('.loader').show();
                return $config;
            },
            response: function ($config) {
                $('.loader').hide();
                return $config;
            },
            responseError: function (response) {
                return response;
            }
        };
    }]);

    app.config(['$stateProvider', '$urlRouterProvider', '$httpProvider',
        function ($stateProvider, $urlRouterProvider, $httpProvider) {
            $httpProvider.interceptors.push('httpRequestInterceptor');
        }]);

</script>

https://stackoverflow.com/a/49632155/4976786

Om Prakash Sharma
  • 1,682
  • 22
  • 13
2

Simple way without interceptors or jQuery

This is a simple way to show a spinner that does not require a third-party library, intercepters, or jQuery.

In the controller, set and reset a flag.

function starting() {
    //ADD SPINNER
    vm.starting = true;
    $http.get(url)
      .then(function onSuccess(response) {
        vm.data = response.data;
    }).catch(function onReject(errorResponse) {
        console.log(errorResponse.status);
    }).finally(function() {
        //REMOVE SPINNER
        vm.starting = false;
    });
};

In the HTML, use the flag:

<div ng-show="vm.starting">
    <img ng-src="spinnerURL" />
</div>

<div ng-hide="vm.starting">
    <p>{{vm.data}}</p>
</div>

The vm.starting flag is set true when the XHR starts and cleared when the XHR completes.

Community
  • 1
  • 1
georgeawg
  • 48,608
  • 13
  • 72
  • 95
1

This works well for me:

HTML:

  <div id="loader" class="ng-hide" ng-show="req.$$state.pending">
    <img class="ajax-loader" 
         width="200" 
         height="200" 
         src="/images/spinner.gif" />
  </div>

Angular:

  $scope.req = $http.get("/admin/view/"+id).success(function(data) {          
      $scope.data = data;
  });

While the promise returned from $http is pending, ng-show will evaluate it to be "truthy". This is automatically updated once the promise is resolved... which is exactly what we want.

dank
  • 579
  • 3
  • 11
  • This is not the dynamic way to do that. – Umair Hamid Jun 03 '16 at 05:51
  • Could you please explain why you believe this is not a dynamic way to achieve this goal. This will help others reading this post learn. Thank you. – dank Jun 07 '16 at 21:52
  • I have multiple request in in my application. $scope.req will only work for single request. When this particular request complete loader will hide and will not wait for completing other requests. The best way to achieve this is to use interceptors. – Umair Hamid Jun 08 '16 at 00:24
1

Used following intercepter to show loading bar on http request

'use strict';
appServices.factory('authInterceptorService', ['$q', '$location', 'localStorage','$injector','$timeout', function ($q, $location, localStorage, $injector,$timeout) {

var authInterceptorServiceFactory = {};
var requestInitiated;

//start loading bar
var _startLoading = function () {
   console.log("error start loading");
   $injector.get("$ionicLoading").show();

}

//stop loading bar
var _stopLoading = function () {
    $injector.get("$ionicLoading").hide();
}

//request initiated
var _request = function (config) {
     requestInitiated = true;
    _startLoading();
    config.headers = config.headers || {};
    var authDataInitial = localStorage.get('authorizationData');
    if (authDataInitial && authDataInitial.length > 2) {
        var authData = JSON.parse(authDataInitial);
        if (authData) {
            config.headers.Authorization = 'Bearer ' + authData.token;
        }
    }
    return config;
}

//request responce error
var _responseError = function (rejection) {
   _stopLoading();
    if (rejection.status === 401) {
        $location.path('/login');
    }
    return $q.reject(rejection);
}

//request error
var _requestError = function (err) {
   _stopLoading();
   console.log('Request Error logging via interceptor');
   return err;
}

//request responce
var _response = function(response) {
    requestInitiated = false;

   // Show delay of 300ms so the popup will not appear for multiple http request
   $timeout(function() {

        if(requestInitiated) return;
        _stopLoading();
        console.log('Response received with interceptor');

    },300);

return response;
}



authInterceptorServiceFactory.request = _request;
authInterceptorServiceFactory.responseError = _responseError;
authInterceptorServiceFactory.requestError = _requestError;
authInterceptorServiceFactory.response = _response;

return authInterceptorServiceFactory;
}]);
Dinesh Vaitage
  • 2,983
  • 19
  • 16
0
.factory('authHttpResponseInterceptor', ['$q', function ($q) {
        return {
            request: function(config) {
                angular.element('#spinner').show();
                return config;
            },
            response : function(response) {
                angular.element('#spinner').fadeOut(3000);
                return response || $q.when(response);
            },
            responseError: function(reason) {
                angular.element('#spinner').fadeOut(3000);
                return $q.reject(reason);
            }
        };
    }]);



 .config(['$routeProvider', '$locationProvider', '$translateProvider', '$httpProvider',
            function ($routeProvider, $locationProvider, $translateProvider, $httpProvider) {
                $httpProvider.interceptors.push('authHttpResponseInterceptor');
    }
]);

in your Template
<div id="spinner"></div>


css   

#spinner,
#spinner:after {
  border-radius: 50%;
  width: 10em;
  height: 10em;
  background-color: #A9A9A9;
  z-index: 10000;
  position: absolute;
  left: 50%;
  bottom: 100px;
}
@-webkit-keyframes load8 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@keyframes load8 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
0

create directive with this code:

$scope.$watch($http.pendingRequests, toggleLoader);

function toggleLoader(status){
  if(status.length){
    element.addClass('active');
  } else {
    element.removeClass('active');
  }
}
Oleg Ozimok
  • 259
  • 2
  • 4
0

Another solution to show loading between different url changes is:

$rootScope.$on('$locationChangeStart', function() {
  $scope.loading++;
});

$rootScope.$on('$locationChangeSuccess', function() {
  $timeout(function() {
    $scope.loading--;
  }, 300);
});

And then in the markup just toggle the spinner with ng-show="loading".

If you want to display it on ajax requests just add $scope.loading++ when the request starts and when it ends add $scope.loading--.

Chrillewoodz
  • 27,055
  • 21
  • 92
  • 175
0

You can try something like this as well:

Create directive :

myApp.directive('loader', function () {
    return {
        restrict: 'A',
        scope: {cond: '=loader'},
        template: '<span ng-if="isLoading()" class="soft"><span class="fa fa-refresh fa-spin"></span></span>',
        link: function (scope) {
            scope.isLoading = function() {
                var ret = scope.cond === true || (
                        scope.cond &&
                        scope.cond.$$state &&
                        angular.isDefined(scope.cond.$$state.status) &&
                        scope.cond.$$state.status === 0
                    );
                return ret;
            }
        }
    };
}); 

Then you add something like this to mainCtrl

    // Return TRUE if some request is LOADING, else return FALSE
    $scope.isLoading = function() {
        return $http.pendingRequests.length > 0;
    };

And HTML can looks like this:

<div class="buttons loader">
    <span class="icon" loader="isLoading()"></span>
</div>
Andurit
  • 5,612
  • 14
  • 69
  • 121
0

The following way will take note of all requests, and hide only once all requests are done:

app.factory('httpRequestInterceptor', function(LoadingService, requestCount) {
    return {
        request: function(config) {
            if (!config.headers.disableLoading) {
                requestCount.increase();
                LoadingService.show();
            }
            return config;
        }
    };
}).factory('httpResponseInterceptor', function(LoadingService, $timeout, error, $q, requestCount) {
    function waitAndHide() {
        $timeout(function() {
            if (requestCount.get() === 0){
                LoadingService.hide();
            }
            else{
                waitAndHide();
            }
        }, 300);
    }

    return {
        response: function(config) {
            requestCount.descrease();
            if (requestCount.get() === 0) {
                waitAndHide();
            }
            return config;
        },
        responseError: function(config) {
            requestCount.descrease();
            if (requestCount.get() === 0) {
                waitAndHide();
            }
            var deferred = $q.defer();
            error.show(config.data, function() {
                deferred.reject(config);
            });
            return deferred.promise;
        }
    };
}).factory('requestCount', function() {
    var count = 0;
    return {
        increase: function() {
            count++;
        },
        descrease: function() {
            if (count === 0) return;
            count--;
        },
        get: function() {
            return count;
        }
    };
})
Yerken
  • 1,944
  • 1
  • 14
  • 18
0

Since the functionality of position:fixed changed recently, I had difficulty showing the gif loader above all elements, so I had to use angular's inbuilt jQuery.

Html

<div ng-controller="FetchController">
      <div id="spinner"></div>
</div>

Css

#spinner {display: none}
body.spinnerOn #spinner { /* body tag not necessary actually */
   display: block;
   height: 100%;
   width: 100%;
   background: rgba(207, 13, 48, 0.72) url(img/loader.gif) center center no-repeat;
   position: fixed;
   top: 0;
   left: 0;
   z-index: 9999;
}
body.spinnerOn main.content { position: static;} /* and whatever content needs to be moved below your fixed loader div */

Controller

app.controller('FetchController', ['$scope', '$http', '$templateCache', '$location', '$q',
function($scope, $http, $templateCache, $location, $q) {

angular.element('body').addClass('spinnerOn'); // add Class to body to show spinner

$http.post( // or .get(
    // your data here
})
.then(function (response) {
    console.info('success');     
    angular.element('body').removeClass('spinnerOn'); // hide spinner

    return response.data;               
}, function (response) {                   
    console.info('error'); 
    angular.element('body').removeClass('spinnerOn'); // hide spinner
});

})

Hope this helps :)

Magor Menessy
  • 381
  • 4
  • 13
0

All answers are or to complicated, or need to set some variables on every request which is very wrong practice if we know the DRY concept. Here simple interceptor example, I set mouse on wait when ajax starts and set it to auto when ajax ends.

$httpProvider.interceptors.push(function($document) {
    return {
     'request': function(config) {
         // here ajax start
         // here we can for example add some class or show somethin
         $document.find("body").css("cursor","wait");

         return config;
      },

      'response': function(response) {
         // here ajax ends
         //here we should remove classes added on request start

         $document.find("body").css("cursor","auto");

         return response;
      }
    };
  });

Code has to be added in application config app.config. I showed how to change mouse on loading state but in there it is possible to show/hide any loader content, or add, remove some css classes which are showing the loader.

Interceptor will run on every ajax call, so no need to create special boolean variables ( $scope.loading=true/false etc. ) on every http call.

Maciej Sikora
  • 19,374
  • 4
  • 49
  • 50
0

Here is my implementation, as simple as a ng-show and a request counter.

It use a new service for all request to $http:

myApp.service('RqstSrv', [ '$http', '$rootScope', function($http, $rootScope) {
    var rqstService = {};

    rqstService.call = function(conf) {

        $rootScope.currentCalls = !isNaN($rootScope.currentCalls) ?  $rootScope.currentCalls++ : 0;

        $http(conf).then(function APICallSucceed(response) {
            // Handle success
        }, function APICallError(response) {
            // Handle error
        }).then(function() {
            $rootScope.currentCalls--;
        });
    }
} ]);

And then you can use your loader base on the number of current calls:

<img data-ng-show="currentCalls > 0" src="images/ajax-loader.gif"/>
0

if you want to show loader for every http request call then you can use angular interceptor to manage http request calls ,

here is a sample code

<body data-ng-app="myApp">
<div class="loader">
    <div id="loader"></div>
</div>

<script>
    var app = angular.module("myApp", []);

    app.factory('httpRequestInterceptor', ['$rootScope', '$location', function ($rootScope, $location) {
        return {
            request: function ($config) {
                $('.loader').show();
                return $config;
            },
            response: function ($config) {
                $('.loader').hide();
                return $config;
            },
            responseError: function (response) {
                return response;
            }
        };
    }]);

    app.config(['$stateProvider', '$urlRouterProvider', '$httpProvider',
        function ($stateProvider, $urlRouterProvider, $httpProvider) {
            $httpProvider.interceptors.push('httpRequestInterceptor');
        }]);

</script>
</body>
Om Prakash Sharma
  • 1,682
  • 22
  • 13
0

Just use ng-show and a boolean

No need to use a directive, no need to get complicated.

Here is the code to put next to submit button or wherever you want the spinner to be:

<span ng-show="dataIsLoading">
  <img src="http://www.nasa.gov/multimedia/videogallery/ajax-loader.gif" style="height:20px;"/>
</span>

And then in your controller:

$scope.dataIsLoading = true

let url = '/whatever_Your_URL_Is'
$http.get(url)
.then(function(response) {
  $scope.dataIsLoading = false
})
Community
  • 1
  • 1
Adam
  • 2,070
  • 1
  • 14
  • 18
  • I agree with your code, it's how I do it, simple and works like a charm, perhaps the only thing I'd change in your code is that instead of using a boolean, use an int, instead of setting to true add 1 to the int, and deduct in response. -- In Html then you do -- This will allow multiple calls to be made at the same time and only remove the loader when all are finished – Clayton C Dec 04 '20 at 09:54
0

Adding onto @Adam's answer,

Use ng-show as suggested, however, in your case you want the functionality to have multiple requests and await all of them before the loader is hidden.

<span ng-show="pendingRequests > 0">
    <img src="http://www.nasa.gov/multimedia/videogallery/ajax-loader.gif" style="height:20px;"/>
</span>

And then in your controller:

$scope.pendingRequests++;

let url = '/whatever_Your_URL_Is'
$http.get(url)
  .then(function(response) {
      $scope.pendingRequests--;
})
Clayton C
  • 531
  • 1
  • 4
  • 19
-1

Here is my solution which i feel is alot easer that the other posted here. Not sure how "pretty" it is though, but it solved all my issues

I have a css style called "loading"

.loading { display: none; }

The html for the loading div can be whatever but I used some FontAwesome icons and the spin method there:

<div style="text-align:center" ng-class="{ 'loading': !loading }">
    <br />
    <h1><i class="fa fa-refresh fa-spin"></i> Loading data</h1>
</div>

On the elements that you want to hide you simply write this:

<something ng-class="{ 'loading': loading }" class="loading"></something>

and in the function i just set this on load.

(function (angular) {
    function MainController($scope) {
        $scope.loading = true

I am using SignalR so in the hubProxy.client.allLocks function (when its done going through the locks) I juts put

 $scope.loading = false
 $scope.$apply();

This also hides the {{someField}} when the page is loading since I am setting the loading class on load and AngularJS removes it afterwards.

stibay
  • 1,200
  • 6
  • 23
  • 44