Over the weekend I was fiddling was some code to see how to make a generic activity indicator for Ajax requests. I’m sure most of you have seen these but if not, here’s one similar to what Facebook uses:
While it’s may not look like much, that animated .gif is pretty important because it tells a user that something is happening and that results will appear shortly. The last thing you want is for a user to think the page has locked up or broken and using this simple method advises them that their request is being processed.
Targeted Indicators
In my case, I was looking for a way to set an indicator within the specific DOM element that would be affected by the XHR call. So if my XHR call was to set the contents of a specific DIV, I wanted the indicator to render within that specific DIV only (similar to what Facebook does).
First, I created three DIVs and 3 anchor tags:
[code lang=”html”]
<a href="#">Box 1</a>|<a href="#">Box 2</a>|<a href="#">Box 3</a>
<br /><br />
<div id="box1"> </div>
<div id="box2"> </div>
<div id="box3"> </div>
[/code]
and I set the DIVs to a specific width and floated them left:
[code lang=”css”]
#box1, #box2, #box3 { background-color:#E5EECC; border:1px solid #D4D4D4; width: 200px; float:left; };
[/code]
Next, I created a new method called fillBox() that would take the target element’s ID and use that to dynamically create a new IMG tag and insert the activity indicator. What I decided to do was leverage jQuery’s global Ajax events to create the generic handlers for this. Using ajaxStart(), which fires off when an Ajax request is initiated, I dynamically create the tag for my activity indicator and inset it using the prepend() method:
[code lang=”js”]
$(ele).ajaxStart( function() {
$(this).prepend( "<img id=’throbber’ src=’facebook.gif’ />" );
})
[/code]
Then using the Ajax global event handler ajaxStop(), I would easily delete the indicator once the Ajax request was done by using the remove() method to delete the IMG tag from the DOM:
[code lang=”js”]
ajaxStop( function() {
$("#throbber").remove();
});
[/code]
Lastly, using some event delegation, I created an event handler for the clicks on the anchor tags:
[code lang=”js”]
$( "a" ).click( function(e) {
var el = ”;
switch(e.target.text) {
case "Box 1":
el = ‘#box1’;
break;
case "Box 2":
el = ‘#box2’;
break;
case "Box 3":
el = ‘#box3’;
break;
}
fillBox( el );
});
[/code]
The handler simply looked for the anchor tag’s text and based on that filled a local var with the ID that would be targeted for the indicator. It then passed that ID to the fillBox() method.
A Little Roadblock
I thought I was done and the code look pretty straightforward. When I ran this, though, I experienced some odd behavior:
When I would click on the “Box 1” link, the indicator would correctly appear in the first DIV followed by the data populating the box. When I would next click on the “Box 3” link, though, both the first box AND the third box would have the indicators displayed! Not good.
I looked into it and got feedback from Adam Sontag who provided the answer I needed. I had incorrectly assumed that ajaxStop() would unbind the jQuery global Ajax events from the attached element. Unfortunately it doesn’t so by using the unbind() method, I was able to resolve the issue.
[code lang=”js”]
$.ajax({
url: ‘test.php’,
cache: false,
success: function(data) {
$(ele).html( data ).unbind( ‘ajaxStart’ ).unbind( ‘ajaxStop’ );
}
});
};
[/code]
The Solution Gets Easier
Then I got more feedback from Karl Swedberg which really streamlined the whole thing. He recommended forgoing the Ajax global events altogether and simply inserting the indicator via my fillBox() method directly and since I was replacing the contents of the DIVs with the html() method, there was no need to use remove() since the IMG tag was already being removed.
[code lang=”js”]
var fillBox = function( ele ) {
$(ele).prepend( "<img id=’throbber’ src=’facebook.gif’ />" );
$.ajax({
url: ‘test.php’,
cache: false,
success: function(data) {
$(ele).html( data );
}
});
};
[/code]
So while I initially was looking to use jQuery’s built-in global Ajax event handlers for making this generic, I’m finding Karl’s solution much cleaner. It requires less event handlers and method calls to do the same task. Love it!
Here’s the end result and you can see the demo here:
[code lang=”html”]
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<title>Throbber Test</title>
<style>
#box1, #box2, #box3 { background-color:#E5EECC; border:1px solid #D4D4D4; width: 200px; float:left; };
</style>
<script src="http://code.jquery.com/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
fillBox = function( ele ) {
$(ele).prepend( "<img id=’throbber’ src=’facebook.gif’ />" );
$.ajax({
url: ‘test.php’,
cache: false,
success: function(data) {
$(ele).html( data );
}
});
};
$(document).ready( function() {
$( "a" ).click( function(e) {
var el = ”;
switch(e.target.text) {
case "Box 1":
el = ‘#box1’;
break;
case "Box 2":
el = ‘#box2’;
break;
case "Box 3":
el = ‘#box3’;
break;
}
fillBox( el );
});
});
</script>
</head>
<body>
<a href="#">Box 1</a>|<a href="#">Box 2</a>|<a href="#">Box 3</a>
<br /><br />
<div id="box1"> </div>
<div id="box2"> </div>
<div id="box3"> </div>
</body>
</html>
[/code]
I still think, IMHO, the global Ajax events are better for this purpose. By better, I mean I think the code is more semantic.
Instead of inserting the element into the DOM each time, I just toggle the display:xxx visibility of the throbber from “none” to “block” and back. Make it visible in the ajaxStart event, make it invisible in the ajaxStop event. That would have prevented you from having the throbber added over and over again.
I bind the ajaxStart and ajaxEnd events to the document so it listens for all Ajax events.
@Kyle: You make a good point. Typically, I’d do the same sort of thing. One gotcha with the display-toggling technique, though, is that it wouldn’t position the activity indicator in the right location, according to Rey’s needs. So, at the very least, it would require an additional .append() call.
@Karl — I typically design the UX of a site to have one global indicator for all activity, because I think it’s poorer design to have a dozen different throbbers all over the place. But that’s just design preference.
However, ajaxStart() and ajaxEnd() are designed to only be run sort of globally when the state changes from “no ajax requests currently” to “some ajax requests currently” and vice versa. This is not a good event mapping (functionally or semantically) for having multiple throbber listeners.
You can still use the “global” Ajax events and have multiple throbbers, though. I’d suggest ajaxSend/ajaxComplete instead. You’d bind both events to each container that needed to “listen” to when an Ajax request was fired/completed for his particular content.
So, like: $(“#container1, #container2, #container3”).bind(“ajaxSend”,start_ajax).bind(“ajaxComplete”,end_ajax);
Then, when making an $.ajax() call, you could specify a custom property of the ajaxOptions object like “target” or something like that, and give a selector or DOM object reference (probably selector is better for avoiding memory leaks).
$.ajax({ … , target:”#container1″ });
$.ajax({ … , target:”#container2″ });
Then, all that needs to happen is something like this:
[code]
function start_ajax(e,xhr,opts) {
var $this = $(this);
if ($this.is(opts.target)) {
$this.find(".throbber").show();
}
}
function end_ajax(e,xhr,opts) {
var $this = $(this);
if ($this.is(opts.target)) {
$this.find(".throbber").hide();
}
}
[/code]
Rey, here’s a “pluginized” version of your code that can be called like $(‘#foo’).fillbox() or $(‘#bar’).fillbox({ url: ‘foo.php’ }). I’ve found that, for element-specific code, it’s not a lot of extra effort to take the typical “pass the element as an argument” version and pluginize it so that it operates on a jQuery collection of elements, like built-in jQuery methods. Also, note how my version has default options that can be changed globally, as well as overridden on a per-use basis.
http://gist.github.com/465622
Wow Ben! Thanks man. That is great! :)
Ben’s plugin version is awesome and helpful. Two little nuances/nitpicks:
1. I’m not in favor of jquery plugins altering markup every time an Ajax call is made. Not only is this more “expensive” DOM performance wise, it can lead to side effects down the road as surrounding markup and CSS change. I’d still be more in favor of the approach that the throbber is always/originally in the markup, hidden by default, and show()/hide() controlled during the Ajax request/response cycle.
2. The approach of using a per-request callback (“success”, “complete”, etc) to do the adding and removing of the spiner could be troublesome if you decide to override the default $.fillbox.options.complete callback, for instance. If you don’t take care when you override the callback with your own custom function, then the throbber will stop being hidden:
[code]
$("#foo").fillbox({complete:function(){ alert("I’m done!"); }}); // throbber stays visible… :(
[/code]
I think this plugin refactored to use the ajaxSend/ajaxComplete “global” event handlers, proxied (as Ben did) to keep the scope, would be a cleaner and more robust approach.
Thank you, very nice :)
Hi guys,
The code work but not with IE 8 (tested with Chrome, Firefox and Safari).
Do you know why?
Thanks