Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the New York ColdFusion User Group (Jun. 2010) with: Clark Valberg and Andy Matthews
Ben Nadel at the New York ColdFusion User Group (Jun. 2010) with: Clark Valberg@clarkvalberg ) and Andy Matthews@commadelimited )

Staggering ngRepeat Animations In AngularJS

By Ben Nadel on

Lately, I've been trying to learn more about the new animation features of the ngAnimate module available as of AngularJS 1.2. In my reading, I saw mention of the ability to "stagger" animations. As someone who has a love-hate relationship with user interface (UI) transitions, this struck me as something I wouldn't fall in love with; but, I wanted to take a look at it before I cast any (too much) judgement.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Staggering only makes sense (for ngRepeat) when you are animating multiple ngRepeat items at the same time. By default, all items will animate into place concurrently. However, if you stagger the animation, it will add a delay before each successive animation.

To stagger animations, you need to provide a "-stagger" CSS event class that defines the duration of the delay. Since I'm animating the "enter" event, I'm providing the "ng-enter-stagger" class. I don't know enough about CSS transitions vs. animation keyframes; but, apparently both approaches work. In this demo, I'm using transitions because it's what I'm used to; and, it's what AngularJS has in its documentation.

NOTE: I assume that you can stagger "leave" animations, although I haven't tested it personally.

One important thing to understand about staggering animations is that it doesn't delay the actual cloning and linking of the ngRepeat items. This is critical to understand because it means that your "staggered" DOM elements are sitting there on the page, waiting to be animated. As such, if your animation setup class (ex, "ng-enter") doesn't fully hide the pending elements, they will be visible to the end user.


 
 
 

 
 Stagging ngRepeat animations in AngularJS put the cloned elements in a pending state, but don't delay the linking phase. 
 
 
 

To experiment with this, I created a demo in which you can add friends to a list. Remember, ngAnimate blocks animations during the initial page loading and bootstrapping. As such, the animations (including the staggering) will only show when subsequent friends are added to the already-rendered list.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Staggering ngRepeat Animations In AngularJS
  • </title>
  •  
  • <style type="text/css">
  •  
  • a[ ng-click ] {
  • color: red ;
  • cursor: pointer ;
  • text-decoration: underline ;
  • user-select: none ;
  • -moz-user-select: none ;
  • -webkit-user-select: none ;
  • }
  •  
  • li.friend.ng-enter {
  • opacity: 0.2 ;
  • padding-left: 30px ;
  • transition: all ease 250ms ;
  • }
  •  
  • li.friend.ng-enter-stagger {
  • transition-delay: 0.1s ;
  • -webkit-transition-delay: 0.1s ;
  •  
  • /*
  • FROM THE DOCUMENTATION:
  • In case the stagger doesn't work then these two values must be set
  • to 0 to avoid an accidental CSS inheritance.
  • */
  • transition-duration: 0s ;
  • -webkit-transition-duration: 0s ;
  • }
  •  
  • li.friend.ng-enter-active {
  • opacity: 1.0 ;
  • padding-left: 0px ;
  • }
  •  
  • </style>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Staggering ngRepeat Animations In AngularJS
  • </h1>
  •  
  • <h2>
  • Friends
  • </h2>
  •  
  • <p>
  • <a ng-click="addFriends()">Add Friends</a>
  • &ndash;
  • <a ng-click="resetFriends()">Reset Friends</a>
  • </p>
  •  
  • <ul>
  • <!--
  • We are going to be animating the LI elements on to the page with a slight
  • delay to create a staggering effect. This will only affect the "subsequent"
  • additions since the initial page render will block the initial ngRepeat
  • animation during linking.
  • --
  • NOTE: Normally, I would avoid any filters (orderBy) on the ngRepeat. But, I
  • am using it here simply to make the demo a bit more interesting, visually.
  • -->
  • <li
  • ng-repeat="friend in friends | orderBy:'name' track by friend.id"
  • class="friend">
  •  
  • {{ friend.name }}
  •  
  • </li>
  • </ul>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.3.8.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [ "ngAnimate" ] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I hold the collection of friends to render.
  • $scope.friends = loadFriends();
  •  
  • // Some possible names for the demo.
  • var names = [
  • "Olivia", "Ava", "Isabella", "Mia", "Lily", "Madelyn", "Madison",
  • "Chloe", "Charlotte", "Aubrey", "Leah", "Abigail", "Kaylee",
  • "Layla", "Harper", "Ella", "Amelia", "Arianna", "Riley", "Aria",
  • "Hailey", "Hannah", "Maria", "Evelyn", "Addison", "Mackenzie"
  • ];
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I add three more friends to the rendered list.
  • $scope.addFriends = function() {
  •  
  • var i = 3;
  •  
  • while ( i-- && names.length ) {
  •  
  • $scope.friends.push({
  • id: ( $scope.friends.length + 1 ),
  • name: names.shift()
  • });
  •  
  • }
  •  
  • };
  •  
  •  
  • // I reset the list of friends to the original set.
  • $scope.resetFriends = function() {
  •  
  • $scope.friends = loadFriends();
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I build (and return) the original list of friends.
  • function loadFriends() {
  •  
  • return([
  • {
  • id: 1,
  • name: "Kim"
  • },
  • {
  • id: 2,
  • name: "Heather"
  • },
  • {
  • id: 3,
  • name: "Tara"
  • },
  • {
  • id: 4,
  • name: "Anna"
  • }
  • ]);
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

The great thing about AngularJS animations is that they are all CSS-driven. The staggering of animations is no different. My Controller code knowns nothing about any of this - it's all defined in the various CSS classes for setup, staggering, and activation.

NOTE: There are non-CSS-based options for animations as well.

I love animations and user interface (UI) transitions when the serve a purpose and feel natural. My biggest concern with staggering animations is that they might make the user "overly aware" of the animations by adding additional time-to-complete. As such, I would suggest that you apply "staggering" with caution. I am sure there are good use-cases for it; but, I would defer it to a final "polishing" step in development. But, that's just my opinion.




Reader Comments

Hi, i tried using stagger animations on ng-repeats on load of the page, but there seems a problem as when ever i filter the list or sort the list more elements add onto the ng-repeat.
I used $timeout to have the delay on load of the page, but now, i am not sure how to proceed.

Thanks

Reply to this Comment

one silly question: how did you pause the dom animantion in firebugs to actually view the class name ,like " ng-aninamte" ?

Reply to this Comment

@Bowen,

Great question. When I have to capture something like that, I usually one of two things (or both):

1. I'll use a really large animation duration - like 50-seconds. That way, I have plenty of time to inspect the element while the animation is still running. In reality, the code I commit has really small animations - but I'll tweak the timing specifically for screen shots.

2. If tweaking the timing doesn't get the job done, I'll inspect the element in Chrome, right click on it, and select "BREAK ON -> Attributes Modifications." When this is selected for the node, the JavaScript debugger will kick-in anytime the attributes on node are changed. This gives me a chance to see state change for the Element that would otherwise be instantaneous. The downside to this is that it opens the debugger *A LOT*.

Reply to this Comment

@Prashant,

It sounds like your might need to add a "track by" in the ngRepeat to ensure that the same items in the list are tracked by the same identifier. It could be that existing [filtered] items are being added to the list because they are being created with new object references? I'm not entirely sure.

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.