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);
mmm
  • 2,272
  • 3
  • 25
  • 41
  • Counting the digest cycles is probably as bad as the timeout, IMHO... Post some code, so we can find a better solution. – orange Dec 13 '14 at 00:55
  • Why would that be a bad idea? performance? elegance? – mmm Dec 13 '14 at 01:11
  • AngularJS version? Using ng-view? – tasseKATT Dec 13 '14 at 12:58
  • Already said I'm using 1.3.6 and ng-repeat up there^ – mmm Dec 13 '14 at 16:44
  • 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`. – Andy Feb 20 '15 at 02:51
  • @Andy 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. – mmm Feb 20 '15 at 14:18
  • @emilySmitley ah, gotcha. Answer coming... – Andy Feb 22 '15 at 16:59
  • @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... – Andy Feb 22 '15 at 17:21
  • 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. – mmm Feb 23 '15 at 21:00

1 Answers1

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>
floribon
  • 19,175
  • 5
  • 54
  • 66
  • 1
    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...? – mmm Feb 24 '15 at 23:04
  • 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? – mmm Feb 24 '15 at 23:06
  • 1
    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. – floribon Feb 24 '15 at 23:19
  • In fact, his solution cannot work: I will comment on it to explain why. – floribon Feb 24 '15 at 23:23
  • @emilySmitley I have updated my answer with a snippet that shows the problem with the other solution that I am trying to point out. – floribon Feb 24 '15 at 23:29
  • I'll check it out in a minute, I'm using angular v1.3.10 by the way. – mmm Feb 24 '15 at 23:38
  • Accepted because your solution is less code, even though they're basically doing the same thing. – mmm Feb 25 '15 at 18:21
  • @emilySmitley thanks, now that I fixed the other answer and that he edited it using the method I suggest, indeed they are :) – floribon Feb 25 '15 at 19:01
  • 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. – Andy Feb 27 '15 at 03:33
  • @Andy 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. – floribon Feb 27 '15 at 03:59
  • @floribon 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... – Andy Feb 27 '15 at 16:39
  • Haha, you are absolutely right, I felt into the same trap! Thanks for correcting me – floribon Feb 27 '15 at 18:29
  • 1
    @floribon if the watcher is called multiple times per digest, isn't this an unreliable way to count the digest cycles? – Royi Namir Dec 20 '15 at 07:28
  • @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) – floribon May 17 '17 at 12:20