joestelmach.com

Ajax Busy Indicators

If someone told me back in 2002 that animated gifs would one day be popular again, I wouldn't have believed them. The image of a construction worker shoveling next to a road-block to indicate a site is 'Under Construction' still brings on a feeling of nausea. However, animated gifs are now proving to be useful and sometimes even necessary in modern day web apps. The difference is that now they have some usability characterstics instead of just looking cute. Since Ajax requests are made behind the scenes, the browser itself doesn't offer any form of visual feedback to indicate that a request has been made and a response is pending. Therefore, we're on our own with letting the user know something is happening and that the application isn't just sitting there being stupid.

So how do we do this? The easiest way is to place an image on the page and set it's display property to none. Then when an ajax request occurs, we simply show the image by setting it's display property to 'inline' or 'block', and hide it again once the request has finished. We can actually forget about the css completely by using the prototype Element methods instead.

Somewhere in your rhtml:
<img src="images/spinner.gif" id="busy_indicator"
  alt="busy" style="display:none" />
<%= link_to_remote("Click Me", :url => {:action => "action_name"},
  :after => "Element.show('busy_indicator');",
  :complete => "Element.hide('busy_indicator');")%>	

I'm using the after callback in this example, which might generate some confusion. The after callback is invoked after the request has been made and the browser is awaiting a response. This differs from the before callback which is invoked before the request is even attempted. I guess the jury is still out on this one, but I feel that the after callback is more appropriate since it will display the image only if the request was actually made. If we use the before callback and the request fails for some reason, then the busy indicator will be sitting there spinning and the user will just sit there and wait. Using the after callback will not display the busy indicator in this situation, and the user will most likely click the link again since they see nothing is happening.

In this simple example, we might also have some style rules associated with the image to place it in a well defined position on the page:

#busy_indicator {
  position:absolute;
  top:300px;
  left:100px;
}

Now, what happens when I have lots of Ajax calls on my page and I'd like the busy indicator to be shown for each one? Using the solution above would require the very un-DRY-like repetition of the after and complete callback code for each link_to_remote, form_remote_tag, etc. Luckily, there's an easy solution to extracting this code into a common place.

The prototype library allows you to register generic callbacks that work across all Ajax requests (those invoked through prototype, ofcourse.) This makes it painless to show our busy indicator at the proper time. Add the following to your application.js file:

Ajax.Responders.register({
  onCreate: function() {
    if($('busy_indicator') && Ajax.activeRequestCount>0)
      Element.show('busy_indicator');
  },
  onComplete: function() {
    if($('busy_indicator') && Ajax.activeRequestCount==0)
      Element.hide('busy_indicator');
    }
});

Note that we are simply just moving our previously defined after and complete callback code. Those callbacks no longer need to be defined in the previous link_to_remote call (or any other Ajaxified link for that matter.) Also note that the before callback described previously is not possible at this level, since onCreate is invoked after the request is created, not before. This code will display our busy indicator as long as there is at least 1 outstanding Ajax request awaiting a response. This is accomplished by looking at the prototype's Ajax.activeRequestCount, which is updated each time an Ajax request is made through prototype (this is actually implemented in the prototype library by registering a callback, just like we're doing here!)

So this is a pretty good solution, but the thing I hate about it is the inability to move the busy indicator around on the page. Sometimes I'd like the indicator to display next to each link or button that I click. However, I don't want to have an image tag sitting next to every single link. That would suck. So what can we do here? One solution would be to still place only a single image tag on the page, but change it's position each time a link is clicked. We may even be able to take this one step further by walking down our dom and generically inserting an onclick handler to position the indicaotor next to each link/button that we press so we don't have to define the onclick handler ourselves each time a link is added. Also, I think it may be benificial to insert the image tag into the dom via Javascript, since the busy indicator is useless outside of an Ajax/Javascript context.

Comments
blog comments powered by Disqus