Spicing Up Your UX With Javascript
Recently (and as others have pointed out) I noticed gmail has a nice feature that allows you to select multiple emails by clicking on one checkbox, then shift+clicking another to select all of the messages in between. This is a very useful UI feature that is present on thousands of desktop applications, which provides users with a natural behavior of being able to select multiple elements in some list with ease.
So why not give the users of your latest application website the same ease of use? What follows is my quick and dirty way of accomplishing it. Granted, my js skills are still in “de-rusting” mode, but after a couple hours I was able to crank out something pretty useful that mimics the same behavior. So let’s dive in!!
Getting Started
To get started, we need an html document with the most basic of elements. All we really want for this tutorial is a list of checkboxes that we can group together. So, we start with an empty document:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>Pizza Order Form</title> </head> <body> </body> </html>
Now we need to add some checkboxes. So, moving with our “Pizza Order Form” theme, we create a fieldset with some checkboxes for toppings. Normally this would be within a form, but for the moment we’re not really making a form to submit anywhere.
<fieldset> <legend>Number of Toppings</legend> <label> <input type="checkbox" name="topping" value="Sausage"> Sausage </label> <br> <label> <input type="checkbox" name="topping" value="Onions"> Onions </label> <br> </fieldset>
I’m sure you see where this is going. To keep things short, I’ll only show two for now.. however I add several more that we’ll see in the finished product. You may also notice that I put label tags around each of the inputs and it’s label. This is another handy usability feature… clicking on the label will also toggle the checkbox.
Now with the html out of the way, the next thing we need to do is work with the javascript. Before we get started I really know we’ll be forced to deal with event handling, and although there are probably nice small libraries made solely for x-browser event handling, I’m going to reach to the closest library on my laptop, jQuery. So without further adieu, I go ahead and add two script elements to the head, one for jQuery and one for our yet to be coded script:
<head> <title>Pizza Order Form</title> <script type="text/javascript" src="js/jquery-1.2.1.pack.js"></script> <script type="text/javascript" src="js/checkboxMagic.js"></script> </head>
Testing Out Event Handling
To start things off, I like to always start with a very small script to test I have jQuery setup right and see if I remember how to use javascript’s closures as well as jQuery’s event handling mechanism. So, lets just bind the onclick event to a handler that prints out the checkbox’s value and have it apply to all the checkboxes that have the same name (for now we’ll just specify the name in the onload handler):
function checkBoxGroup(name){
var checkboxes = $('input[name="'+name+'"]');
var doOnClick = function(event){
alert(this.value);
};
checkboxes.click(doOnClick);
}
$(document).ready(function(){
checkBoxGroup('topping');
});
the first line of checkBoxGroup is simple… it uses jQuery and a CSS selector to select all input elements that have the specified name. We also create a locally scoped function that takes an event and alerts this.value (jQuery will set this to point to the element that fired the event… in the case, the checkbox that you click). To top things off, we bind this function to handle all click events for checkboxes. Finally, we call the function to perform the bindings inside of $(document).ready, which is jQuery’s way of indicating when the document is considered completely loaded.
Whew… need a break yet?
So we save everything, and crack open the file in a web browser. Clicking on each checkbox now alerts the value of that checkbox… just as we expected.
Hold on a Second!
Now it’s time to get our hands dirty, and I feel almost bad for writing this now after I finished the code… I refactored it quite a bit and although it is now easier on the eyes and much cleaner, there was a bit of a learning experience involved. So I’d like to recap the requirements before we continue, as it’ll help us to decide how to approach the problem.
As per gmail’s behavior, it all starts when a user clicks on some random checkbox. From there, whenever they hold the shift key and click on another checkbox, both below or above, all the checkboxes are checked in between. If they have selected some checkboxes already, and shift click another box either above or below the existing selection, the boxes inbetween are also checked. Finally, holding shift and clicking the original checkbox unchecks all of them.
So for the first requirement, it seems we simply need a way of “holding on” to the first checkbox so we can reference it later.
var startCheckbox = null
var doOnClick = function(event){
if(!startCheckbox){
startCheckbox = this;
}
};
Not much… so let’s add some behavior for when the shift key is held. Thankfully, jQuery normalizes the event before giving it to you, so to check if the shift key is pressed all we need to do is check event.shiftKey, which is a boolean that will be set to true if the shift key was held when the event fired.
var doOnCheck = function(event){
var currentCheckbox = this;
if(!startCheckbox){
startCheckbox = currentCheckbox;
}else if(event.shiftKey){
// code to handle checking the checkboxes in between will be here
}
};
Let’s build a method checking all the boxes between the startCheckbox and the one selected holding the shift key (if it’s not the start one!). Unfortunately, there’s a few things we can see we’ll need to continue.
Firstly, we need to be able to know what the indexes are of the start and end points in the checkbox group, which sadly Array.indexOf is unreliable in IE. So we can simply use jQuery’s index method. We’ll pass in an HTMLInputElement and get it’s index in the checkbox collection. I like to minimize my object lookups in js, so I wrap it in a local function:
var index = function(checkbox){
return checkboxes.index(checkbox);
};
We also need to know of a way to check if the currently checked checkbox is before or after the original checkbox. This is made dead simple thanks to our new index method. We create a new method that takes two checkboxes (the currentCheckbox and the startingCheckbox) and tells us if the current one is after it:
var selectionAfter = function(currentCheckBox, startingCheckbox){
return index(currentCheckBox) > index(startingCheckbox);
};
Sweet. Now, given a start and end index, we need a method that will loop over all the checkboxes and check each one. Ready for something scary?
var checkBoxesBetween = function(start, end) {
for (var i = start, current; i < checkboxes.length && (current = checkboxes[i]); ++i){
current.checked = (i <= end);
}
};
Boo! Indeed… probably not a sight for the faint of heart, but made with special care. We start by defining the initializer. i is set to the same value of the start checkbox’s index, and we define another variable current (which will be used to minimize object lookups). the control expression is multi-purpose… we continue until i is equal to checkboxes.length AND we assign current to equal checkboxes[i]. If we haven’t passed the end index, we’ll want to check the checkbox. Otherwise, we want to uncheck it. To accomplish this, we set the current.checked property’s value based on the test of whether the current index is greater than or equal to the end index.
current.checked = (i <= end);
So, tying this all together, we’ll add some code to our else if statement for the click handler… if the selection is after the start checkbox, we want to check all the checkboxes between the startCheckbox and the currentCheckbox. This is what our click handler looks like now:
var doOnCheck = function(event){
var currentCheckbox = this;
if(!startCheckbox){
startCheckbox = currentCheckbox;
}else if(event.shiftKey){
if(selectionAfter(this, startCheckbox)){
checkBoxesBetween(index(startCheckbox), index(currentCheckbox));
} else{
// we'll handle this later!
}
}
};
Okay… now lets look at what we have so far in our checkboxMagic.js file:
function checkBoxGroup(name){
var checkboxes = $("input[name='"+name+"']");
var startCheckbox = null
var index = function(checkbox){
return checkboxes.index(checkbox);
};
var selectionAfter = function(currentCheckBox, startingCheckbox){
return index(currentCheckBox) > index(startingCheckbox);
};
var checkBoxesBetween = function(start, end) {
for (var i = start, current; i < checkboxes.length && (current = checkboxes[i]); ++i){
current.checked = (i <= end);
}
};
var doOnCheck = function(event){
var currentCheckbox = this;
if(!startCheckbox){
startCheckbox = currentCheckbox;
}else if(event.shiftKey){
if(selectionAfter(this, startCheckbox)){
checkBoxesBetween(index(startCheckbox), index(currentCheckbox));
} else{
// we'll handle this later!
}
}
};
checkboxes.click(doOnCheck);
}
$(document).ready(function(){
checkBoxGroup('topping');
});
Now’s a good time to save and play with the result. So far, good… if you click on a checkbox and shift click a checkbox after it, it selects all in between. Also, if you hold shift and click another checkbox above the one you just checked, it uncheckes the ones after it. However… we are still missing two more pieces of behavior… if we hold shift and click above the first box we checked, we should check the checkboxes between those too. Likewise, shift clicking the first box we checked should uncheck everything. Let’s take a break and add these last two behaviors.
The Final Behaviors
Actually… this might be easier than we thought… we could probably just use our checkboxesBetween method and change the order of the arguments.
if(selectionAfter(this, startCheckbox)){
checkBoxesBetween(index(startCheckbox), index(currentCheckbox));
} else{
checkBoxesBetween(index(currentCheckbox), index(startCheckbox));
}
Does it work? UGH!! Kind of…. if we shift click below the checkbox, everything is good… if we shift click above it… well, it looks like it DID do the right thing almost. It checked the checkboxes inbetween the first we clicked and the one we clicked above it, but it unchecked the checboxes below the first one. Apparently we need someway to track the last checkbox, and we’ll really just check between the new checkbox and the last checkbox checked. We create a new variable near startCheckbox called endCheckbox and assign it the value of the currentCheckbox (it was clicked while holding shift that is).
We’ll also create a new if statement to first check if endCheckbox has been set. If so, we’ll use that for the end index. Otherwise, we’ll do what we did before… just swap the start and end values. Here we go:
var doOnCheck = function(event){
var currentCheckbox = this;
if(!startCheckbox){
startCheckbox = currentCheckbox;
}else if(event.shiftKey){
if(selectionAfter(this, startCheckbox)){
checkBoxesBetween(index(startCheckbox), index(currentCheckbox));
} else{
if(endCheckbox){
checkBoxesBetween(index(currentCheckbox), index(endCheckbox));
}else{
checkBoxesBetween(index(currentCheckbox), index(startCheckbox));
}
}
endCheckbox = currentCheckbox;
}
};
Works like a charm. Whew… almost done. Now all I need to do is add our final feature, we want to uncheck everything if the user shift+clicks the original checkbox they checked. We’ll need to uncheck ALL checkboxes belonging to this group. We’ll also want to reset the values for both the startCheckbox and the endCheckbox for use next time we want to select a range of checkboxes. This leaves us with a method looking a little like this:
var resetCheckboxes = function(){
for(var i = 0; i < checkboxes.length; i++){
checkboxes[i].checked = false;
}
endCheckbox = null;
startCheckbox = null;
}
Finally, we just check if the currentCheckbox is equal to the startCheckbox (when shift click is used) and if so, call resetCheckboxes and return. This leaves us with the following as our complete javascript file contents:
function checkBoxGroup(name){
var checkboxes = $("input[name='"+name+"']");
var startCheckbox = null
var endCheckbox = null;
var index = function(checkbox){
return checkboxes.index(checkbox);
};
var selectionAfter = function(currentCheckBox, startingCheckbox){
return index(currentCheckBox) > index(startingCheckbox);
};
var checkBoxesBetween = function(start, end) {
for (var i = start, current; i < checkboxes.length && (current = checkboxes[i]); ++i){
current.checked = (i <= end);
}
};
var resetCheckboxes = function(){
for(var i = 0; i < checkboxes.length; i++){
checkboxes[i].checked = false;
}
endCheckbox = null;
startCheckbox = null;
}
var doOnCheck = function(event){
var currentCheckbox = this;
if(!startCheckbox){
startCheckbox = currentCheckbox;
}
else if(event.shiftKey){
if(currentCheckbox == startCheckbox){
resetCheckboxes();
return;
}
if(selectionAfter(this, startCheckbox)){
checkBoxesBetween(index(startCheckbox), index(currentCheckbox));
} else{
if(endCheckbox){
checkBoxesBetween(index(currentCheckbox), index(endCheckbox));
}else{
checkBoxesBetween(index(currentCheckbox), index(startCheckbox));
}
}
endCheckbox = currentCheckbox;
}
};
checkboxes.click(doOnCheck);
}
$(document).ready(function(){
checkBoxGroup('topping');
});
And with that, we wrap things up. You can see the demo of the finished result here. Of course, there are some pitfalls I suppose. For starters, the implementation is not perfect and it’s definitely possible to break it by going click and shift click crazy. Also, the click handler feels very procedural and could be improved upon. But hopefully this has helped illustrate both how to accomplish nice interface behaviors as well as how completely unconventional javascript can be (I think it look beautiful though, but that’s my opinion).
Well, sound off and let me know what you think! This has been my first real tutorial. ![]()
If you're new here, you may want to subscribe to my RSS feed. Thanks for visiting!








February 6th, 2008 at 3:20 pm
Dude, ur code is full of smells!!!!!