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.
(samples taken from Facebook, Amazon & Windows Azure Dashboard)[/caption]
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 element to the container on which it is set when it is initialized. The new
div
:
- Is absolutely positioned (centered) on the parent
- Has a spinner gif as a background image (I recommend the excellent ajaxload to generate something suitable)
- Is hidden by default
This
div
can then be shown/hidden along with the original content when the value of theisLoading
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 offadeOut
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.