1

I am making a page with AngularJS and some jQuery plugins in which the angular-js controller initializes its model by calling some servlet methods via ajax asynchronously. I want to show a loading gif when the first ajax call starts and hide it when the last ajax call finishes.

Since I don't know which will be the last ajax call to finish, I cannot know where to place the instructions to hide the loading gif. How can I achieve this behavior?

A code example of what i want:

myApp.controller("adminCtrl", function($scope, $http) {
    $scope.initData1 = function() {
        /* is this the first call? then show the gif */
        $http(...).success(function() { /* is this the last response received? then hide the gif */});
    }

    $scope.initData2 = function() {
        /* is this the first call? then show the gif */
        $http(...).success(function() { /* is this the last response received? then hide the gif */});
    }

    $scope.initData3 = function() {
        /* is this the first call? then show the gif */
        $http(...).success(function() { /* is this the last response received? then hide the gif */});
    }

    initData1();
    initData2();
    initData3();
}

I hope you understand my problem and know any way to achieve this.

Roberto Linares
  • 2,215
  • 3
  • 23
  • 35
  • I accept @Valentin Waeselynck's answer since it is reusable in multiple controllers (which is my case) and because I'm not certain if Chandermani's answer using promises would work in browsers like IE8+ (which is also my case). Even though, I consider Chandermani's answer about promises the best approach for modern browsers. The loading bar is also cool. – Roberto Linares Sep 30 '14 at 16:19

4 Answers4

2

Look at progress bar such as http://chieffancypants.github.io/angular-loading-bar/ that can show progress when ever a http request is made. It is basically a http interceptor that tracks the number the http requests and show\hides the progress bar accordingly.

If you want to solve this specific scenario, you can use $q.all to achieve the behaviour. Firstly for all init* functions return the http promise

 $scope.initData1 = function() {
        /* is this the first call? then show the gif */
        var promise = $http(...);
        promise.success(function(data) { // handle data});
        return promise;
    }

Now in the calling code just do

//show animation
$q.all([$sope.initData1(),$sope.initData2(),$sope.initData3()]).then(function(responseArray) {
   //hide animation
})
Chandermani
  • 42,589
  • 12
  • 85
  • 88
  • I really like that progress bar but I don't know how it will react with multiple concurrent ajax posts. Will it wait for the last one to finish or will it restart on every new concutrent ajax call? (I'll give it a try anyways) – Roberto Linares Sep 29 '14 at 16:49
  • Another question, since $q is an ES6 implementation, does it mean that is it only supported on ES6-ready browsers? can I run it on IE8 at least? do I need extra plugins (Ej. jQuery) to emulate the required ES6 functionality? – Roberto Linares Sep 29 '14 at 16:56
1

How about this : declare a service where you can register pending tasks, then declare them done.

myApp.factory('pendingService', [function () {
  var pendingTasksCount = 0;

  return {
    anyPending: function () {
      return pendingTasksCount > 0;
    },
    registerNewTask: function () {
      pendingTasksCount += 1;
      return function declareTaskDone() {
        pendingTasksCount -= 1;
      }
    }
  };
}]);

Then in your controller :

myApp.controller("adminCtrl", function($scope, $http, pendingService) {

  // expose "any pending task" property in scope
  $scope.showGif = pendingService.anyPending;

  $scope.initData1 = function() {
    var declareDone  = pendingService.registerNewTask();
    $http(...).success(function() {
      // do stuff
      // ...
      declareDone();
    });
  };

  $scope.initData2 = function() {
    var declareDone  = pendingService.registerNewTask();
    $http(...).success(function() {
      // do stuff
      // ...
      declareDone();
    });
  };

  $scope.initData3 = function() {
    var declareDone  = pendingService.registerNewTask();
    $http(...).success(function() {
      // do stuff
      // ...
      declareDone();
    });
  };

  initData1();
  initData2();
  initData3();
});

And in the HTML :

<img src="mygifurl" alt="loading" ng-show="showGif()"/>

If you need this behavior to be local, not global, you can just use the same factory to create a local object.

Valentin Waeselynck
  • 5,950
  • 26
  • 43
  • I like this answer as it is controller independent and therefore reusable in other controllers, which is my case. Thanks – Roberto Linares Sep 29 '14 at 16:46
  • Do I have to use $scope.$apply() on declareDone() or registerNewTask() to update my view? – Roberto Linares Sep 29 '14 at 17:00
  • Having been in this situation for my projects, I'd say that you usually won't have to, because the events that trigger `registerNewTask()` or `declareDone()` will already be part of an angular cycle (e.g : a button click, then an HTTP response). – Valentin Waeselynck Sep 29 '14 at 18:29
0

you can use $.active to check how many active ajax requests are in progress.

myApp.controller("adminCtrl", function($scope, $http) {
    $scope.initData1 = function() {
        if($.active==0){
            //this is the last active ajax request
        }
        /* is this the first call? then show the gif */
        $http(...).success(function() { /* is this the last response received? then hide the gif */});
    }

    $scope.initData2 = function() {
        if($.active==0){
            //this is the last active ajax request
        }
        /* is this the first call? then show the gif */
        $http(...).success(function() { /* is this the last response received? then hide the gif */});
    }

    $scope.initData3 = function() {
        if($.active==0){
            //this is the last active ajax request
        }
        /* is this the first call? then show the gif */
        $http(...).success(function() { /* is this the last response received? then hide the gif */});
    }

    initData1();
    initData2();
    initData3();
}
Community
  • 1
  • 1
Amin Jafari
  • 7,157
  • 2
  • 18
  • 43
  • If this works, the idiomatic angular way would rather be to expose the $.active property in the scope IMHO. – Valentin Waeselynck Sep 28 '14 at 05:48
  • I would prefer an angular-related solution rather a jquery-related solution, since I only use jQuery for some plugins and manage all the controller logic in angularjs. Also, how is jQuery aware of angularjs ajax posts? – Roberto Linares Sep 29 '14 at 16:43
0

I would use ngClass for this, and a counter variable, e.g.

<html ng-app="testApp">
    <head>
        <style class="text/css">
            .showing {
                /* instead of this, you can add your gif here */
                background: #f0f;
            }
        </style>
    </head>
    <body ng-controller="TestController">
        <div class="test" ng-class="{'showing': count > 0 && count < 3}">
            Count: {{count}}
        </div>
        <script type="text/javascript" src="angular.min.js"></script>
        <script type="text/javascript">
            (function() {
                var testApp = angular.module("testApp", []);
                testApp.controller("TestController", function($scope) {
                    $scope.count = 0;

                    var timeout = function(t) {
                        // this represents your ajax callbacks
                        setTimeout(function() {
                            $scope.$apply(function() {
                                $scope.count = $scope.count + 1;
                            });
                        }, t * 1000);
                    };

                    timeout(1);
                    timeout(2);
                    timeout(3);
                });
            })();
        </script>
    </body>
</html>

You need to modify the scope inside the $scope.$apply function context (in this particular example), or your view won't update (for more information on why this happens, see: http://www.jeffryhouser.com/index.cfm/2014/6/2/How-do-I-run-code-when-a-variable-changes-with-AngularJS). If you use promises, you may be able to avoid this problem, but I believe using ngClass is the angular way to modify the view.

emerino
  • 1,100
  • 11
  • 17
  • In this case, I assume I should keep a `count` $scope variable and increment it / decrement it on every ajax post / response. Is this correct? – Roberto Linares Sep 29 '14 at 16:40
  • Yes, you should have a 'count' variable in the scope that you increment everytime an ajax call returns (as you can see in the example). You can mix this approach with the one Chandermani suggested, once all promises complete you can have a boolean flag that ngClass can use to add or remove classes as needed. – emerino Sep 29 '14 at 16:46