Skip to main content
Ben Nadel at the New York ColdFusion User Group (Dec. 2008) with: Clark Valberg and Michael Dinowitz
Ben Nadel at the New York ColdFusion User Group (Dec. 2008) with: Clark Valberg ( @clarkvalberg ) Michael Dinowitz ( @mdinowitz )

Conditional Animations And Transition Timing In AngularJS

By on

Animations and transitions add great value to the user experience (UX); but, they do so in two different ways that are often in conflict. The first point of value is that they create a more natural, more organic experience that helps the user build a mental model around the interconnection of different front-end modules. The second point of value is the "Oooo-ahhh" factor. The problem is, while your product and marketing teams love the "Oooo-ahhh" factor, your user probably doesn't. Or at least, not after the first few times they've used your application. As such, I wanted to noodle on a way to get the best of both world by using conditional animations and transition timings.

Run this demo in my JavaScript Demos project on GitHub.

How many of you get a brand new Mac computer, and one of the first things you do is go into the System Preferences and disable the "Genie effect" because it's the most annoying thing ever created? The reason that you do this is because the genie effect has outlived its purpose. The first time you saw it, it was probably great. It had that solid "Oooo-ahhh" factor. And, it helped you understand the connection between your Dock and your applications and how to launch things and where they lived. But, once you understand that - once you get used to using the Dock and opening applications - that effect becomes a burden on the user experience (UX). That effect becomes the "dial-up modem" portion of the application launch.

Luckily, in our computer systems, there are generally ways to turn off things like the Dock animations. But, in our online systems, such preferences are generally non-existent. But, what if we could phase them out? What if the animations and transitions were noticeable at first; and then, quickly faded into the background?

That's exactly what I wanted to try to do with an AngularJS service and corresponding directive. In this experiment, I've created a "bn-conditional-animation" directive that adds an incrementing CSS class name to the linked element. The first time that the directive is linked to the given element, it adds the class:

conditional-animation-n1

... then, the second time it links, it will add the class:

conditional-animation-n2

This "-nX" pattern will continue to increment from 1 to 10 and then stop incrementing (based on the Provider settings). On its own, this does absolutely nothing. It's up to you, as the developer, to wire these conditional classes into your CSS such that the existence of said classes has an impact on the timing of your user interface transitions.

To see this in action, I've created a demo in which two different lists of data can be toggled. Both of these lists use the conditional animation directive; but, each of them uses different settings based on the CSS.

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

	<title>
		Conditional Animations And Transition Timing In AngularJS
	</title>

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

	<h1>
		Conditional Animations And Transition Timing In AngularJS
	</h1>

	<p>
		<a ng-click="toggleFriends()">Toggle Friends</a>
		&mdash;
		<a ng-click="toggleEnemies()">Toggle Enemies</a>
	</p>

	<!--
		The list of friends will only animate the first 2 times it is rendered. After
		that, it will show up and hide instantly.
	-->
	<div
		ng-if="isShowingFriends"
		class="box friends"
		bn-conditional-animation="friend-list">

		<h2>
			Friends
		</h2>

		<ul>
			<li ng-repeat="friend in friends">
				{{ friend }}
			</li>
		</ul>

	</div>

	<!--
		The list of enemies will only animate the first 5 times it is rendered. After
		that, it will show up and hide instantly.
	-->
	<div
		ng-if="isShowingEnemies"
		class="box enemies"
		bn-conditional-animation="enemies-list">

		<h2>
			Enemies
		</h2>

		<ul>
			<li ng-repeat="enemy in enemies">
				{{ enemy }}
			</li>
		</ul>

	</div>


	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.3.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs/angular-animate-1.4.3.js"></script>
	<script type="text/javascript">

		// Create an application module for our demo.
		angular.module( "Demo", [ "ngAnimate" ] );


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I control the root of the demo.
		angular.module( "Demo" ).controller(
			"AppController",
			function AppController( $scope ) {

				// I determine which boxes are being rendered.
				$scope.isShowingFriends = false;
				$scope.isShowingEnemies = false;

				// I define the list of data points in each box.
				$scope.friends = [ "Sarah", "Joanna", "Kim", "Tricia" ];
				$scope.enemies = [ "Pam", "Anna", "Jane", "Sue", "Cat" ];


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


				// I toggle the rendering of the enemies list.
				$scope.toggleEnemies = function() {

					$scope.isShowingEnemies = ! $scope.isShowingEnemies;

				};


				// I toggle the rendering of the friends list.
				$scope.toggleFriends = function() {

					$scope.isShowingFriends = ! $scope.isShowingFriends;

				};

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I add conditional-animation classes to the linked element on each rendering.
		angular.module( "Demo" ).directive(
			"bnConditionalAnimation",
			function bnConditionalAnimation( conditionalAnimationService ) {

				// Get the settings for conditional animation rendering.
				var classPrefix = conditionalAnimationService.getClassPrefix();
				var maxCount = conditionalAnimationService.getMaxCount();

				// Return the directive configuration object.
				return({
					link: link,
					restrict: "A"
				});


				// I bind the JavaScript events to the local view-model.
				function link( scope, element, attributes ) {

					// Each linked instances of this directive keeps count,
					// individually, of the number of renderings. As such, the
					// developer needs to either provide a unique name; or, just
					// group this rendering with other non-provided names.
					var animationKey = ( attributes[ "bnConditionalAnimation" ] || "*" );

					// The whole point of this is to phase animations out over time.
					// As such, there is a natural limit to the usefulness of this over
					// time. Therefore, we're going to max out the actual class name
					// after 10-renderings. If you need more granularity than that, you
					// might be missing the intent here.
					var count = Math.min(
						conditionalAnimationService.incrementCount( animationKey ),
						maxCount
					);

					// Add the appropriate nth rendering class.
					attributes.$addClass( classPrefix + count );

				}

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I work with the bnConditionalAnimation directive to help keep track of which
		// elements have been linked, and how many times they have been rendered.
		angular.module( "Demo" ).provider(
			"conditionalAnimationService",
			function conditionalAnimationServiceProvider() {

				// I am the prefix used when adding the conditional class names to the
				// rendered elements. This value will be followed by the numeric
				// rendering count for the given instance.
				// --
				// Example: ( "conditional-animation-n" + 3 ).
				var classPrefix = "conditional-animation-n";

				// I am the maximum count that will actually be appended to the class
				// prefix. The internal count will continue to increment; but, overall,
				// when it comes to rendering, the class name will never go above this.
				var maxCount = 10;

				// Return the public API.
				return({
					setClassPrefix: setClassPrefix,
					setMaxCount: setMaxCount,
					$get: conditionalAnimationService
				});


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


				// I set the class prefix to use when adding conditional class names.
				function setClassPrefix( newClassPrefix ) {

					classPrefix = newClassPrefix;

				}


				// I set the max value to be used when rendering class names.
				function setMaxCount( newMaxCount ) {

					maxCount = newMaxCount;

				}


				// ---
				// SERVICE DEFINITION.
				// ---


				// I provide the conditional animation service.
				function conditionalAnimationService() {

					// I hold the count of each rendered item.
					var cache = Object.create( null );

					// Return the public API.
					return({
						getClassPrefix: getClassPrefix,
						getCount: getCount,
						getMaxCount: getMaxCount,
						incrementCount: incrementCount
					});


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


					// I return the class prefix for conditional classes.
					function getClassPrefix() {

						return( classPrefix );

					}


					// I return the number of times the given instance has been rendered.
					function getCount( key ) {

						return( getCachedInstance( key ).count );

					}


					// I return the max count that should be used when generating
					// conditional class names. This value is capped while the underlying
					// render count is unbounded.
					function getMaxCount() {

						return( maxCount );

					}


					// I increment the rendering count for the given instance.
					function incrementCount( key ) {

						return( ++ getCachedInstance( key ).count );

					}


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


					// I return the cache of the given instance. If the cache does not
					// yet exist, it is created and returned.
					function getCachedInstance( key ) {

						var normalizedKey = normalizeKey( key );

						// Ensure the existence of the cache.
						if ( ! cache[ normalizedKey ] ) {

							cache[ normalizedKey ] = {
								key: key,
								count: 0
							};

						}

						return( cache[ normalizedKey ] );

					}


					// I return a normalized key that won't collide with any other
					// values on the Object prototype.
					// --
					// NOTE: Since we are using Object.create( null ) to setup the cache,
					// this probably isn't necessary.
					function normalizeKey( key ) {

						return( "animation:" + key );

					}

				}

			}
		);

	</script>

</body>
</html>

As you can see, when I attach the directive to a given element, I have to give it some sort of unique identifier so that the conditional animation service knows how to localize the incrementing value. I thought maybe I could use the existing class value as the identifier if an explicit one was omitted; but, I'd have to think harder about the practical use-cases.

In this particular demo, I'm attaching this directive to the animating element. But, something like this wouldn't work on a list of values (like an ngRepeat) as each list item would cause the directive-linking to increment the internal count. In such a scenario, you'd want to move the conditional class up to a parent element and then use contextual CSS to manage the animations.

In this version of the directive, the animations reset when the page is refreshed. But, this code could easily be updated to persist to something like localStorage in order to maintain animation counts across refreshes. That said, resetting the counts might provide an opportunity to "reeducate" the user whenever they return to the application.

Now, I'm no CSS master, but this is the CSS that I'm using for this particular demo. What you'll want to notice is how the "conditional-animation-nX" classes are used to set the transition timing:

a[ ng-click ] {
	color: red ;
	cursor: pointer ;
	text-decoration: underline ;
	user-select: none ;
		-moz-user-select: none ;
		-webkit-user-select: none ;
}

div.box {
	background-color: #F0F0F0 ;
	border: 1px solid #CCCCCC ;
	border-radius: 3px 3px 3px 3px ;
	padding: 6px 20px 6px 20px ;
	position: relative ;
}


/* Set up the friends timing. */

div.friends.ng-enter {
	opacity: 0.0 ;
	transition: 0s opacity ease ;
}

div.friends.ng-enter-active {
	opacity: 1.0 ;
}

div.friends.ng-leave {
	transition: 0s opacity ease ;
}

div.friends.ng-leave-active {
	opacity: 0.0 ;
}

div.friends.conditional-animation-n1 {
	transition-duration: 1s ;
}

div.friends.conditional-animation-n2 {
	transition-duration: 0.5s ;
}


/* Set up the enemies timing. */

div.enemies.ng-enter {
	opacity: 0.0 ;
	transition: 0s opacity ease ;
}

div.enemies.ng-enter-active {
	opacity: 1.0 ;
}

div.enemies.ng-leave {
	transition: 0s opacity ease ;
}

div.enemies.ng-leave-active {
	opacity: 0.0 ;
}

div.enemies.conditional-animation-n1 {
	transition-duration: 1s ;
}

div.enemies.conditional-animation-n2 {
	transition-duration: 0.85s ;
}

div.enemies.conditional-animation-n3 {
	transition-duration: 0.6s ;
}

div.enemies.conditional-animation-n4 {
	transition-duration: 0.4s ;
}

div.enemies.conditional-animation-n5 {
	transition-duration: 0.15s ;
}


/* Set up the "info note" rendering. */

div.box.conditional-animation-n1:before,
div.box.conditional-animation-n2:before,
div.box.conditional-animation-n3:before,
div.box.conditional-animation-n4:before,
div.box.conditional-animation-n5:before,
div.box.conditional-animation-n6:before,
div.box.conditional-animation-n7:before,
div.box.conditional-animation-n8:before,
div.box.conditional-animation-n9:before,
div.box.conditional-animation-n10:before {
	background-color: red ;
	border-radius: 3px 0px 3px 0px ;
	color: #FFFFFF ;
	content: "Render n1" ;
	font-size: 13px ;
	left: 0px ;
	position: absolute ;
	padding: 5px 10px 5px 10px ;
	top: 0px ;
}

div.box.conditional-animation-n2:before {
	content: "Render n2" ;
}

div.box.conditional-animation-n3:before {
	content: "Render n3" ;
}

div.box.conditional-animation-n4:before {
	content: "Render n4" ;
}

div.box.conditional-animation-n5:before {
	content: "Render n5" ;
}

div.box.conditional-animation-n6:before {
	content: "Render n6" ;
}

div.box.conditional-animation-n7:before {
	content: "Render n7" ;
}

div.box.conditional-animation-n8:before {
	content: "Render n8" ;
}

div.box.conditional-animation-n9:before {
	content: "Render n9" ;
}

div.box.conditional-animation-n10:before {
	content: "Render n10" ;
}

The whole point of this is create a user experience that has longevity. Meaning, it continues to be good over time. If your user ever stops and thinks, "Oh, cool animation," that's fine the first or second time; but, it's a user experience failure every subsequent time. A great user experience is transparent (source: InVisionApp blog). But, sometimes you need a little of the razzle-dazzle and this kind and an approach might be able to provide the best of both worlds.

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