Preventing Animation During The Initial Render Of ngRepeat In AngularJS
As of AngularJS 1.2, directives that conditionally include content have also supported "enter" and "leave" style animations (with the help of the ngAnimate module). For most of these directives - like ngIf, ngInclude, and ngSwitch - animating the "enter" state makes a lot of sense. But, with ngRepeat, which is a much more stateful directive, animating the initial rendering of the data can lead to a kind of junky user experience. As such, it'd be nice to prevent the initial ngRepeat animation while still allowing animation on subsequent collection mutations.
Run this demo in my JavaScript Demos project on GitHub.
When an item is added to or removed from a list, it makes sense to have ngRepeat animate that transition. The movement pulls the user's eye to the change in data and allows the user to build a more natural, more organic mental model of what's going on. But, animating every item in the collection simultaneously on data-load makes little sense. It doesn't help the mental model and, in all likelihood, the animation was probably designed for a single-item change.
In order to prevent the animation of the initial ngRepeat data-load, we can leverage the fact that AngularJS prevents nested animations from taking place (by default). If we animate-in the parent container only once the ngRepeat data is ready, it will implicitly block the ngRepeat animation. Once everything is rendered, however, additional changes to the ngRepeat collection will naturally lead to "enter," "leave," and "move" style animations.
NOTE: Nested animations are allowed if the parent container has the ngAnimateChildren directive.
Now, just because we are "animating" the parent container, it doesn't actually mean that we're "animating" it. What I mean is, the transition can be instant. If, for example, we use ngIf to show the parent container, AngularJS will consider it "animating" even if we don't have a transition on it. As such, the ngIf can instantly show the nested ngRepeat while still blocking the initial ngRepeat animation.
AngularJS is pretty player that way!
To demonstrate this, I am going to use the ngIf directive to hide the ngRepeat directive until the ngRepeat data has loaded (via a $timeout-based network latency simulation). What you'll notice (if you watch the video or try the demo) is that the initial list shows up instantly while additional items are animated in:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Preventing Animation During The Initial Render Of ngRepeat 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-active {
opacity: 1.0 ;
padding-left: 0px ;
}
</style>
</head>
<body ng-controller="AppController">
<h1>
Preventing Animation During The Initial Render Of ngRepeat In AngularJS
</h1>
<h2>
Friends
</h2>
<form ng-submit="processForm()">
<input ng-model="form.name" type="text" />
<input type="submit" value="Add Friend" />
</form>
<!--
In order to prevent the initial ngRepeat render from animating each ngRepeat
clone, we have to hide the parent container (UL) until the ngRepeat data is
actually available. This way, when the data is available, we'll show the
container which, while instant, will get flagged as "animating". This will
prevent the ngRepeat directive from animating its elements during the initial
render of the friends collection.
Take care that not all "show" directives block / disable nested animations.
Class-based transition animations like ngShow and ngHide will not block or
cancel existing or successive animations. As such, using ngShow would not
prevent the ngRepeat animation from happening on the first load.
--
NOTE: I am using the term "initial render" very loosely to define the visual
rendering of the (not the initial linking of the directive).
-->
<ul ng-if="friends.length">
<!-- These will animate-in using CSS class-based animations. -->
<li
ng-repeat="friend in friends track by friend.id"
class="friend">
{{ friend.name }}
</li>
</ul>
<!-- Show the loading state if no friends yet. -->
<p ng-if="! friends">
<em>Loading data...</em>
</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, friendService ) {
// I will hold the collection of friends to render.
$scope.friends = null;
// I hold the ngModel data.
$scope.form = {
name: ""
};
// Initialize the local data store.
loadRemoteData();
// ---
// PUBLIC METHODS.
// ---
// I process the form, adding a new friend.
$scope.processForm = function() {
if ( ! $scope.form.name ) {
return;
}
friendService.addFriend( $scope.form.name )
.then( loadRemoteData )
;
$scope.form.name = "";
};
// I load the friends collection from the remote repository.
function loadRemoteData() {
friendService.getFriends()
.then(
function handleResolve( friends ) {
$scope.friends = friends;
}
)
;
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I provide access to the "remote" friend repository.
// --
// NOTE: Obviously, this is not a remote service; but, I am putting the "get"
// behind a $timeout() so that we can see a bit of latency, which is where the
// animations need to be a bit more thoughtful.
app.factory(
"friendService",
function( $q, $timeout ) {
// Default our internal collection.
var friends = [
{
id: 1,
name: "Sarah"
},
{
id: 2,
name: "Heather"
}
];
// Return the public API.
return({
addFriend: addFriend,
getFriends: getFriends
});
// ---
// PUBLIC METHODS.
// ---
// I add a friend with the given name and return the new ID (promise).
function addFriend( name ) {
var friend = {
id: ( new Date() ).getTime(),
name: name
};
friends.push( friend );
return( $q.when( friend.id ) );
}
// I get all the friends (promise).
function getFriends() {
var deferred = $q.defer();
$timeout(
function() {
// NOTE: Return a COPY of the collection so that we don't
// break encapsulation and allow external forces to mutate
// our internal collection.
deferred.resolve( angular.copy( friends ) );
},
350,
// We don't need to trigger a digest - $q will do that for us
// when we resolve the deferred value.
false
);
return( deferred.promise );
}
}
);
</script>
</body>
</html>
In the comments, I mention that the animation would not be blocked if you were to use the the ngShow or ngHide directives instead of ngIf. This is because ngShow and ngHide use CSS class-based transitions. Meaning, they work by adding and removing the ".ng-hide" CSS class, as opposed to creating and destroying actual DOM (Document Object Model) elements. Such class-based transitions do not prevent nested animations (from the documentation):
Class-based transitions refer to transition animations that are triggered when a CSS class is added to or removed from the element (via $animate.addClass, $animate.removeClass, $animate.setClass, or by directives such as ngClass, ngModel and form). They are different when compared to structural animations since they do not cancel existing animations nor do they block successive transitions from rendering on the same element. This distinction allows for multiple class-based transitions to be performed on the same element
When AngularJS 1.2 added animations, I was a bit concerned that the multi-transclusion limitation was overly problematic. But, the more I experiment with the ngAnimate module (which, admittedly, is very new to me), the more excited I get. The AngularJS team seems to have really thought about transitions very deeply.
Want to use code from this post? Check out the license.
Reader Comments
Thanks for this in-deep primer!
@Vlad,
My pleasure! Glad you enjoyed it.
Thanks! That saved my day :)