‘Loading’ Placeholders using Knockout JS

Update: this feature is now available as part of the ko.plus library available on GitHub and NuGet!


Whenever you have an asynchronous call to the server from a web page it is good practice to have some form of loading indicator to let the user know that something is happening in the background.

Examples of loading images from around the web

(samples taken from Facebook, Amazon & Windows Azure Dashboard)

On a recent project I found myself constantly repeating code to hide areas of the UI whilst running an operation in the background and insert a ‘Loading…’ animation, so instead of repeating the same boilerplate I decided to write a re-usable custom binding to get the same effect.

Ideally, all I want to do is to specify a boolean property that indicates when the ‘Loading’ animation should be visible:

<div data-bind="loadingWhen: isLoading"></div>

The Custom Binding

Custom bindings in knockout are made up of two functions – init and update – that are called when the binding is first initialised and when its value is updated respectively.  Each function has access to both the element on which it has been set and the current value of the flag, so we have all the information we need.

The binding works by appending a new <div /> element to the container on which it is set when it is initialized.  The new div:

  1. Is absolutely positioned (centered) on the parent
  2. Has a spinner gif as a background image (I recommend the excellent ajaxload to generate something suitable)
  3. Is hidden by default

This div can then be shown/hidden along with the original content when the value of the isLoading flag changes.

Init

In the initialize step we want the create the ‘loader’ div and insert it into the parent element.  To do that we can use some basic jQuery to instantiate the div and set up the required CSS properties:

ko.bindingHandlers.loadingWhen = {
	init: function (element) {
		var
		//cache a reference to the element as we use it multiple times below
		$element = $(element),
		//get the current value of the css 'position' property
		currentPosition = $element.css("position")
		//create the new div with the 'loader' class and hide it
		$loader = $("<div></div>").addClass("loader").hide();

		//add the loader div to the original element
		$element.append($loader);

		//make sure that we can absolutely position the loader against the original element
		if (currentPosition == "auto" || currentPosition == "static")
			$element.css("position", "relative");

		//center the loader
		$loader.css({
			position: "absolute",
			top: "50%",
			left: "50%",
			"margin-left": -($loader.width() / 2) + "px",
			"margin-top": -($loader.height() / 2) + "px"
		});
	}
}

This is all fairly straightforward, though it is worth noting the setting of the CSS position on the original element.  This is because we need to use absolute positioning on the loader div so that it appears centered over the original content, and absolute positioning always works relative to the first ancestor element that does not have static positioning.  We want the loader position relative to the original element, so we need to set it’s position to make sure it is used.
Relative positioning means relative to the elements original position, so we should be able to safely replace either auto or static with relative without any offsets to have it remain in the original location.
The loader itself is given top and left margins of 50% (meaning 50% of the parent element), and then negative margins of half of the loader’s width and height so that it is properly centered.

Update

Next up we need to show and hide the content based on the value of the flag. For this we will add an update method to the binding that handles the hiding and showing using jQuery.

ko.bindingHandlers.loadingWhen = {
	init: function (element) {
		//unchanged
	},
	update: function (element, valueAccessor) {
		var
		//unwrap the value of the flag using knockout utilities
		isLoading = ko.utils.unwrapObservable(valueAccessor()),

		//get a reference to the parent element
		$element = $(element),

		//get a reference to the loader
		$loader = $element.find("div.loader")

		//get a reference to every *other* element
		$childrenToHide = $element.children(":not(div.loader)");

		//if we are currently loading...
		if (isLoading) {
			//...hide and disable the children...
			$childrenToHide.css("visibility", "hidden").attr("disabled", "disabled");
			//...and show the loader
			$loader.show();
		}
		else {
			//otherwise, fade out the loader
			$loader.fadeOut("fast");
			//and re-display and enable the children
			$childrenToHide.css("visibility", "visible").removeAttr("disabled");
		}
	}
};

Things to note here are:

  • We are using visibility: hidden instead of fadeOut as we don’t want to remove the element from the DOM; we just want to hide it.  If we removed it then the size of the parent element might change, which could cause the loader to jump about
  • We are disabling the original content to prevent it responding to any clicks etc whilst hidden

Done

And that’s all there is to it! Knockout handles the notification of changes to the isLoading flag so we should now be able to drop this into our website.

You can see a working example of this here.

Advertisements

2 thoughts on “‘Loading’ Placeholders using Knockout JS

  1. Stephan Parrot says:

    That’s awesome!
    Thank you for the detailed explanation!
    I’ve made a fiddle using it with an extra touch but for some reasons, it’s not working properly.
    I load a list of images, the loader shows up fine. Then when I click an image, an avatar and some infos gets loaded but the loader doesn’t show up and then, I click on another image and now it shows as expected.
    See it here: http://fiddle.jshell.net/t3Kqc/7/

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s