Animating A Single Item Using ngRepeat And ngAnimate In AngularJS
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.
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:
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:
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.
Want to use code from this post? Check out the license.
depending on the context, I'd be more towards using
, as it gives you full control over the state of this "component" + animations work out of the box ;-)
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.
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.
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).
Thanks, this is a great trick. Really elegant solution and exactly what I was looking for.
Awesome - glad you enjoyed this approach :)