Animating Static Child Nodes Using ngAnimate In AngularJS
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:
| | | ||
| ![]() | | ||
| | |
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.
@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:
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.