2

I have a controller where I need to load content using ajax. While it's loading, I'd like a spinner to appear in the interim. The code looks something like the below:

<i class="fa fa-2x fa-spin fa-spinner" ng-show="isLoadingContent"></i>

And the corresponding js:

$scope.isLoadingContent = true;
$q.all(promises).then(function (values) {
    $scope.isLoadingContent = false;
    // more code - display returned data

However, the UI the spinner does not appear where/when I expect it to appear when I step through the code.

$scope.isLoadingContent = true;
debugger; // the spinner does not appear on the UI
$q.all(promises).then(function (values) {
    debugger; // the spinner finally does appear in the UI at this point
    $scope.isLoadingContent = false;
    // more code - display returned data

I have tried stepping through the code but came up short as to what's going on -- and I am sure I am misunderstanding the sequence of events happening in the Event Loop and where the angular-cycle plays it's role in all of this.

Is someone able to provide an explanation as to why the spinner is set to appear within the promise's method rather than where I set $scope.isLoadingContent? Is it not actually getting set but rather getting queue'd up in the event-loop's message-queue?

------------ EDIT ------------

I believe I came across an explanation as to what's going on. Thanks in large part to, @jcford and @istrupin.

So a little tidbit missing in the original post, the event firing the promise calls and the spinner update was actually based around a $scope.$on("some-name", function(){...}) event - effectively a click-event that is triggered outside of my current controller's scope. I believe this means the $digest cycle doesn't work as it typically does because of where the event-origination is fired off. So any update in the $on function doesn't call $apply/$digest like it normally does, meaning I have to specifically make that $digest call.

Oddly enough, I realize now that within the $q.all(), it must call $apply since, when debugging, I saw the DOM changes that I had expected. Fwiw.

tl;dr - call $digest.

mche
  • 616
  • 10
  • 16
  • please provide a [mcve] – Daniel A. White Sep 07 '17 at 18:56
  • can you once try using ng-if instead of ng-show. – Ved Sep 07 '17 at 18:56
  • Can you show how the promises are created? – Frank Modica Sep 07 '17 at 18:59
  • 2
    What is triggering the loading of data? A button? Most likely what is going on here is your code is being run outside the angular digest cycle. For instance if you are using a jQuery event handler instead of an Angular event directive. Or if you are using window.setTimeout instead of the Angular $timeout service. – JC Ford Sep 07 '17 at 19:22
  • @JCFord - Yes, it is a button click, specifically it's a `$scope.$on("some-name", function(){...})` event that, while the function is implemented inside the current scope I am working on, I guess since the button click is actually outside of the scope is why the digest cycle is not running in the scope of my original post? Kind of confusingly written but hopefully I am explaining myself. – mche Sep 07 '17 at 20:07

3 Answers3

3

A combination of both answers will do the trick here. Use

$scope.$evalAsync()

This will combine scope apply with timeout in a nice way. The code within the $evalAsync will either be included in the current digest OR wait until the current digest is over and start a new digest with your changes.

i.e.

$q.all(promises).then(function (values) {
    $scope.$evalAsync($scope.isLoadingContent = false);
});
Mattkwish
  • 753
  • 7
  • 16
  • 1
    There should be no need to do `$evalAsync` inside `$q.then`. When I was talking about $timeout I only ment that for debugging purpose. – Marcin Malinowski Sep 07 '17 at 19:45
  • Sure there is, if you need to immediately update the view within your `then` function. – Mattkwish Sep 07 '17 at 19:49
  • are you saying that without $evalAsync there will be no digest cycle after execution of then callback? – Marcin Malinowski Sep 07 '17 at 19:50
  • just as you have explained evalAsync will not update the dom immediatelly. After exeuting `then` callback the digest is going to be triggered anyway and $evalAsync will only join that digest process so it is redundant. – Marcin Malinowski Sep 07 '17 at 19:51
  • No, but i can definitely see a scenario where inside a then() function you need to update the view, then execute more logic, then update the view again all within the then clause. The digest will run eventually, but sometimes you may need to update the user immediately, in which case $evalAsync is the safest way to do it. If you execute $evalAsync within the then() it can trigger its own scope apply (if its safe) before the then clause finishes. It doesnt always wait for the next available digest, which it seems you are assuming it does. – Mattkwish Sep 07 '17 at 19:52
  • asyncEval will not start updating immediatelly. Refer to docs (or just the name of it) https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$evalAsync $evalAsync is only scheduling a digest cycle. It will NOT interrupt the callback execution to immediatelly update HTML. – Marcin Malinowski Sep 07 '17 at 19:59
  • What is need of `$evalAsync` here? `$q` will take care of digest cycle.. – Pankaj Parkar Sep 07 '17 at 20:04
  • from angularjs documentation `$evalAsync([expression], [locals]); Executes the expression on the current scope at a later point in time. The $evalAsync makes no guarantees as to when the expression will be executed` – Marcin Malinowski Sep 07 '17 at 20:29
  • @mche If this answer worked for you, could you select it as the answer? The point of `$scope.$evalAsync` is to only digest a single value in the scope (the one passed to it). – Mattkwish Jan 06 '19 at 18:36
2

Try adding $scope.$apply() after assigning $scope.isLoadingContent = true to force the digest. There might be something in the rest of your code keeping it from applying immediately.

As pointed out in a number of comments, this is absolutely a hack and is not the best way to go about solving the issue. That said, if this does work, you at least know that your binding is set up correctly, which will allow you to debug further. Since you mentioned it did, the next step would then be to see what's screwing up the normal digest cycle -- for example triggering outside of angular, as suggested by user JC Ford.

istrupin
  • 1,423
  • 16
  • 32
  • 1
    Why to force $digest cycle here. Everything in Angular way. – Ved Sep 07 '17 at 18:55
  • `throw $scope.$apply()` is definitly not going to help lol ;) – Marcin Malinowski Sep 07 '17 at 19:03
  • It's not the cleanest way to fix it, but it'll make it work. There's definitely something going on that's holding up the normal digest though. Fixing that would be the "right" way. – istrupin Sep 07 '17 at 19:07
  • After doing a little research, I'm just going to be using `$digest()` since I only need one particular scope to update the DOM. – mche Sep 07 '17 at 20:03
-2

I usually use isContentLoaded (as oposite to isLoading). I leave it undefined at first so ng-show="!isContentLoaded" is guaranteed to show up at first template iteration.

When all is loaded i set isContentLoaded to true.

To debug your template you need to use $timeout $timeout(function () { debugger; }) That will stop the code execution right after first digest cycle with all the $scope variable values reflected in the DOM.