Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Jared Rypka-Hauer
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Jared Rypka-Hauer@ArmchairDeity )

Animating Static Child Nodes Using ngAnimate In AngularJS

By Ben Nadel on

The other day, I found myself with an interesting problem. I had a container that I was conditionally including using the ngIf directive in AngularJS. And, as the container was added and removed from the DOM (Document Object Model), I wanted nested elements within the container to animate; but, I didn't necessarily have a meaningful animation for the container itself (it was a non-visual element). Thankfully, after some experimentation, I was able to get child nodes to animate, using ngAnimate, even though AngularJS was only observing the container.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

The solution to this problem was to give the dynamic container element a transition duration. While I didn't have a meaningful animation for the container itself, simply giving it a duration was enough to get AngularJS to add the ng-animate CSS classes. Then, once I was able to get those in place, I was able to animate the child nodes by defining contextual rules that were based on the container's ng-animate classes.

So, for example, one of the CSS blocks for the nested elements looked like this:

div.container.ng-enter div.box { ... }

Here, you can see that the div.box styles will only be applied in the context of a container that is in the "ng-enter" phase of the animation life-cycle. Essentially, I'm animating descendant elements by hooking into the animation state of the ancestor element. With this approach, I am can animate "static" elements while AngularJS is adding or removing "dynamic" elements.

To see this in action, I have a demo with a dynamic container. The container is managed by the ngIf directive. As the container is added and removed, I'm animating child nodes by hooking into the animation states of the container:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Animating Child Nodes Using ngAnimate In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Animating Child Nodes Using ngAnimate In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="toggleContainer()">Show Container</a>
  • </p>
  •  
  • <!--
  • NOTE: The "container" element is the one that AngularJS is checking for animation
  • settings; however, it is NOT the element that we are "truly" animating. Rather,
  • we are conditionally animating the child / descendant / nested nodes based on the
  • state of the container.
  • -->
  • <div ng-if="isShowingContainer" class="container">
  •  
  • <div class="backdrop"></div>
  •  
  • <a ng-click="toggleContainer()" class="hide">Hide Container</a>
  •  
  • <div class="box box1">One</div>
  • <div class="box box2">Two</div>
  • <div class="box box3">Three</div>
  • <div class="box box4">Four</div>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.4.3.min.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.isShowingContainer = false;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // If the container is visible, I hide it. If it is hidden, I show it.
  • $scope.toggleContainer = function() {
  •  
  • $scope.isShowingContainer = ! $scope.isShowingContainer;
  •  
  • };
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

And, here's the CSS that I'm using. Take note of the "ng-enter" and "ng-leave" classes for div.container - they have a duration, but no meaningful animation. All of the animation is done by the nested elements.

  • a {
  • color: red ;
  • cursor: pointer ;
  • text-decoration: underline ;
  • user-select: none ;
  • -moz-user-select: none ;
  • -webkit-user-select: none ;
  • }
  •  
  • div.container {
  • bottom: 0px ;
  • left: 0px ;
  • position: fixed ;
  • right: 0px ;
  • top: 0px ;
  • z-index: 2 ;
  • }
  •  
  • div.backdrop {
  • background-color: rgba( 0, 0, 0, 0.8 ) ;
  • bottom: 0px ;
  • left: 0px ;
  • position: absolute ;
  • right: 0px ;
  • top: 0px ;
  • }
  •  
  • a.hide {
  • background-color: #FF0099 ;
  • border-radius: 4px 4px 4px 4px ;
  • color: #FFFFFF ;
  • height: 50px ;
  • left: 50% ;
  • line-height: 50px ;
  • margin: -25px 0px 0px -120px ;
  • position: absolute ;
  • text-align: center ;
  • top: 50% ;
  • width: 240px ;
  • }
  •  
  • div.box {
  • background-color: #F0F0F0 ;
  • border: 1px solid #CCCCCC ;
  • border-radius: 4px 4px 4px 4px ;
  • height: 100px ;
  • line-height: 100px ;
  • position: absolute ;
  • text-align: center ;
  • width: 100px ;
  • }
  •  
  • div.box1 {
  • left: 150px ;
  • top: 150px ;
  • }
  •  
  • div.box2 {
  • right: 150px ;
  • top: 150px ;
  • }
  •  
  • div.box3 {
  • bottom: 150px ;
  • right: 150px ;
  • }
  •  
  • div.box4 {
  • bottom: 150px ;
  • left: 150px ;
  • }
  •  
  •  
  •  
  • /* CSS Transition Information. */
  •  
  •  
  • /*
  • This is the container element managed by the NG-IF directive. As such, this is the
  • element that AngularJS is going to be checking to see if it has any animation
  • settings associated with it. Even though we are are not "truly" animating this
  • element, we want AngularJS to "transition" this element in and out in order to give
  • the descendant nodes time to transition in their own way.
  • */
  • div.container.ng-enter {
  • transition-duration: 500ms ;
  • }
  •  
  • div.container.ng-leave {
  • transition-duration: 250ms ;
  • }
  •  
  •  
  • /*
  • Now that the container is set to transition (in a non-functional way), we can
  • configure the descendant nodes to transition based on the NG-ENTER AND NG-LEAVE
  • classes that get associated with the transitioning container.
  •  
  • In order to make sure that none of the nested transitions don't exceed the container
  • transition in duration, we're just going to have the descendant nodes inherit the
  • duration that the container is using.
  •  
  • NOTE: We don't have to worry about the "NG-ANIMATE-CHILDREN" directive since
  • AngularJS isn't actually managing these children - it's only managing the container.
  • CSS is taking care of the rest.
  • */
  • div.container.ng-enter div.backdrop,
  • div.container.ng-leave div.backdrop,
  • div.container.ng-enter a.hide,
  • div.container.ng-leave a.hide,
  • div.container.ng-enter div.box1,
  • div.container.ng-leave div.box1,
  • div.container.ng-enter div.box2,
  • div.container.ng-leave div.box2,
  • div.container.ng-enter div.box3,
  • div.container.ng-leave div.box3,
  • div.container.ng-enter div.box4,
  • div.container.ng-leave div.box4 {
  • transition-duration: inherit ;
  • transition-property: bottom, left, right, top, opacity ;
  • transition-timing-function: ease ;
  • }
  •  
  •  
  • /* Backdrop. */
  •  
  • div.container.ng-enter div.backdrop {
  • opacity: 0.0 ;
  • }
  •  
  • div.container.ng-enter-active div.backdrop {
  • opacity: 1.0 ;
  • }
  •  
  • div.container.ng-leave-active div.backdrop {
  • opacity: 0.0 ;
  • }
  •  
  •  
  • /* Close link. */
  •  
  • div.container.ng-enter a.hide {
  • opacity: 0.0 ;
  • top: -5% ;
  • }
  •  
  • div.container.ng-enter-active a.hide {
  • opacity: 1.0 ;
  • top: 50% ;
  • }
  •  
  • div.container.ng-leave-active a.hide {
  • opacity: 0.0 ;
  • top: 105% ;
  • }
  •  
  •  
  • /* Box 1. */
  •  
  • div.container.ng-enter div.box1 {
  • top: -100px ;
  • left: -100px ;
  • }
  •  
  • div.container.ng-enter-active div.box1 {
  • top: 150px ;
  • left: 150px ;
  • }
  •  
  • div.container.ng-leave-active div.box1 {
  • top: -150px ;
  • left: -150px ;
  • }
  •  
  •  
  • /* Box 2. */
  •  
  • div.container.ng-enter div.box2 {
  • right: -100px ;
  • top: -100px ;
  • }
  •  
  • div.container.ng-enter-active div.box2 {
  • right: 150px ;
  • top: 150px ;
  • }
  •  
  • div.container.ng-leave-active div.box2 {
  • right: -150px ;
  • top: -150px ;
  • }
  •  
  •  
  • /* Box 3. */
  •  
  • div.container.ng-enter div.box3 {
  • bottom: -100px ;
  • right: -100px ;
  • }
  •  
  • div.container.ng-enter-active div.box3 {
  • bottom: 150px ;
  • right: 150px ;
  • }
  •  
  • div.container.ng-leave-active div.box3 {
  • bottom: -150px ;
  • right: -150px ;
  • }
  •  
  •  
  • /* Box 4. */
  •  
  • div.container.ng-enter div.box4 {
  • bottom: -100px ;
  • left: -100px ;
  • }
  •  
  • div.container.ng-enter-active div.box4 {
  • bottom: 150px ;
  • left: 150px ;
  • }
  •  
  • div.container.ng-leave-active div.box4 {
  • bottom: -150px ;
  • left: -150px ;
  • }

When I run the above page and toggle the container, we get the following output:


 
 
 

 
 Animating static child nodes using ngAnimate in AngularJS.  
 
 
 

As you can see, the transitional CSS is being applied to the nested elements because the nested elements are piggy-backing off of the ng-animate classes that have been added to the parent. The beauty of this is that it is still driven completely by the CSS, which is a huge part of what makes the ngAnimate module such an elegant solution.




Reader Comments

WARNING: I think there may be something about the ngAnimate code that only allows this approach to work if there is only a *single* container element animating. If you have multiple, concurrently animating containers, this seems to break down in an odd way (several elements skipping the animation).

I am currently debugging to see if I can figure out why this would change depending on the number of elements. I suspect it has to do with when the elements are added to the DOM - but not sure.

Reply to this Comment

@All,

OK, a quick follow-up to this post. There is an issue when you have more than one container - the child nodes have to take the injected "transition-delay" into account:

http://www.bennadel.com/blog/2909-child-animations-have-to-take-the-magical-transition-delay-into-account-in-angularjs.htm

I am not entirely sure why this works fine with just a single container; but, with more than one container, you need to "inherit" the transition-delay in the child nodes.

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.