Let's consider a fairly trivial, but probably typical, Ajax-based application. I've got a series of buttons:
Each button, when clicked, hits a service on my application server and fetches some data. In my case, just a simple name:
The code for this is rather simple. (And note - for the purposes of this blog entry I'm keeping things very simple and including my JavaScript in the HTML page. Please keep your HTML and JavaScript in different files!)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>
<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>
<div id="resultDiv"></div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$result = $("#resultDiv");
$(".loadButton").on("click", function(e) {
var thisId = $(this).data("prodid");
console.log("going to load product id "+thisId);
$result.text("");
$.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
console.log("back with "+JSON.stringify(res));
$result.text("Product "+res.name);
});
});
});
</script>
</body>
</html>
I assume this makes sense to everyone as it is pretty boiler-plate Ajax with jQuery, but if it doesn't, just chime in below in a comment. Ok, so this works, but we have a small problem. What happens in the user clicks both buttons at nearly the same time? Well, you would probably say the last one wins, right? But are you sure? What if something goes wrong (database gremlin - always blame the database) and the last hit is the first one to return?
What you can see (hopefully - still kinda new at making animated gifs) is that the user clicks the first button, then the second, and sees first the result from the second button and then the first one flashes in.
Now to be fair, you could just blame the user. I'm all for blaming the user. But what are some ways we can prevent this from happening?
One strategy is to disable all the buttons that call this particular Ajax request until the request has completed. Let's look at that version.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>
<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>
<div id="resultDiv"></div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$result = $("#resultDiv");
$(".loadButton").on("click", function(e) {
//disable the rest
$(".loadButton").attr("disabled","disabled");
var thisId = $(this).data("prodid");
console.log("going to load product id "+thisId);
$result.text("Loading info...");
$.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
console.log("back with "+JSON.stringify(res));
$(".loadButton").removeAttr("disabled");
$result.text("Product "+res.name);
});
});
});
</script>
</body>
</html>
I've added a simple call to disable all the buttons based on class. I then simple remove that attribute when the Ajax request is done. Furthermore, I also include some text to let the user know that - yes - something is happening - and maybe you should just calm the heck down and wait for it. The result makes it more obvious that something is happening and actively prevents the user from clicking the other buttons.
Another strategy would be to actually kill the existing Ajax request. This is rather simple. The native XHR object has an abort method that will kill it, and jQuery's Ajax methods returns a wrapped XHR object that gives us access to the same method.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>
<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>
<div id="resultDiv"></div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$result = $("#resultDiv");
var xhr;
var active=false;
$(".loadButton").on("click", function(e) {
var thisId = $(this).data("prodid");
console.log("going to load product id "+thisId);
$result.text("Loading info...");
if(active) { console.log("killing active"); xhr.abort(); }
active=true;
xhr = $.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
console.log("back with "+JSON.stringify(res));
$result.text("Product "+res.name);
active=false;
});
});
});
</script>
</body>
</html>
I use two variables, xhr and active, so that I can track active xhr requests. There are other ways to track the status of the XHR object - for example, via readyState - but a simple flag seemed to work best. Obviously you could do it differently but the main idea ("If active, kill it"), provides an alternative to the first method.
When using this, you can actually see the requests killed in dev tools:
Any comments on this? How are you handling this yourself in your Ajax-based applications?
p.s. As a quick aside, Brian Rinaldi shared with me a cool little UI library that turns buttons themselves into loading indicators: Ladda