Ask Ben: Optimizing Form Inputs For Numeric Keypad Usage
I have a question that has me stumped. I am building several data entry forms and our users want a carriage return to move the cursor to the next field. This is for speed - they are using their right hand for number entry and the enter key is easily available - their left hand is holding or keeping place on paper so reaching across for the tab key is 'inconvenient'. Also some fields are populated with a hand scanner which includes something (line feed or carriage return) after the scan - in some forms they are using, the cursor jumps to the next field after the scan. How do I do this? I have goggled until I am blind and can't find any help on this issue. This can't be the only time in the history of the world that someone has wanted this.
You know what I like about this problem - it takes what is the natural function of a FORM element and completely breaks it; but, it does it in such a way that makes the form ultimately more usable for the target audience. So often in web development, we build forms that are designed to work in a traditional way with very little thought about the people who will actually be using them after the product is launched. So, before we get into this, thank you so much for really aligning your goals with target audience - it makes me proud.
Ok, that said, the best part of this solution is also the hardest part to deal with; by default, hitting the Enter key while focused in a form field will cause the form to submit. We need to remove that functionality, but not fully. This wasn't really dicussed in the question, but I am going to make the decision that if we are on the last input, then we want the submit to actually work. So, if we are currently typing in field one and the we hit enter, we want to "tab" to field two. However, if we are currently typing in field "N" and we hit enter, we want to submit the form (rather than looping back to the first field).
To make this process easier, I am requiring two things. First, let's use jQuery; there's simply no reason to ever use complex Javascript without using jQuery. Secondly, to remove hard-coding and to make this as flexible as possible, we need to give our target inputs a special CSS class for the jQuery to hook into. That being said, let's take a look at the code:
<!--- Check to see if form was submitted. --->
<cfif StructKeyExists( FORM, "submitted" )>
<!--- Dump out form for debugging. --->
<cfdump var="#FORM#" />
<cfabort />
</cfif>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Form Tabbing Demo</title>
<!-- Linked files. -->
<script type="text/javascript" src="jquery-1.2.6.min.js"></script>
<script type="text/javascript">
// When the DOM loads, we want to initialized the form.
$( InitForm );
// Initialize the FORM.
function InitForm(){
// Get a reference to the first form in our document.
var jForm = $( "form:first" );
// Get all the form elements in the form that we have
// marked to use our "tabbing". This will be all the
// input fields with the class, "input-field".
var jInput = jForm.find( "input.input-field" );
// For each input, we want to trap the key down
// event to check to see if we need to be tabbing or
// allowing a form submission.
jInput.each(
function( intI ){
// Get this input.
var jThis = $( this );
// Hook up the keydown event to be handled by
// our event handler. Pass through needed
// variable references.
jThis.keydown(
function( objEvent ){
return(
InputKeyDownHandler( objEvent, jThis, jInput )
);
}
);
}
);
}
// This will handle the key down event for each input
// handler in our jquery collection.
function InputKeyDownHandler( objEvent, jInput, jInputs ){
// Check to see if this key was the return key (13).
// If not, just return out normally.
if (objEvent.which != "13"){
return( true );
}
// If we are this far, then we know that we are
// dealing with the return key (13).
// Get the index of the input we are currently in.
var intI = jInputs.index( jInput );
// Now that we have the current index, we have a
// decision to make. If this is the last input in
// our list of inputs, then we simply want to allow
// the submit to happen. If this is NOT the last
// input, then we want to "tab" to the next input
// field.
if (intI == (jInputs.length - 1)){
// We have reached the end of the tabbing.
// Return true and let the form submit.
return( true );
} else {
// We have not reached the end of the tabbing.
// Focus the next input.
jInputs.get( intI + 1).focus();
// Return false to prevent form submission.
return( false );
}
}
</script>
</head>
<body>
<cfoutput>
<h1>
Form Tabbing Demo
</h1>
<form
action="#CGI.script_name#"
method="post">
<!--- Flag submission. --->
<input type="hidden" name="submitted" value="1" />
<p>
Field One:<br />
<input
type="text"
name="f1"
size="10"
maxlength="10"
class="input-field"
/>
</p>
<p>
Field Two:<br />
<input
type="text"
name="f2"
size="10"
maxlength="10"
class="input-field"
/>
</p>
<p>
Field Three:<br />
<input
type="text"
name="f3"
size="10"
maxlength="10"
class="input-field"
/>
</p>
<p>
<input type="submit" value="Submit" />
</p>
</form>
</cfoutput>
</body>
</html>
There's actually a lot less code here than appears. The commenting and explanation make it bulky, but in reality, this is like 10 lines of code. When the DOM loads, jQuery is finding all the fields that have the class "input-field". It then binds a key down event listener to each of those fields which will monitor the keys presses looking for the enter key (key code 13).
I hope this points you in the right direction.
Want to use code from this post? Check out the license.
Reader Comments
@Ben:
That still seems a bit verbose. You should be able to do the same thing with:
$(document).ready(function (){
var $fields = $("form:first input.input-field").keypress(function (e){
// if the user pressed [ENTER]
if( (e.keyCode || e.charCode) == 13 ){
// get the current array position
var pos = $fields.index(e.target);
// if we're less than the last element, go to the next element and place focus
if( pos < $fields.length-1 ) $fields.eq(pos).next().focus();
else $fields.eq(0).focus();
// prevent the default action
e.preventDefault();
return false;
}
return true;
});
});
(I didn't test this, but it should work or at least be very close.)
Ben...
A few comments. Why bother putting a class on the form fields? Why not just get all children of the FORM tag:
var jInput = jForm.find("input, select, textarea");
Then you could conceivable use this code for existing forms without recoding.
$( InitForm );?
Never seen that sort of construct. Works though...had to test it. :)
So as far as the default behaviour goes, that of hitting the tab button to advance to the next field, you're not changing that right?
One last thing, this might be a little expensive to fire off a function with every single keystroke, which is what it appears you're doing. It might be more efficient, if inside the jThis.keydown( block, you first test to see if the return key has been pressed before firing the function. Might save a few cycles right?
All in all, it's a cool idea, and well written code. Good job.
Also, you could condense this line from my post:
var jInput = jForm.find("input, select, textarea");
into this line:
var jInput = $("input, select, textarea",jForm);
That looks for input, select, and textarea in the context of jForm.
@Dan,
I think our logic is more or less doing the same thing. I just break it out a bit more. My concern when demoing jQuery is that I make is so concise that it loses its readability. As such, I tend to err on the verbose side. But, I think your code is fairly straightforward. I am still trying to find that sweet-spot in explanation vs. brevity.
@Andy,
Yeah, I too had to test the $( InitForm ) thing to see if it would work. Normally, I wouldn't do that, but on my blog, I aim to never have more than 66 characters of code per line (to prevent wrapping). One of the downsides (maybe the only one) of jQuery is that when properly tabbed, it gets really wide really fast. So, sometimes, I break things into separate functions on my blog just to make the code more "left-aligned".
As far as the fields having classes, I just liked that so that I didn't include any unintended fields in the jQuery stack. I felt that was easier to do that adding logic to exclude things like hidden fields and submit buttons.
@Ben:
Part of the problem with being verbose, you often end up adding in extra processing or showing steps that are unnecessary.
For example, when you use the each() and the keypress() together provides more overhead, plus if someone is using this as a complete learning tutorial one might deduce that you have to do it that way--when in fact the keypress() will work on every element in the jQuery array.
I agree the syntax can sometimes look overwhelming, but that's why you just use lots of comments. :)
@Andy:
You can't use keydown because several browsers only support the keypress event for the [ENTER] key. The keydown event will never be fired for the [ENTER] key in IE and Safari.
@Dan,
This is true; although "unnecessary" can be an opinion. For example, I would rather get the jQuery stack of elements first, rather than have that stack be returned from the .keypress() binding. It's not necessary, but I think creating the stack before you use it is a more readable separation of concerns.... but again, stuff like that is all subjective.
All said, your code is definitely cleaner than mine.
If you haven't tried it, I highly recommend checking out the jQuery Hotkeys plugin:
http://code.google.com/p/js-hotkeys/
This developer has done a lot of the legwork in capturing keyboard events across browsers. The live demo is quite impressive.
The "unnecessary" comment was more in regards to the each/keypress logic. Run the bind method (which is what keypress does) already runs "each" on the jQuery collection. So often you end up hurting performance by breaking things out.
While I prefer code maintainability and legibility over going for poor performance, when it comes to JavaScript and DOM manipulation, that's when I'll let performance rule my code.
There are just so many uncontrolled variables in play when executing JavaScript on a user's browser, that I want to make sure my code is efficient as possible (within reason, as any general library like jQuery has overhead that may not always be necessary.) However, when I know I can reduce loop or additional conditional processing, I'm going to do it.
In the end, there's really no wrong or right--just sharing my insight into the issue.
@Dave,
That hotkeys things looks awesome. I just took a look at the live demo - very cool.
@Dan,
I agree. Also, if I can make a small excuse, I first had the "each" method to use the index variable, but then after I started using the index() method, I didn't refactor well.
you wouldn't have to check for last if the selector had excluded it:
var $fields = $('input.input-field:not(:last)');
then all you need is:
$fields.keypress( function (e) {
if( (e.which) == 13 ) {
$fields[ $fields.index(e.target) + 1 ].focus();
e.preventDefault();
return false;
}
return true;
});
@DB,
Brilliant! That would have made the code MUCH more simple.
@DB,
Your code results in an error when you try to "tab" (with the enter key) from the next-to-last input to the last. You still need to check to see if you're at the end of the list.
Also, you don't need to fire e.preventDefault(). You only need to return false, and jQuery handles the rest.
@Ben,
I've written thousands of lines of jQuery code, and I didn't know about the index() function until your post. Thanks!
@David,
This is the first time that I have ever used it :)
@David:
I'm pretty sure the e.preventDefault() is needed for IE6. I know I've run into problems with not included that inside form elements where the [ENTER] key can submit the form. The return false; prevents the keypress event in the field itself, but doesn't necessarily catch some of the bubbled events.
@Dan,
Interesting - I've not encountered that issue. And I definitely didn't encounter that issue when testing this code on IE6. Could this be an issue that was only addressed in a recent build of jQuery?
One other thing: putting "e.which == 13" without commenting what 13 stands for is just asking for pain and heartache down the road. An alternate approach is self-documenting code like this:
ENTER_KEY = 13;
if (e.which == ENTER_KEY)
I was just working on something similar today.
Good example. I am just getting my head around the JQuery stuff and things like this are always helpful :)
@Jeff,
Prepare yourself to never want to program without jQuery again :)
RE: OP said "Also some fields are populated with a hand scanner which includes something (line feed or carriage return) after the scan - in some forms they are using, the cursor jumps to the next field after the scan. How do I do this?"
check the scanners documentation
(unfortunately how to program every scanner is different, but it usually comes with a cd or manual containing barcodes to direct the actions. Most of those manuals are available online, and depending on the age of your scanner shouldn't be too hard to find)
there are generally pre and post scan characters that can be set. in most of mine, i set the horizontal tab as the post scan, then if it tabs to the submit button I deal with that. If you do change what it does, don't forget to test the apps outside of yours that the scanner works with.
ok, i had to fix this - you know how irritating it is to copy some code and have it fail.
var $allfields = $('input.input-field');
var $tabfields = $('input.input-field:not(:last)');
$tabfields.keypress( function (e) {
if( (e.which) == 13 ) {
$allfields[ $allfields.index(e.target) + 1 ].focus();
return false;
}
return true;
});
@DB,
Nice update. Looks solid to me.
I always know where to come to find the simple answers to the dumb questions I sometimes have. Thanks so much for all you do. You help us lesser learned become better learned.
KeeKee