$animate:before, $animate:after, $animate:close And The ngAnimate Enter Workflow In AngularJS
After my blog post yesterday, on animating an element in from a mouse-click location in AngularJS, I was a little irked that I couldn't get consistent results from the "$animate:before" and "$animate:after" events triggered by the $animate service. I couldn't figure out how they fit into the overall animation workflow. As such, I spent a couple of hours digging through (and console-logging the heck-fire out of) the ngAnimate module. At this point, my understanding still feels shaky; but, I've tried to outline the general control flow a the ngRepeat's "enter" animation.
The following list of lists of lists doesn't represent all of the logic branching in animation. And, it doesn't really take into account the difference between structural animation and class-based animation. But, as you look through this workflow pay special attention to where classes are added, where the events are triggered, and where the forced-repaint happens (via the $$animateReflow() service).
NOTE: To take asynchronous code and make it sequential, for the sake of readability, I've tried to create separate "queuing" and "execution" line-items for asynchronous actions.
- ng-repeat
- link() / $watchCollection()
- $animate.enter()
- $delegate.enter() - Calls applyStyles() in base service
- runAnimationPostDigest() - Will run at the end of the digest in $$postDigest
- $animate.enter()
- link() / $watchCollection()
-
- Directive link() function
- bind on( "$animate:before" )
- bind on( "$animate:after" )
- bind on( "$animate:close" )
-
- $rootScope.$$postDigest()
- performAnimation()
- Add "ng-animate" class and other "temp" classes
- Queue "$animate:before" event via $$asyncCallback()*
- before() animations
- DOM operation - Seems to only be used by "leave" events
- Queue "$animage:after" event via $$asyncCallback()*
- after() animations
- animateBefore() - This will return UNDEFINED if animateSetup() returns FALSE (which will cancel the animation)
- animateSetup() - This will return FALSE if no duration is detected on the computed styles
- Add "ng-enter" class
- getElementAnimationDetails()
- $window.getComputedStyle(element) - At this point, your animation setup class has to be applied otherwise the computed styles won't have duration and will short-circuit the animation
- animateSetup() - This will return FALSE if no duration is detected on the computed styles
- Queue afterReflow()** via $$animateReflow()
- animateBefore() - This will return UNDEFINED if animateSetup() returns FALSE (which will cancel the animation)
- performAnimation()
-
- $$asyncCallback()
- triggerHandler( "$animate:before" )
- triggerHandler( "$animate:after" )
-
- $$animateReflow() !!!! CAUSES REPAINT !!!!
- animateAfter()
- animateRun()
- Add "ng-enter-active" class
- getElementAnimationDetails() - If the duration has been removed from the element, the animation will end
- Setup timer(s) for animation duration
- animateRun()
- animateAfter()
-
- Animation | Transition | $timeout fallback
- onEnd()
- Remove "ng-enter-active" class
- animateClose()
- Remove "ng-enter" class
- activeAnimationComplete() / closeAnimation()
- Apply option styles [options:to/from]
- Remove temp classes
- Queue cleanup() via $$asyncCallback()* - NOTE: Class-based animations are not queued
- Queue "$animate:close" via $$asyncCallback()*
- doneCallback()
- Resolve animation promise
- onEnd()
-
- $$asyncCallback()
- cleanup()
- Remove "ng-animate" class
- triggerHandler( "$animate:close" )
- cleanup()
* $$asyncCallback() uses requestAnimationFrame() if it's available; if not, it will fallback to using the $timeout() service.
** afterReflow() - Uses requestAnimationFrame() if it's available; if not, it will fallback to using the $timeout() service.
As you can see, animating an element is hella complicated. Especially when it's constructed in such a way that you (as the developer) have to know almost nothing about it. But, when you look at this list notice that the "$animate:before" and "$animate:after" events are both triggered after the "ng-enter" class is added but, before the forced repaint is executed. This means that if either your "$animate:before" or your "$animate:after" event handlers force a repaint, there is a chance that your element will "flicker" on the screen with the "ng-enter" styles before the "ng-enter-active" class is applied.
NOTE: I was only able to get this flickering in Firefox. Chrome seems to render too quickly to show the element in the "ng-enter" only state.
In my demo yesterday, there was no "ng-enter" only flicker because I was applying the custom CSS in the directive linking function. If you look at the list above, my action would force a repaint; but, that repaint would occur before the "ng-enter" class is added to the element.
Possible Bug In AngularJS ngAnimate 1.3.15.min.js
While I was digging into all of this, I believe I found a bug in the 1.3.15 build of the ngAnimate module. It seems, as best I can tell, the minified version of the $$animateReflow() service "optimizes away" the forced-repaint. To see what I'm talking about, take a look at this video:
Notice that the minified version of the code lacks the line:
var a = bod.offsetWidth + 1;
As such, the minified $$animateReflow() won't force the repaint before the "ng-enter-active" class is applied.
Anyway, I hope some of this was helpful. For me, the big take-away is simply that the "$animate:before" and "$animate:after" events aren't particularly helpful. If I need to setup custom animations, I'll continue to do so in the linking functions, before the "enter" class is added.
Reader Comments
Nice find, Ben. I just started working with ngAnimate and didn't know about the $animate:before and $animate:after events you're showing here. On top of that, that's a pretty nasty bug you uncovered!
@Fesh,
Thanks my man! The AngularJS team says that bug is fixed in a recent patch of the 1.3.15 library that has not yet been released. And, apparently it will also be fixed in 1.4 as well (but that is still in "beta").
Glad I could help shed some light on the inner workings!
You focus on the "enter"-specific functionality. I was working on something related to animations that would rely on animation events (in 1.3.15) and noticed that "after" and "close" events don't ever seem to fire for "leave" animations. This is pretty confusing, as it looks like the functions that fire the events are called in the case of all animations, not just enter.
Wondering if anyone else has seen this.
@RLM,
Sure enough, it's a known issue.
https://github.com/angular/angular.js/issues/12096
https://github.com/angular/angular.js/issues/6049