Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Sandy Clark
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Sandy Clark@sandraclarktw )

Animating A Single Item Using ngRepeat And ngAnimate In AngularJS

By Ben Nadel on

It's easy to think about animations when you have objects that have conditional existence. Meaning, sometimes they're on the page, sometimes they're not. But, what about when you have data that is always present, but is sometimes changing? How can you animate that? One idea would be to create a custom directive that watches the data and leverages the $animate service explicitly. But, it turns out, with a funky hack, of sorts, you can use the native ngRepeat directive to animate changes within a single object reference.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Sometimes, I have a situation in which my Controller has a collection of data; but, I'm only ever rendering the "selected" item in that collection. To do this, I'll typically use some sort of "selected" View-Model value that references the selected item within the collection. Using this approach, however, the structure of the DOM (Document Object Model) tree isn't really changing as I iterate through the collection - it's really just the text interpolation that's being updated.

So, how would I animate this change? At first thought, I considered using the native ngIf directive. It allows for animation and only deals with a single reference. However, this doesn't work because, while I am dealing with a single item, I do need to transition two items simultaneously: the newly selected item and the previously selected item.

After a bit of noodling, I thought I might be able to use the ngRepeat directive. It does what I want, in so much as that it will transition items in and out simultaneously. But, I didn't want to have to maintain an additional "selected collection" in my Controller just for the ngRepeat. Luckily, I don't have to. In my View, I can create a single-item collection, on-the-fly, that watches for changes in my "selected item" View-Model reference.

To see this in action, I created a demo in which I maintain a collection of Friends; but, I only ever render the currently "selected friend." I have controls to move to the previous and next friend. And, using ngRepeat, I can easily animate that transition:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Animating A Single Item Using ngRepeat And ngAnimate In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Animating A Single Item Using ngRepeat And ngAnimate In AngularJS
  • </h1>
  •  
  • <div class="container {{ orientation }}">
  •  
  • <!--
  • Here, we are using a "hack" to attach ngAnimate to a "single item" cycle.
  • There is no "collection" in this demo - we only have "selectedFriend"; so,
  • in order to animate changes to the single item, we stuff it into an inline
  • array. This way, every time the selectedFriend changes, the single item in
  • the array will be replaced. This will cause ngRepeat to trigger a "leave"
  • and an "enter" animation event.
  •  
  • NOTE: "selectedFriend" doesn't overwrite itself! In the context of the "[]"
  • notation, it's in the main scope; however, when it's define as the item in
  • the repeater, it's in a CHILD SCOPE of the repeater. Same object, just stored
  • in two different scopes.
  • -->
  • <div
  • ng-repeat="selectedFriend in [ selectedFriend ]"
  • class="friend">
  •  
  • <div class="name">
  • {{ selectedFriend.name }}
  • </div>
  •  
  • <div class="meta">
  • ID: {{ selectedFriend.id }}
  • &nbsp;|&nbsp;
  • {{ selectedFriend.nickname }}
  • </div>
  •  
  • </div>
  •  
  • </div>
  •  
  • <p class="controls">
  • &laquo;
  • <a ng-click="showPrevious()">Previous Friend</a>
  • &mdash;
  • <a ng-click="showNext()">Next Friend</a>
  • &raquo;
  • </p>
  •  
  •  
  • <!-- 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 contain the list of friends to iterate in our cycle UI.
  • var friends = [
  • {
  • id: 1,
  • name: "Sarah",
  • nickname: "Stubbs"
  • },
  • {
  • id: 2,
  • name: "Heather",
  • nickname: "Squeaks"
  • },
  • {
  • id: 3,
  • name: "Kim",
  • nickname: "Stud Muffin"
  • },
  • {
  • id: 4,
  • name: "Joanna",
  • nickname: "Jo-Jo"
  • }
  • ];
  •  
  • // I hold the index of the selected friend in the array.
  • var selectedIndex = 0;
  •  
  • // I relay the orientation of the cycle as the user moves from one
  • // friend to the next (whether its a "previous" orientation or a
  • // "next" orientation). This is used to make the animation feel more
  • // natural.
  • $scope.orientation = "next";
  •  
  • // I am the selected friend, currently being displayed.
  • $scope.selectedFriend = friends[ selectedIndex ];
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I move to the previous friend in the collection.
  • $scope.showPrevious = function() {
  •  
  • $scope.orientation = "previous";
  •  
  • if ( --selectedIndex < 0 ) {
  •  
  • selectedIndex = ( friends.length - 1 );
  •  
  • }
  •  
  • $scope.selectedFriend = friends[ selectedIndex ];
  •  
  • }
  •  
  •  
  • // I move the next friend in the collection.
  • $scope.showNext = function() {
  •  
  • $scope.orientation = "next";
  •  
  • if ( ++selectedIndex >= friends.length ) {
  •  
  • selectedIndex = 0;
  •  
  • }
  •  
  • $scope.selectedFriend = friends[ selectedIndex ];
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

At first, the ngRepeat expression might be confusing:

selectedFriend in [ selectedFriend ]

It looks like I'm overwriting the "selectedFriend" variable. But, I'm not. In the context of the inline array, the selectedFriend refers to the selectedFriend within my Controller's View-Model. However, as the ngRepeat unrolls the array, it creates a new child scope for each item. In this expression, I'm using selectedFriend as the "item" reference as well. So, ngRepeat is, essentially, copying the selectedFriend reference from the parent scope into the child scope of the single-item repeater.

Now, as the selectedFriend changes, it means that the item at index 0 - the only item in the collection - changes. This causes ngRepeat to animate-out the previous item and animate-in the new item:


 
 
 

 
 Using ngRepeat and ngAnimate to animate a single item outside the context of a traditional collection. 
 
 
 

That's pretty awesome! I know this feels like a "hack"; but, it's just so dang easy. As a follow-up to this, maybe I'll try and build a custom AngularJS "component directive" that does the same kind of thing using the $animate service explicitly. I think what we'll see, though, is a lot of code that does exactly what ngRepeat is already going for us.




Reader Comments

depending on the context, I'd be more towards using

  • ui-view

/

  • ng-view

, as it gives you full control over the state of this "component" + animations work out of the box ;-)

Reply to this Comment

@Tomasz,

Will that work if the state is not driven by the Location / Route? In my demo, the selection of the Friend is not tied to the route. It's just a view-model value. I am not too familiar with the UI-view / NG-view.

Reply to this Comment

@Ben,

well, that was the whole idea behind it - to tie it to the route :-)
stateless "component" = more control = profit ;-)

in order to skip the binding, you were correct - one would need to build one that uses $transclude argument of linking function and duplicates the template with new content and follows ng-enter/ng-leave logic.

Reply to this Comment

@Tomasz,

I can dig it :) The irony of it is that the Angular-UI module, from which the ng-if directive was extracted, used to work that way(ish) - it didn't use a boolean - it compared the actually value. So, ui-if="1" and ui-if="2" would actually lead to two different linking invocation (even though they are both truthy).

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.