Skip to main content
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Erik Meier and Max Pappas and Reem Jaghlit
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Erik Meier Max Pappas ( @maxian ) Reem Jaghlit

Experimenting With ngAnimate And ng-animate-ref In AngularJS 1.4

By on

CAUTION: This is an exploratory blog post.

A couple of days ago, in response to my post about animating static elements using ngAnimate, Dave Ackerman asked me about the new "ng-animate-ref" feature in AngularJS 1.4. I had not seen of this before, so I wanted to take a quick look. The documentation isn't super obvious at first; so, this post is really just me trying to get something to work.

Run this demo in my JavaScript Demos project on GitHub.

The goal of ng-animate-ref is to allow an element to animate across two structural changes within the DOM (Document Object Model). To be totally honest, I can't think of a great use-case for this type of animation off the top of my head. The first few experiments that I wanted to try ended up being a no-go as they only had one structural change. So, while I got something working for this demo, I don't think I have nearly enough experience to speak about proper usage.

Before we look at any code, I had a few technical blindspots that I had to overcome through trial-and-error and much source code viewing:

  • The ng-animate-ref attribute can use interpolation. This allows a dynamic element in one view to have the same ng-animate-ref attribute value as a dynamic element in another view (which is required for the animation to take place).

  • This only pertains to structural changes. Meaning, it only involves elements that are being added or removed from the DOM - it won't apply to class-based changes.

  • This requires two simultaneous structural changes. Meaning, you can't animate an arbitrary element into a new location if the origin element isn't also being removed. Both animations have to be initiated in the same post-digest phase.

  • This requires that the two related structural elements have at least one non-core CSS class in common. A non-core class is any class that does not begin with "ng-". This is required to "marry" the containers together.

  • The origin element needs to have some sort of transition so that ngAnimate has time to calculate the location and size of the origin element.

  • Once the target location is calculated, it will not be recalculated mid-transition.

Ok, that said, let's take a look at some code. In the following demo, I have two ngRepeat lists, one on either side of the screen. When an item in one of the lists is selected by the user, it is removed from the current list and inserted into the other list. We're going to use the ng-animate-ref feature to animate that change across the two lists. Since ngRepeat creates structural changes, we will be marrying our "ng-leave" element to our "ng-enter" element, using the "thumbnail-ref-container" class, and animating the embedded thumbnail to the new location.

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Experimenting With ngAnimate And ng-animate-ref In AngularJS 1.4
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

	<h1>
		Experimenting With ngAnimate And ng-animate-ref In AngularJS 1.4
	</h1>

	<div class="collection unselected">

		<h3>
			Unselected ( {{ unselectedImages.length }} )
		</h3>

		<ul>
			<!--
				In order for the ng-animate-ref to work, the LEAVE'ing element and the
				ENTER'ing element have to share at least one non-core CSS class. In this
				case, they are both going to have "thumbnail-ref-container".

				CAUTION: This class name cannot begin with "ng-".

				NOTE: The actual "transport" element that gets created is based on the
				[ng-animate-ref] attribute, not the "class in common."
			-->
			<li
				ng-repeat="image in unselectedImages track by image.id"
				class="thumbnail-ref-container">

				<a ng-click="selectImage( image )">
					<img
						ng-src="./images/{{ image.id }}.{{ image.ext }}"
						ng-animate-ref="image-{{ image.id }}"
						class="thumbnail"
					/>
				</a>

			</li>
		</ul>

	</div>

	<div class="collection selected">

		<h3>
			Selected ( {{ selectedImages.length }} )
		</h3>

		<ul>
			<!--
				In order for the ng-animate-ref to work, the LEAVE'ing element and the
				ENTER'ing element have to share at least one non-core CSS class. In this
				case, they are both going to have "thumbnail-ref-container".
			-->
			<li
				ng-repeat="image in selectedImages track by image.id"
				class="thumbnail-ref-container">

				<a ng-click="unselectImage( image )">
					<img
						ng-src="./images/{{ image.id }}.{{ image.ext }}"
						ng-animate-ref="image-{{ image.id }}"
						class="thumbnail"
					/>
				</a>

			</li>
		</ul>

	</div>

	<!--
		When the LI is removed from one list and added to the other list, an ng-anchor
		transport element will be created and appended to the body. It will look something
		like this and will use any existing classes on the origin element (IMG) plus the
		ng-anchor related classes.

		<img
			ng-animate-ref="image-1"
			src="./images/1.jpg"
			class="thumbnail ng-anchor ng-anchor-out-add ng-anchor-out ng-anchor-out-add-active"
			style="width: 75px; height: 100px; top: 163px; left: 41px;"
		/>
	-->


	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.4.5.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 ) {

				// I am the collection of unselected images.
				$scope.unselectedImages = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ].map(
					function operator( id ) {

						return({
							id: id,
							ext: "jpg"
						});

					}
				);

				// I am the collection of selected images.
				$scope.selectedImages = [];


				// ---
				// PUBLIC METHODS.
				// ---


				// I select the given image, moveing it to the selected list.
				$scope.selectImage = function( image ) {

					moveImageTo( image, $scope.unselectedImages, $scope.selectedImages );

				};


				// I unselect the given image, moveing it to the unselected list.
				$scope.unselectImage = function( image ) {

					moveImageTo( image, $scope.selectedImages, $scope.unselectedImages );

				};


				// ---
				// PRIVATE METHODS.
				// ---


				// I move the given image from one list to the other.
				function moveImageTo( image, from, to ) {

					var index = from.indexOf( image );

					from.splice( index, 1 );
					to.push( image );

					// After the list is mutated, let's sort it to make the animations
					// a bit easier to see on the screen.
					to.sort(
						function operator( a, b ) {

							return( ( a.id < b.id ) ? -1 : 1 )

						}
					);

				}

			}
		);

	</script>

</body>
</html>

One thing to drive home is that while the LI elements are the ones being linked via the "thumbnail-ref-container" class, they are not the ones being animated by ng-animate-ref. Yes, they are being animated by the "ng-leave" and "ng-enter" aspects of ngAnimate; but, it is the IMG tag, within the LI, that is actually being animated by the ng-animate-ref feature.

Now, let's look at the CSS for this demo. The transitional stuff is down at the bottom:

div.collection {
	border: 2px solid #CCCCCC ;
	min-width: 200px ;
	position: absolute ;
	top: 90px ;
	user-select: none ;
		-moz-user-select: none ;
		-webkit-user-select: none ;
}

div.collection.unselected {
	left: 30px ;
}

div.collection.selected {
	right: 30px ;
	text-align: right ;
}

div.collection h3 {
	background-color: #FAFAFA ;
	border-bottom: 1px solid #CCCCCC ;
	margin: 0px 0px 0px 0px ;
	padding: 10px 0px 10px 0px ;
	text-align: center ;
}

ul {
	list-style-type: none ;
	margin: 0px 0px 0px 0px ;
	padding: 20px 20px 20px 20px ;
}

ul li {
	margin: 20px 0px 20px 0px ;
	padding: 0px 0px 0px 0px ;
}

ul li:first-child {
	margin-top: 0px ;
}

ul a {
	cursor: pointer ;
	display: block ;
}

ul img {
	display: block ;
	max-height: 100px ;
	max-width: 100px ;
}


/*
	List Transitions.

	In this case, we don't really need an ENTER transition since the enter item will
	be hidden during the ng-anchor transportation (via visibility:hidden). As such,
	we only need to have a LEAVE transition to create a pleasing visual effect:

	1. The leave-hole doesn't fill up immediately.
	2. It gives ngAnimate time to locate the origin and position the transport.
*/

ul li.ng-leave {
	opacity: 1.0 ;
	transition: opacity 250ms ease ;
}

ul li.ng-leave-active {
	opacity: 0.0 ;
}


/*
	Anchor Transitions.

	Because this anchor element is added directly to the BODY, we can't really make
	the class definitions contextual. They have to be self-contained.
*/

img.ng-anchor {
	z-index: 10 ;
}

img.ng-anchor-out {
	/* I don't have any transition for the first-half of the transport. */
}

img.ng-anchor-in {
	transition: all 250ms ease ;
}

The ng-animate-ref transition runs in two phases: out and in. This gives the two related views time to get into their appropriate locations (remember, the target location isn't recalculated mid-phase). In this case, however, I don't need a transition for the "out" phase since the parent ngRepeat containers are always on the page. As such, I don't need to wait for the target container to show up - I can forego the "out" transition and only define the "in" transition, which will animate the thumbnail from one list to the other.

When we run this page and click on two of the images, you can see that we get two temporary IMG tags appended to the BODY tag:

ng-animate-ref in AngularJS 1.4 allows us to transition an element from one structure change to another.

Anyway, that's my first look at the ng-animate-ref feature. Since this is both a new feature for AngularJS and a new feature for myself, I apologize ahead of time for any technical inaccuracies or poorly worded explanations in this post. I'm just one man trying to wrap his head around some funky stuff!

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel