A Loading Overlay

In this tutorial, we’ll create an overlay that puts us in control of the user experience while parts of an application load in the background.

  • Difficulty: Intermediate
  • Dojo Version: 1.6

The Problem

Loading various scripts and other resources and building even a relatively simple UI can take time. As widgets are instantiated and data is fetched, distracting re-rendering of the page can occur. To avoid this, it’s common practice to overlay some or all of your page with a “loading” message that lets your users know what’s going on while simultaneously hiding any re-rendering or intermediate states in your UI that you don’t want them to see. The solution should not cause significant additional load time by requiring too many of its own dependencies, and it should avoid hard-wiring its behavior to any particular event so we can keep some flexibility in how we define “loaded”.

The Solution

Using methods from Dojo Base, we’ll inject an overlay element and use CSS to style and position it. A simple API will be developed to easily switch in and out of the loading state, and some simple animation will be used to provide a smooth transition between states. This will be accomplished using dojo.create, dojo.fadeOut, dojo.style, and dojo.ready.

Here’s what we will be building:

Demo screenshot with loading overlay fading out

Discussion

One of the challenges here is not the implementation itself, but juggling the priority of various requirements. One could imagine creating a complicated loader widget with a percentage bar, maybe even a non-modal or minimized state which allows the user to continue interacting with the rest of the page while other components load in the background. This not that recipe; we wanted to keep this implementation lightweight and strike a balance between demonstrating practical techniques for developing with Dojo, while keeping it simple enough to solve the most common case using a minimum of resources.

Step 1: Building the Overlay

If you use your DOM inspector of choice on the demo page, you’ll see the loading overlay appears as a child element of the <body>. It’s just a plain <div>, and its size and positioning are handled by CSS.

Overlay in the Firebug inspector

We’re creating the overlay element in JavaScript so that it doesn’t need to be hardcoded into every page we want to apply this behavior to. Since all of the ingredients to do this are included in Dojo Base, we don’t even necessarily need to wait for dojo.ready—as long as our code runs after dojo.js has been included on the page, and after the <body> element is created (but not necessarily closed/complete), we can add elements to the DOM at will.

Our code to generate the overlay looks like this:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<title>Demo: Loading Overlay (Step 1)</title>
		<link rel="stylesheet" href="style.css" media="screen">
		<!-- load dojo and provide config via data attribute -->
		<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.6/dojo/dojo.xd.js"
			data-dojo-config="isDebug: true">
		</script>
	</head>
	<body>
		<script>
		var wrapper = dojo.create("div", {
			id: "loadingOverlay",
			"class": "loadingOverlay pageOverlay",
			innerHTML: '<div class="loadingMessage">Loading...</div>'
		}, dojo.body());
		</script>
		<h1>Demo: Loading Overlay</h1>
		<p>The rest of your page...</p>
	</body>
</html>

The corresponding stylesheet looks like this:

.loadingOverlay {
    background:  #fff url('/js/dojo/1.6/dijit/themes/claro/images/loadingAnimation.gif') no-repeat 10px 23px;
    position: absolute;
    z-index: 1001;
    display: none;
}

.pageOverlay {
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
}

.loadingMessage {
    padding: 25px 40px;
    color: #999;
}

Because we’re injecting the overlay element using Dojo, if Dojo doesn’t load for some reason, or the user has JavaScript disabled, the overlay simply won’t be created. That’s good, because it means the site can still be used in those scenarios. The alternative approach of having the overlay element already in the page and visible on load means that, unless everything is working perfectly, the overlay remains enabled forever with no way for the user to dismiss it.

The Dijit themes each come with their own images directory, complete with icons, gradients, and other handy graphical assets used by the Dijit widgets. Even if you don’t use Dijit on your page, all these graphics are licensed under the same terms as the code and are ready for use.

A module structure

There are three things we will be wanting from this overlay:

  1. Creation and set-up of the overlay element(s)
  2. Show the overlay to indicate loading in progress
  3. Hide the overlay to indicate loading complete

We'll turn this into three methods on an overlay object: init, show and hide.

To make management of private properties and state easier; and to avoid creating unnecessary globals, we’ll write a function that returns our loadingOverlay object. The approach we'll use is sometimes known as the "module pattern" and looks like this:

var loadingOverlay = (function(){

	// local variables, only accessible to functions declared within this scope
	var foo = null;

	// return an object, with function properties (methods)
	return {
		init: function(){
			// perform any initialization
			
			// return 'this' as a convenience to faciliate chaining
			// e.g. overlay.init().show()
			return this;
		},
		show: function(){
			// show the overlay element
		},
		hide: function(){
			// hide the overlay element
		}
	};
})();

We can move the overlay creation code discussed earlier directly into the init function. The show and hide methods will reference the element we created as a overlayNode variable:

var overlayNode;
return {
	init: function(){
		// create the container element for the overlay
		// We store the reference in the overlayNode closure variable
		overlayNode = dojo.create('div', {
			id: 'loadingOverlay',
			'class': 'loadingOverlay pageOverlay',
			innerHTML: '
Loading...
' }, dojo.body()); return this; }, ...

Step 2: Show and Hide the Overlay

Our first attempt at adding functions to show and hide the overlay is really simple:

{
	init: function() { ... }
	show: function(){
		// show the overlay
		dojo.style( overlayNode, {
			display: 'block'
		});
	},
	hide: function(){
		// hide the overlay
		dojo.style( overlayNode, {
			display: 'none'
		});
	}
}

Our CSS has already positioned the overlay, so all we need to do in JavaScript is toggle the display style property to make it show up. While there are many different methods for doing this, using dojo.style helps prevent accidental specificity conflicts (versus adding/removing a class) and ensures any cross-browser behavior is normalized (versus setting style properties directly on the element).

We can now test this from the console. Load up the page, and in your browser’s console type: loadingOverlay.hide():

Invoking demo.endLoading from the Firebug console

View Demo (Step 2)

Step 3: Adding a Transition

Simply toggling the display of the overlay makes for an abrupt transition. We can make this experience much nicer by fading out the overlay to reveal the page:

endLoading: function(){
	dojo.fadeOut({
		node: dojo.byId("loadingOverlay"),
		onEnd: function(node){
			dojo.style(node, "display", "none");
		}
	}).play();
}, …

View Demo

dojo.fadeOut is part of Dojo Base, so is available and ready to use immediately. However, all it does is animates the opacity of the node; we need to add an additional step at the end of the animation to set display: none. Without this extra step, the overlay would still be present but invisible and would block clicks on the page.

Remember that animations happen asynchronously, so any statements after dojo.fadeOut will run before the animation completes. Review the effects and animation tutorials for more information on using animation.

Step 4: Defining “Loaded”

With the implementation of our API essentially complete, our last step is to choose when to call each function. You’re probably familiar by now with dojo.ready, so let’s hook that up:

// put up the loading overlay while the page initializes
loadingOverlay.init().show();

dojo.ready(function(){
	// take down the loading overlay when the page is ready
	loadingOverlay.hide();
});

View Demo

That works, and perhaps it is enough. As you add code dependencies to your page with dojo.require, the overlay will stay up until they are all loaded. However, since we created an API for this overlay, we’ve got much more flexibility in how we define “loaded”. We can call loadingOverlay.hide() whenever we want and with its partner function loadingOverlay.show(), we can re-use this overlay later for other load-related events (such as Ajax requests).

To explore this, let's add some more context and see how it plays out with a page with multiple load events. Our plain demo page now makes a Twitter search API call to fetch a list of recent mentions of a given keyword. There's an initial "loaded" event when the page, its dependencies and the initial query are loaded. By clicking a button to make a new query, the page goes back into a loading state and we need to toggle the overlay again.

View Demo

There's a new demo object with requestData and renderItems methods. These are responsible for getting the query results from Twitter into the page. The piece of interest for this tutorial is the integration of the loading overlay:

dojo.ready(function(){
	// load the initial query results
	demo.requestData('dojotoolkit').then(function(){
		// remove the loading overlay when complete
		loadingOverlay.hide();
	});
	
	dojo.query("#queries button").onclick(function(evt){
		// put up the overlay at the start of a new query
		loadingOverlay.show();
		// clear out the old content
		dojo.empty("demoList");
		
		// request the new content
		demo.requestData( evt.target.innerHTML ).then(function(){
			// remove the loading overlay when complete
			loadingOverlay.hide();
		});
	});
});

The demo.requestData function returns a Deferred object; it lets us add callbacks that will be executed when the request is complete. (You can find out more about this technique in the Getting Started with Deferreds tutorial.) Up to this point, the loading overlay is still visible, so instead of simply adding the code to hide it in a dojo.ready block, we can hide it here in the requestData callback.

The dojo.query statement sets up a click handler on each of the buttons to issue a new Twitter query. We put up the loading overlay at the start of this operation and take it down in the requestData callback.

Polish and refinements

Although the demo is functional, to really tighten up there are a few refinements needed. If you clicked the buttons fast enough, the requests stack up and we can end up with overlapping fadeOut animations and even rendering the same results multiple times. To make it reentrant we can add guards to handle both these problems:

dojo.ready(function(){			
	// load the initial query results
	var request = demo.requestData('dojotoolkit').then(function(){
		request = null;
	});
	
	dojo.query("#queries button").onclick(function(evt){
		if(request){
			// cancel an in-flight request
			request.cancel();
		}
		// put up the overlay at the start of a new query
		loadingOverlay.show();
		// clear out the old content
		dojo.empty("demoList");
		
		// request the new content
		request = demo.requestData( evt.target.innerHTML ).then(function(){
			request = null;
		});
	});
});

For the animation we make changes in our loadingOverlay object

var loadingOverlay = (function(){
	var overlayNode = null, 	
		hideAnim = null;

	return {
		overlayId: "loadingOverlay",
		init: function(containerNode){
			...
		},
		show: function(){
			// stop any in-progress hide animation
			hideAnim && hideAnim.stop();

			// show the overlay
			dojo.style( overlayNode, {
				display: 'block',
				opacity: 1
			});
		},
		hide: function(){
			if(!hideAnim){
				// handle the case where repeated calls to hide() are made
				// the animation object is stored in a closure variable
				hideAnim = dojo.fadeOut({
					...
				}).play();
			}
		}
	};
})();		

Conclusion

Judicious application of just a few Dojo methods has created a nicer user experience and put you the developer in control of what you load when, and what your user sees in the meantime. We've applied the module pattern with Dojo to contain and manage the solution; selectively exposing methods and properties for use on the page. This recipe's solution is of course just one way to tackle the stated problem, but even with the refinements with the Twitter demo integration, it remains simple, lightweight and readily extended in any direction your requirements take you.

Colophon

Error in the tutorial? Can’t find what you are looking for? Let us know!