4

If I execute a variable assignment in an angular model:

$scope.foo = 1;

How do I know when the view has been entirely updated based on the new value I just set on the model ?

(The reason I am asking this is that in practice, I am feeding a fairly large data structure to $scope.foo and it takes angular a couple of solid seconds to update the view on screen even on a beefy desktop machine so, I am looking for ways to know when I can safely remove a loading animation from the screen)

scniro
  • 16,844
  • 8
  • 62
  • 106
mathieu
  • 2,954
  • 2
  • 20
  • 31
  • Pass $scope.foo into a function that starts the loading animation, updates the view and then hides the loading animation? – Max Baldwin Mar 08 '15 at 16:44
  • The problem is that as far as I can tell, angular provides no way to know when "updates the view" is finished. Hence this question – mathieu Mar 08 '15 at 16:50
  • I would go with New Dev's answer. Makes the most sense. I wouldn't rely on a timeout. – Max Baldwin Mar 08 '15 at 16:51
  • It looks like a duplicate of this question: https://stackoverflow.com/questions/14968690/sending-event-when-angular-js-finished-loading/ – Rias Mar 08 '15 at 16:52
  • have you thought about using a $watch() ? – StarsSky Mar 08 '15 at 16:54
  • @MaxBaldwin, `$timeout` is not arbitrary and would not cause race conditions. It places the execution at the next digest cycle, which is exactly what is needed here – New Dev Mar 08 '15 at 17:24

3 Answers3

1

This answer assumes the delay occurs because the digest cycle is running. If the scope update is waiting on an asynchronous operation then the answer from @New-Dev is the more correct approach.

Probably the best way is to use the $timeout service. With it you can register a function to run after the current digest cycle completes. You will want to specify zero for the delay parameter and true (or default) for the invokeApply parameter, since I assume that your loading animation is shown/hidden using an ngShow directive or something.

$timeout(function() { $scope.showLoadingAnimation = false; }, 0);

Alternately, you can use the undocumented $$postDigest method on the scope. This method will run a function once, after the next digest cycle completes. Note that this function will not run in the context of a $scope.$apply, so if you want AngularJS to notice your scope change you will need to call $apply yourself.

$scope.$$postDigest(function() { 
  $scope.$apply(function() { $scope.showLoadingAnimation = false; });
});

(Insert standard disclaimers about using undocumented functions here.)

John Bledsoe
  • 17,142
  • 5
  • 42
  • 59
  • Are you saying that unless I trigger cascading updates from this one update, a single digest cycle is enough to propagate all changes to the view ? – mathieu Mar 08 '15 at 16:53
  • Well, your example was `$scope.foo = 1`, by which I assume you mean `$scope.foo = `. If that's correct, then yes, a single digest cycle will update the view with all the results of that change. If there are asynchronous processes going on that you haven't described, such as scope changes in promise then calls, then you'll need to wait until those are done as well. – John Bledsoe Mar 08 '15 at 17:04
1

The "real" answer to this question, and one you are not going to like, is:

"you can't"

When you make a change to a model in Angular, you have absolutely no idea how many $digest() calls are going to be required to fully materialize the DOM.

Let's say you have a very large array foos - it' has one million elements.

In your markup, you do the following:

<div ng-repeat="foo in foos"><div ng-if="foo.somecondition">I'm a visible foo!></div></div>

In you controller, you set foos:

.controller('myController', function($scope, $fooService) {
    $scope.foos = $fooService.get();
    $timeout(function(){
        //foo array done?  Not quite!!!!
    });
});

(get() is not asynchronous in this example, it returns the array of foos immediately).

After the controller constructor is run, $digest() will (eventually) run by Angular. This digest cycle will generate one million rows in the DOM (which will take a long time and will lock the browser while the DOM is being modified). At the end of the digest, if you used $timeout in the controller, your $timeout function would fire, BUT the DOM would not yet be fully built.

If examined the DOM at that very second, you'd find that you had your million rows, but the inner ng-if would not have been evaluated yet. Angular, at the end of the first digest cycle, would have noted that another digest cycle was needed and it would go right into running the next digest cycle - after your $timeout was evaluated.

Of course, this is a rather contrived example. It get's worse if you are loading in data asynchronously, etc.

I would recommend the real problem is that your foo data is too big - without knowing the details of your implementation, I can't offer much to help figure out how you would better manage the problem.

The key is that in Angular, you only worry about the state of the model, not the state of the DOM.

If I were going to try to display an array of a million foos, in order to keep the browser responsive, I would do it something like this:

.controller('myController', function($scope, $fooService) {
    $scope.loading = true;
    $scope.foos = [];

    var length = $fooService.length;  //1,000,000 foos
    var i = 0;

    var loadFoos = function() {
        var count = 0;
        while(count < 10) {
            $scope.foos.push($fooService.get(i + count++));
            if(i + count == length) {
                $scope.loading = false;  //we're done!
                return;
            }
        }
        i += count;
        //note, using setTimeout vs. $timeout deliberately - I want each digest cycle to completely finish and return the control of the execution context to the browser.
        setTimeout(function(){
            $scope.$apply(function() {
                loadFoos();
            }, 0);
        })
    };

    loadFoos();  //kick things off.

});

What I'm doing is loading foos 10 at a time and then returning control to the browser in the interim. You can now use the $scope.loading flag to indicate that foos are still loading, and all will work. I threw this together pretty quick as an example to get the point across - I'm sure there are ways to make it a little more elegant and it might even have a few syntax errors.

If I were doing this "for real" I'd probably wrap this functionality into the service itself.

Joe Enzminger
  • 11,110
  • 3
  • 50
  • 75
  • 1
    Joe, how certain are you that `$timeout` will run in between the two digests? [Here's an example](http://plnkr.co/edit/7Gi9syS2o7FEdVea05u9?p=preview) seemingly disproving this assertion - `ng-if` are showing only the even iterations, and a change of `flag` variable, to force a second digest cycle. Still, there is no lag between "rendering..." and actually showing the results. I agree on other suggestions of loading in batches – New Dev Mar 08 '15 at 19:02
  • My point is really that $timeout doesn't guarantee that the DOM has been fully materialized. It just guarantees that a $digest() has been run at least once (which may or may not have fully materialized the DOM, depending on your specific markup). Looking at the angular code, you may be right about my assumptions on when specifically $timeout is run, but my point still applies - it shouldn't be used as a way to verify that the DOM has been modified fully in response to a model change. – Joe Enzminger Mar 08 '15 at 19:27
  • The question was somewhat vague about "propagating to View". Sure, the browser may have not had a chance to render all the added DOM elements, and some DOM elements (say, `ng-include`) may have their own async actions. I think the lion share of the delay is due to the long digest cycle - which is addressed with `$timeout` in this case. – New Dev Mar 08 '15 at 20:19
0

If you get the data in an async manner, then you remove the animation in the handler:

$scope.isLoading = true;
somSvc.loadData().then(function(data){
  $scope.foo = data;
  $scope.isLoading = false;
});

As soon as you assign data to a scope variable, the change will take place during the next digest cycle.

This assignment will take some time, so if you need to change something in the View beforehand, you would need to delay assignment of data to $scope.foo so that Angular would have a chance to change the View.

You can't just do this right before the assignment of data, because in the same digest cycle that you change assign the data, Angular would be busy rendering the DOM:

$scope.isLoading = true;
somSvc.loadData().then(function(data){
  $scope.isLoading = false;
  $scope.isRendering = true;

  $timeout(function(){
    $scope.foo = data;
    $scope.isRendering = false;
  }, 0);
});

http://plnkr.co/edit/KslF9lED0tkT7bKJ81LF?p=preview

EDIT: If you only have a single "loading" indicator, then the example above can be simplified to:

$scope.isLoading = true;
somSvc.loadData().then(function(data){
  $scope.foo = data;
  $timeout(function(){
    $scope.isLoading = false;
  }, 0);
});
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • Yes, I do this already. The problem is that, as I said in my question, after I have assigned "$scope.foo = data" , it takes a couple of seconds until the view is updated on screen so, I want to defer setting isLoading = false until much later. – mathieu Mar 08 '15 at 16:51