Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Chris Laning
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Chris Laning@claning )

Child Animations Have To Take The "Magical" Transition-Delay Into Account In AngularJS

By Ben Nadel on

The other day, I blogged about animating "static" child nodes, using ngAnimate, inside of a dynamic container in AngularJS. This worked quite nicely for a single container; however, as I continued to experiment, I noticed that it started to break when I had multiple containers animating at the same time. After a few [way too many] hours of debugging, I discovered that the ngAnimate module injects a "transition-delay" property during the setup phase. And, in order for child nodes to piggy-back on the parent transition, they too have to take this "magical" transition-delay into account.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

For some reason, when I have a single container being animated, the contextual nested child node animations "just work." The reasons for that are beyond my understanding of the AngularJS code. But, the moment that I add more than one simultaneously animating container, the child animations fail to execute during the "enter" phase.

After adding dozens of console.log() statements and "debugger" statements, a gym break, and a lunch break, I realized that the ngAnimate module was injecting an inline style for "transition-delay" into each of the container elements:


 
 
 

 
 Contextual animations, in AngularJS, have to take the magical  
 
 
 

From what I can understand in the code, this negative delay causes the "setup" transitions to finish immediately, thereby putting the element in the "enter" (or "leave") state. When the animation start is subsequently triggered, the transition-delay can be removed and the "active" class can be added, at which point the element will gracefully transition into the active configuration.

This inline style attribute works well for the container, but not for any contextual animations (that are powered by contextual class existence). As such, contextual animations barely make it out of the "enter" state before the animation is kicked over into the "active" state. This creates a visual effect in which the animations appears to fail altogether.

To fix this, we have to let the child nodes inherit the injected "transition-delay" style. This way, the child nodes will properly consume the setup styles before the animation is triggered.

To see this in action, I've created a demo with three Divs that are all controlled by ngIf directives watching the same value. As each Div is animated onto the page, a nested Span tag will also be animated. Here is the markup:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Child Animations Have To Take The "Magical" Transition-Delay Into Account In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Child Animations Have To Take The "Magical" Transition-Delay Into Account In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="toggleViews()">Toggle Views</a>
  • </p>
  •  
  • <!--
  • NOTE: The following three DIV elements all use the same exact styles. The only
  • difference between "a", "b", and "c" is the LEFT style that is being used. As
  • the DIV performs its ng-enter and ng-leave animations, we are going to animate
  • the SPAN element as well, based on the contextual animation class.
  • -->
  •  
  • <div ng-if="isShowingView" class="view view-a">
  •  
  • <span>View A</span>
  •  
  • </div>
  •  
  • <div ng-if="isShowingView" class="view view-b">
  •  
  • <span>View B</span>
  •  
  • </div>
  •  
  • <div ng-if="isShowingView" class="view view-c">
  •  
  • <span>View C</span>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.4.5.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • angular.module( "Demo", [ "ngAnimate" ] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function AppController( $scope ) {
  •  
  • $scope.isShowingView = false;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I toggle the existence of the animated views.
  • $scope.toggleViews = function() {
  •  
  • $scope.isShowingView = ! $scope.isShowingView;
  •  
  • };
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

Each Div hooks into the ngAnimate classes for "enter" and "leave". But, each of the Span tags will also piggy-pack on those classes by creating contextual CSS rules that depend upon the ngAnimate classes on the ancestor nodes. In the following CSS, take note of the comment that explains the "transition-delay."

  • a[ ng-click ] {
  • color: red ;
  • cursor: pointer ;
  • text-decoration: underline ;
  • user-select: none ;
  • -moz-user-select: none ;
  • -webkit-user-select: none ;
  • }
  •  
  • div.view {
  • border: 2px solid #FF0099 ;
  • border-radius: 4px 4px 4px 4px ;
  • height: 200px ;
  • left: 40px ;
  • overflow: hidden ;
  • position: fixed ;
  • top: 130px ;
  • width: 288px ;
  • }
  •  
  • div.view.view-a {
  • left: 40px ;
  • }
  •  
  • div.view.view-b {
  • left: 370px ;
  • }
  •  
  • div.view.view-c {
  • left: 700px ;
  • }
  •  
  • div.view span {
  • border: 1px dotted #AAAAAA ;
  • border-radius: 4px 4px 4px 4px ;
  • display: block ;
  • margin: 20px 20px 0px 20px ;
  • padding: 10px 10px 10px 10px ;
  • text-align: center ;
  • }
  •  
  •  
  • /* Container [ng-if] animation configuration. */
  •  
  • div.view.ng-enter,
  • div.view.ng-leave {
  • transition: opacity 2s ease ;
  • }
  •  
  • div.view.ng-enter {
  • opacity: 0.0 ;
  • }
  •  
  • div.view.ng-enter-active {
  • opacity: 1.0 ;
  • }
  •  
  • div.view.ng-leave {
  • opacity: 1.0 ;
  • }
  •  
  • div.view.ng-leave-active {
  • opacity: 0.0 ;
  • }
  •  
  •  
  • /* Nested element animation configuration. */
  •  
  • div.view.ng-enter span,
  • div.view.ng-leave span {
  • transition-duration: inherit ;
  • transition-property: margin-top ;
  • transition-timing-function: inherit ;
  •  
  • /*
  • When AngularJS goes to apply the "enter" and "leave" animation setup classes, it
  • injects an inline, negative animation-delay into the container. While I am not
  • 100% sure what this is doing (this is over my head in terms of CSS kung-fu), it
  • appears to complete the transition immediately, at which point, the element is
  • properly poised to enter the "active" state.
  •  
  • Now, while this works for the container, the nested / child element animations
  • don't know to use this negative delay. As such, they never make it to the "start"
  • condition and therefore, don't appear to animate. As such, we have to INHERIT
  • any delay that is being used by the container.
  • */
  • transition-delay: inherit ;
  • }
  •  
  • div.view.ng-enter span {
  • margin-top: 200px ;
  • }
  •  
  • div.view.ng-enter-active span {
  • margin-top: 20px ;
  • }
  •  
  • div.view.ng-leave span {
  • margin-top: 20px ;
  • }
  •  
  • div.view.ng-leave-active span {
  • margin-top: 200px ;
  • }

As you can see, each of the Span tags inherits the "transition-delay" property. This way, when ngAnimate injects the inline transition-delay value, the contextual child node animations will also see it. This keeps the container and the child nodes in alignment.

The ngAnimate module is bad-ass. But, as with everything ever, there are caveats and edge-cases. Part of the way in which the ngAnimate module works is by the injecting a "magical" transition-delay property. In order to get contextual animations to work, we just need to be aware of this and adjust our CSS accordingly.




Reader Comments

@All,

It looks like "transition-delay" was part of the ngAnimate 1.4 rewrite. In earlier versions - AngularJS 1.2 and 1.3 - child elements have to inherit the "transition-property" rule:

http://www.bennadel.com/blog/2932-child-animations-have-to-inherit-transition-property-in-angularjs-1-2-and-1-3.htm

In AngularJS 1.2, the configuration-phase animations are blocked using "transition-property: none", which means the children have to be able inherit that.

Reply to this Comment

Great work Ben!! I love your blog and this post has been particularly helpful. Sadly this trick doesn't work if you use a "transition-duration" value below a second ( "200ms" or ".2s). The ng-leave transition get "stuck" for a while and then fires normally.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.