Counting Angular's $digest Cycles
Asked Answered
B

1

16

tl;dr:

I want to have angular trigger css animations on page load. Is there a way to count angular's digest cycles within say, a controller or directive?


long version:

I have some angular animations which I want to run when the page loads, using ng-enter, ng-leave, ng-move and so on... with an ng-repeat directive.

As of 1.3.6, I know that angular waits to apply any animations until after 2 digest cycles occur, so these animations aren't happening at all because the data is (almost always)loaded into the view on the first digest cycle of my application. (sauce: https://docs.angularjs.org/api/ngAnimate#css-staggering-animations)

I'm wondering if there's some way that I can count digest cycles and either trigger the animations, or load the data in after the 2nd digest cycle?

Also, if I wait until 2 digest cycles, is there a risk that the second cycle wont occur in some instances meaning that my data wouldn't load into the view? If this is the case, is there a way that I can guarantee that at least 2 digest cycles will occur every time?

As a temporary fix, I'm using $timeout to load my data in after 500ms, but I know this is a really bad idea.


relevant code:

(changed some of the names of certain things because of an NDA on this project)

html:

<div ng-repeat="pizza in pizzas" class="za" ng-click="bake(pizza)"></div>

css/sass (browser prefixes removed for brevity):

.za {
  //other styles

  &.ng-enter,
  &.ng-leave,
  &.ng-move {
    transition: all 1s $slowOut;
    transform: translate(1000px, 0px) rotateZ(90deg);
  }
  &.ng-enter,
  &.ng-leave.ng-leave-active
  &.ng-move, {
    transform: translate(1000px, 0px) rotateZ(90deg);
  }
  &.ng-enter.ng-enter-active,
  &.ng-leave,
  &.ng-move.ng-move-active {
    transform: translate(0, 0) rotateZ(0deg);
  }
  &.ng-enter-stagger,
  &.ng-leave-stagger,
  &.ng-move-stagger {
    transition-delay: 2s;
    transition-duration: 0s;
  }
}

js:

// inside a controller
timeout(function() {
  scope.pizza = [ // actually injecting 'myData' and using `myData.get()` which returns an array of objects
    {toppings: ['cheese', 'formaldehyde']},
    {toppings: ['mayo', 'mustard', 'garlic']},
    {toppings: ['red sauce', 'blue sauce']}
  ];
}, 500);
Brecciate answered 13/12, 2014 at 0:31 Comment(9)
Counting the digest cycles is probably as bad as the timeout, IMHO... Post some code, so we can find a better solution.Pergolesi
Why would that be a bad idea? performance? elegance?Brecciate
AngularJS version? Using ng-view?Mulciber
Already said I'm using 1.3.6 and ng-repeat up there^Brecciate
what is this timeout function, is it angular's $timeout service or some wrapper for setTimeout? (I don't see a $ before it in your code) If you are using setTimeout directly it will not cause a $digest like $timeout.Ootid
@Ootid I like to use this pattern with angular's DI -> ['$timeout', function(timeout) {}]. I don't like having to type a $ every time I want to use an angular service in a directive or controller.Brecciate
@emilySmitley ah, gotcha. Answer coming...Ootid
@emilySmitley is the data hard-coded into your controller? Otherwise how is it loaded on the first digest cycle? I'm sure the Angular designers assumed you would be loading all data from the backend, so maybe this is a flaw in the design...Ootid
It's loaded in with angular's '$http.get()'. But just so you know, I no longer need to count angular's digest cycles because I removed a lot of the animations from my project.Brecciate
E
13

As pointed out in the documentation:

If you want to be notified whenever $digest is called, you can register a watchExpression function with no listener. (Since watchExpression can execute multiple times per $digest cycle when a change is detected, be prepared for multiple calls to your listener.)

So you can count the $digest with the following code:

var nbDigest = 0;

$rootScope.$watch(function() {
  nbDigest++;
});


Update: illustration as to why you cannot rely on HTML, if you look at your dev console you will see Angular complaining about not being able to end a digect cycle (abortion after 10 cycles)

angular.module("test", []).controller("test", function($scope) {
  
  $scope.digestCount = 0;

  $scope.incrementDigestCount = function() {
      return ++$scope.digestCount;
  }
  
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="test" ng-controller="test">
  <div>{{incrementDigestCount()}}</div>
</body>
Employee answered 24/2, 2015 at 20:11 Comment(14)
But if the watcher is called multiple times per digest, isn't this an unreliable way to count the digest cycles? Couldn't it wind up being something like nbDigest = 0 > 3 > 5 > 8 > etc...?Brecciate
Also, wouldn't @Andy's solution be more efficient because it just sets up two variables, one integer and one function on the scope vs one integer and one watcher?Brecciate
When using curly brackets {{ expression }}, a watcher for the expression is implicitely created. On the core, our solutions do exactly the same, but his implies html which is not needed, hence mine is actually more performant as I save angular a DOM refresh. As for your initial question, this watcher on rootScope will be executed once per digest cycle. This is the official and recommended way to benchmark the number of digest cycle occurring, see the doc.Employee
In fact, his solution cannot work: I will comment on it to explain why.Employee
@emilySmitley I have updated my answer with a snippet that shows the problem with the other solution that I am trying to point out.Employee
I'll check it out in a minute, I'm using angular v1.3.10 by the way.Brecciate
Accepted because your solution is less code, even though they're basically doing the same thing.Brecciate
@emilySmitley thanks, now that I fixed the other answer and that he edited it using the method I suggest, indeed they are :)Employee
I was just thinking, if you do want to see the result on the webpage for whatever, reason, you could use jQuery or pure DOM manipulation to put the digest count on the page, and that wouldn't trigger another digest cycle.Ootid
@Ootid you can still use Angular the same, you increment $scope.count in the watcher and display {{count}} in the HTML. As long as you don't also return the count from the watcher, it won't trigger another one.Employee
@Employee this doesn't make sense to me, {{count}} creates a $scope.$watch('count'), so in a digest cycle, the first watcher will increment the count, then angular will see that this second watch has changed, so it will re-execute all watchers, hence the first increments the count again, etc...Ootid
Haha, you are absolutely right, I felt into the same trap! Thanks for correcting meEmployee
@Employee if the watcher is called multiple times per digest, isn't this an unreliable way to count the digest cycles?Tramline
@Royi Wow, sorry for answering almost 2 years late, I probably missed the notification :p Each watcher will be called only once per digest cycle. Now during a scope.$apply angular will keep running digest cycles as long as a watcher returns a value different from its previous run. This is valuable information for benchmarking (gives you an idea of the stability and inter-dependence of your watchers), so I wouldn't try to work around it (and honestly I wouldn't know how to, unless maybe you override the $apply method or edit the source code)Employee

© 2022 - 2024 — McMap. All rights reserved.