Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: John Ramon
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: John Ramon ( @johnramon )

Cross-Fading Images With AngularJS

By on

In AngularJS, your Controllers don't know anything about your DOM (Document Object Model) - they know about data and about exposing behavior. Views, on the other hand, are all about the DOM; but, they know nothing about user interactions. All user interactions and UI (User Interface) events are managed by Directives. When you get into AngularJS, it can be really tricky to wrap your head around this division of responsibilities. This is especially true when you're dealing with a situation that feels particularly "UI oriented." Cross-fading images, for example, might be something you've done a thousand times with jQuery; but, in an AngularJS context, old-habits can be hard to translate.

View this demo in my JavaScript-Demos project on GitHub.

When I refer to "cross-fading" images, I mean fading one image into another image during something like a slide-show or an image presentation. AngularJS makes it dead-simple to show an image based on a user's choice; in fact, that's the kind of stuff that makes AngularJS seem completely magical. But the "cross-fade" effect, on the other hand, is not something that AngularJS really facilitates.

So, how can we cross-fade images in an "Angular" way? Well, to start with, I would suggest not cross-fading the images at all. As I've talked about before, by delaying the programming of "effects," you help to ensure that the appropriate division of responsibilities is maintained within your AngularJS application. Then, only after the low-fidelity version works, should you go in and add the effects using a Directive (or set of Directives).

In the following demo, I am using the custom Directive, "bnFadeHelper." However, I added this Directive only after I had my image presentation demo working. This kept all of the data in my Controller and properly quarantined the animations (and animation logic) within my Directive. In fact, you can take the directive out and the demo still works.

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

	<title>
		Cross-Fading Images With AngularJS
	</title>

	<style type="text/css">

		p.viewport {
			border-radius: 5px 5px 5px 5px ;
			box-shadow: 1px 1px 5px #454545 ;
			height: 400px ;
			position: relative ;
			width: 400px ;
		}

		p.viewport img.fader {
			border-radius: 5px 5px 5px 5px ;
			display: none ;
			height: 400px ;
			left: 0px ;
			position: absolute ;
			top: 0px ;
			width: 400px ;
			z-index: 2 ;
		}

		p.viewport img.fader.show {
			display: block ;
			transition: opacity 250ms ease ;
		}

		p.viewport img.fader.fadeOut {
			opacity: 0 ;
		}

		p.viewport img.image {
			border-radius: 5px 5px 5px 5px ;
			display: block ;
			height: 400px ;
			width: 400px ;
		}

		p.nav {
			cursor: default ;
			text-align: center ;
			width: 400px ;
		}

		p.nav a {
			background-color: #FAFAFA ;
			border: 1px solid #CCCCCC ;
			border-radius: 3px 3px 3px 3px ;
			color: #333333 ;
			display: inline-block ;
			padding: 4px 9px 2px 9px ;
			text-decoration: none ;
		}

	</style>
</head>
<body>

	<h1>
		Cross-Fading Images With AngularJS
	</h1>

	<p bn-fade-helper class="viewport">

		<img
			ng-src="{{ image.source }}"
			title="{{ image.title }}"
			class="image"
		/>

	</p>

	<p class="nav">
		<a ng-click="showImage( 1 )">1</a> -
		<a ng-click="showImage( 2 )">2</a> -
		<a ng-click="showImage( 3 )">3</a> -
		<a ng-click="showImage( 4 )">4</a> -
		<a ng-click="showImage( 5 )">5</a> -
		<a ng-click="showImage( 6 )">6</a>
	</p>


	<!-- Load jQuery and AngularJS from the CDN. -->
	<script
		type="text/javascript"
		src="//code.jquery.com/jquery-2.0.0.min.js">
	</script>
	<script
		type="text/javascript"
		src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
	</script>
	<script type="text/javascript">


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


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


		// I control the root of the application.
		app.controller(
			"AppController",
			function( $scope ) {

				// I am the collection of images to show.
				$scope.images = [
					{
						source: "frances-1.jpg",
						title: "What are you smirking about?"
					},
					{
						source: "frances-2.jpg",
						title: "No makeup - no problem."
					},
					{
						source: "frances-3.jpg",
						title: "Grace and beauty."
					},
					{
						source: "frances-4.jpg",
						title: "Another smirk."
					},
					{
						source: "frances-5.jpg",
						title: "What are you looking at?"
					},
					{
						source: "frances-6.jpg",
						title: "Saddness."
					}
				];

				// Default to a random image.
				$scope.image = getRandomImage();


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


				// I show the image at the given index.
				$scope.showImage = function( index ) {

					// Use -1 to adjust image for zero-based array.
					$scope.image = $scope.images[ index - 1 ];

				};


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


				// I return a random image from the current collection.
				function getRandomImage() {

					var imageCount = $scope.images.length;

					var index = Math.floor(
						( Math.random() * imageCount * 2 ) % imageCount
					);

					return( $scope.images[ index ] );

				}

			}
		);


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


		// I provide a "Fade" overlay for the primary image whenever
		// the primary image source changes. This allows for a "softer"
		// transition from image to image.
		app.directive(
			"bnFadeHelper",
			function() {

				// I alter the DOM to add the fader image.
				function compile( element, attributes, transclude ) {

					element.prepend( "<img class='fader' />" );

					return( link );

				}


				// I bind the UI events to the $scope.
				function link( $scope, element, attributes ) {

					var fader = element.find( "img.fader" );
					var primary = element.find( "img.image" );

					// Watch for changes in the source of the primary
					// image. Whenever it changes, we want to show it
					// fade into the new source.
					$scope.$watch(
						"image.source",
						function( newValue, oldValue ) {

							// If the $watch() is initializing, ignore.
							if ( newValue === oldValue ) {

								return;

							}

							// If the fader is still fading out, don't
							// bother changing the source of the fader;
							// just let the previous image continue to
							// fade out.
							if ( isFading() ) {

								return;

							}

							initFade( oldValue );

						}
					);


					// I prepare the fader to show the previous image
					// while fading out of view.
					function initFade( fadeSource ) {

						fader
							.prop( "src", fadeSource )
							.addClass( "show" )
						;

						// Don't actually start the fade until the
						// primary image has loaded the new source.
						primary.one( "load", startFade );

					}


					// I determine if the fader is currently fading
					// out of view (that is currently animated).
					function isFading() {

						return(
							fader.hasClass( "show" ) ||
							fader.hasClass( "fadeOut" )
						);

					}


					// I start the fade-out process.
					function startFade() {

						// The .width() call is here to ensure that
						// the browser repaints before applying the
						// fade-out class (so as to make sure the
						// opacity doesn't kick in immediately).
						fader.width();

						fader.addClass( "fadeOut" );

						setTimeout( teardownFade, 250 );

					}


					// I clean up the fader after the fade-out has
					// completed its animation.
					function teardownFade() {

						fader.removeClass( "show fadeOut" );

					}

				}


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

			}
		);


	</script>

</body>
</html>

Notice how simple my Controller is? It's basically a collection of objects and a single public method that allows for the selection of one of those objects. There's nothing in my Controller that's related to the image cross-fade. All of that information - all of that DOM (Document Object Model) awareness - is siloed within the bnFadeHelper directive.

The bnFadeHelper Directive watches for changes in the data model; and, when it sees the image selection change, it shows a copy of the previous image and then fades it out of view once it has detected that the new image selection has finished loading.

I am sure there are many other ways to implement a cross-fade effect in JavaScript, all of which can be used in an AngularJS context. But, no matter which approach you take, just make sure that you keep your data and your behavior in your Controllers and your special effects and UI-events in our Directives.

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

Reader Comments

1 Comments

Hi!

Great example!

One important tip - jquery must be included before angularjs. Otherwise, only jqlite is available and doesn't support the needed .find features. It took me a while to figure that out, but once I got it right the code worked just fine.

Thanks!

1 Comments

Thanks I was just planning to do something like this when I found your tutorial. I was planning to use some jquery fade effects inside the Directive but I prefer your approach using mostly CSS rules.

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