Whether you’re trying to learn more about JavaScript and jQuery or you just want to code the most direct solution for your immediate problem, sometimes writing code from scratch is the best approach. This chapter aims to provide you with simple, generic solutions that will help you get started writing your own code.
It is important to note that while there are great benefits to starting from scratch, some of the more common problems you’ll encounter, such as remaining character count, autoresizing textareas, and form validation (just to name a few), have already been addressed by others. Please see Chapter 11 or visit the jQuery forums and blogs online for more information on how community-critiqued and community-tested plugins can help you. Sometimes it helps to look at someone else’s code to find out what you could do better with yours.
When necessary, I provide some sample HTML under the “Problem” heading of each recipe. This isn’t a philosophical statement—there isn’t anything wrong or problematic with naked HTML. I am, however, trying to reinforce the mind-set that JavaScript should be used for enhancing existing HTML and improving user interaction. JavaScript is, and should be considered, completely separate from your HTML.
In the following recipes I am only showing XHTML code snippets relevant to the problem. Please make sure that your code is a complete XHTML document and that it passes validation.
Also, I am only including $(document).ready(function(){...})
in the
recipes where that is part of the discussion. All other solutions
assume you will place the JavaScript in the correct location for your
code structure—either in the .ready()
handler or at the bottom of your
file after the XHTML code in question. Please see Chapter 1 for more
information.
You have a login form on your home page, and you’d like the username text input to be focused on page load.
Use the jQuery $(selector).focus()
method:
// when the HTML DOM is ready $(document).ready(function(){ // focus the <input id="username" type="text" ...> $('#username').focus(); });
Using $(document).ready()
should be fast enough. However, in situations like
retrieving a huge HTML file over a slow connection, the cursor might
focus later than desired—the
user could have already entered her username and could be in the
process of typing her password when $(document).ready()
executes and puts her
cursor back in the username text input. How annoying! In this case you
could use inline JavaScript after the <input>
tag to make focus
immediate:
<input name="username" id="username" type="text" /> <script type="text/javascript"> $('#username').focus(); </script>
Or, if you prefer to keep your code together in the $(document).ready()
block, you can check to
see whether the text input has any text in it before giving it
focus:
// when the HTML DOM is ready $(document).ready(function(){ var $inputTxt = $('#username'), if( $inputTxt.val() == '' ) { // focus the username input by default $inputTxt.focus(); } });
What will happen when JavaScript is disabled? The user will have to manually click into the text input to start typing.
Your order form has fields for both shipping and billing contact information. You’ve decided to be nice and supply the user with a checkbox that indicates the user’s shipping information and billing information are the same. When checked, the billing text fields should be disabled:
<fieldset id="shippingInfo"> <legend>Shipping Address</legend> <label for="shipName">Name</label> <input name="shipName" id="shipName" type="text" /> <label for="shipAddress">Address</label> <input name="shipAddress" id="shipAddress" type="text" /> </fieldset> <fieldset id="billingInfo"> <legend>Billing Address</legend> <label for="sameAsShipping">Same as Shipping</label> <input name="sameAsShipping" id="sameAsShipping" type="checkbox" value="sameAsShipping" /> <label for="billName">Name</label> <input name="billName" id="billName" type="text" /> <label for="billAddress">Address</label> <input name="billAddress" id="billAddress" type="text" /> </fieldset>
If all you want to do is disable the billing fields, it’s as
simple as using the jQuery .attr()
and .removeAttr()
methods when the change
event is triggered:
// find the "sameAsShipping" checkbox and listen for the change event $('#sameAsShipping').change(function(){ if( this.checked ){ // find all text inputs inside billingInfo and disable them $('#billingInfo input:text').attr('disabled','disabled'), } else { // find all text inputs inside billingInfo and enable them $('#billingInfo input:text').removeAttr('disabled'), } }).trigger('change'), // close change() then trigger it once
While selecting a checkbox and disabling the form fields might be enough to get the point across to the user, you could go the extra mile and prepopulate the billing text fields with data from shipping information.
The first part of this solution is the same in structure as the
solution shown previously. However, in addition to disabling the
billing fields, we are also prepopulating them with data from the
shipping fields. The following code assumes the shipping and billing
<fieldset>
elements contain
the same number of text inputs and that they are in the same
order:
// find the "sameAsShipping" checkbox and listen for the change event $('#sameAsShipping').change(function(){ if( this.checked ){ // find all text inputs inside billingInfo, disable them, then cycle through each one $('#billingInfo input:text').attr('disabled', 'disabled').each(function(i){ // find the shipping input that corresponds to this billing input var valueFromShippingInput = $('#shippingInfo input:text:eq('+i+')').val(); // set the billing value with the shipping text value $(this).val( valueFromShippingInput ); }); // close each() } else { // find all text inputs inside billingInfo and enable them $('#billingInfo input:text').removeAttr('disabled'); } }).trigger('change'), // close change() then trigger it
The second part of this solution updates the billing fields automatically when the user enters information into the shipping fields, but only if the billing fields are otherwise disabled:
// find the shippingInfo text inputs and listen for the keyup and change event $('#shippingInfo input:text').bind('keyup change',function(){ // if "sameAsShipping" checkbox is checked if ( $('#sameAsShipping:checked').length ){ // find out what text input this is var i = $('#shippingInfo input:text').index( this ); var valueFromShippingInput = $(this).val(); $('#billingInfo input:text:eq('+i+')').val( valueFromShippingInput ); } }); // close bind()
In the preceding solution I’m using the input:text
selector to avoid disabling the checkbox itself.
Using .trigger('change')
immediately executes the .change()
event. This will check the state of the checkbox initially, in case it
is checked by default. Also, this protects against Firefox and other
browsers that hold on to radio button and checkbox states when the
page is refreshed.
What will happen when JavaScript is disabled? You should hide
the checkbox by default in CSS. Then use JavaScript to add a class
name to a parent element that would override the previous CSS rule. In
the following example code I’ve added an extra <div>
surrounding the checkbox and
label so they can be easily hidden:
<style type="text/css" title="text/css"> #sameAsShippingWrapper { display:none; } .jsEnabled #sameAsShippingWrapper { display:block } </style> ... // when the HTML DOM is ready $(document).ready(function(){ $('form').addClass('jsEnabled'), }); ... <form> ... <div id="sameAsShippingWrapper"> <label for="sameAsShipping">Same as Shipping</label> <input name="sameAsShipping" id="sameAsShipping" type="checkbox" ... /> </div> .... </form>
As an alternative to hiding the checkbox in CSS and showing it using JavaScript, you could add the checkbox to the DOM using JavaScript. I prefer to keep my HTML, CSS, and JavaScript separate, but sometimes this is the better solution:
var html_label = '<label for="sameAsShipping">Same as Shipping</label>'; var html_input = '<input name="sameAsShipping" id="sameAsShipping" type="checkbox" value="sameAsShipping" />'; $( html_label + html_input ).prependTo('#billingInfo").change( ... ).trigger( ... );
You have a series of radio buttons. The last radio button is labeled “Other” and has a text input field associated with it. Naturally you’d want that radio button to be selected if the user has entered text in the Other field:
<p>How did you hear about us?</p> <ul id="chooseSource"> <li> <input name="source" id="source1" type="radio" value="www" /> <label for="source1">Website or Blog</label> </li> <li> <input name="source" id="source2" type="radio" value="mag" /> <label for="source2">Magazine</label> </li> <li> <input name="source" id="source3" type="radio" value="per" /> <label for="source3">Friend</label> </li> <li> <input name="source" id="source4" type="radio" value="oth" /> <label for="source4">Other</label> <input name="source4txt" id="source4txt" type="text" /> </li> </ul>
In the HTML code you’ll notice the radio button, label, and
associated text input elements are wrapped in an <li>
tag. You don’t necessarily need
this structure, but it makes finding the correct radio button much
easier—you’re guaranteed there’s only one radio button sibling:
// find any text input in chooseSource list, and listen for blur $('#chooseSource input:text').blur(function(){ // if text input has text if ( $(this).val() != '' ) { // find the radio button sibling and set it be selected $(this).siblings('input:radio').attr('checked',true); } });
To take the concept one step further, when the radio button is
selected, we can .focus()
the text field. It’s important to note that the
following code completely replaces the previous solution. Instead of
using the .blur()
method and then chaining a .each()
method, just use the .each()
method since that gives us access to
all the objects we need:
$('#chooseSource input:text').each(function(){ // these are both used twice, let's store them to be more efficient // the text input var $inputTxt = $(this); // the associated radio button var $radioBtn = $inputTxt.siblings('input:radio'), // listen for the blur event on the text input $inputTxt.blur(function(){ // if text input has text if ( $inputTxt.val() != '' ) { // select radio button $radioBtn.attr('checked',true); } }); // listen for the change event on the radio button $radioBtn.change(function(){ // if it is checked, focus on text input if ( this.checked ) { $inputTxt.focus(); } }); }); // close each()
The jQuery .sibling()
method
only returns siblings, not the HTML element you’re
attempting to find siblings of. So, the code $(this).siblings('input:
could be rewritten
radio
')$(this).siblings('input')
because
there is only one other input that is a sibling. I prefer including
the :radio
selector because it is
more explicit and creates self-commenting code.
It would have been very easy to target the Other text input
directly using $('#source5txt').focus(...)
and have it
directly target the radio button using its id
attribute. While that’s a perfectly
functional approach, the code as shown previously is more flexible.
What if someone decided to change the id
of the Other radio button? What if each
radio button had a text input? The abstract solution handles these
cases without additional work.
Why use .blur()
instead of
.focus()
on the text input? While
.focus()
would be more immediate
for selecting the associated radio button, if the user were simply
tabbing through the form elements, .focus()
would accidentally select the radio
button. Using .blur()
and then
checking for a value avoids this problem.
What will happen when JavaScript is disabled? The user will have to manually click into the text input to start typing and manually select the radio button. You are left to decide how to validate and process submitted data should the user enter text and select a different radio button.
You need to select all checkboxes and deselect all checkboxes using dedicated Select All and Deselect All links:
<fieldset> <legend>Reasons to be happy</legend> <a class="selectAll" href="#">Select All</a> <a class="deselectAll" href="#">Deselect All</a> <input name="reasons" id="iwokeup" type="checkbox" value="iwokeup" /> <label for="iwokeup">I woke up</label> <input name="reasons" id="health" type="checkbox" value="health" /> <label for="health">My health</label> <input name="reasons" id="family" type="checkbox" value="family" /> <label for="family">My family</label> <input name="reasons" id="sunshine" type="checkbox" value="sunshine" /> <label for="sunshine">The sun is shining</label> </fieldset>
Target the Select All and Deselect All links directly using
their class
attributes. Then attach
the appropriate .click()
handler:
// find the "Select All" link in a fieldset and list for the click event $('fieldset .selectAll').click(function(event){ event.preventDefault(); // find all the checkboxes and select them $(this).siblings('input:checkbox').attr('checked','checked'), }); // find the "Deselect All" link in a fieldset and list for the click event $('fieldset .deselectAll').click(function(event){ event.preventDefault(); // find all the checkboxes and deselect them $(this).siblings('input:checkbox').removeAttr('checked'), });
If you are interested in activating and deactivating the dedicated links, you should see Recipe 10.5 in this chapter. In that solution, the individual checkboxes update the toggle state, and you will need this logic to activate and deactivate the dedicated links appropriately.
What will happen when JavaScript is disabled? You should hide the links by default in CSS. Then use JavaScript to add a class name to a parent element that will override the previous CSS rule:
<style type="text/css" title="text/css"> .selectAll, .deselectAll { display:none; } .jsEnabled .selectAll, .jsEnabled .deselectAll { display:inline; } </style> ... // when the HTML DOM is ready $(document).ready(function(){ $('form').addClass('jsEnabled'), });
You need to select and deselect all checkboxes using a single toggle, in this case another checkbox. Additionally, that toggle should automatically switch states if some (or all) of the checkboxes are selected individually:
<fieldset> <legend>Reasons to be happy</legend> <input name="reasons" id="toggleAllReasons" type="checkbox" class="toggle" /> <label for="toggleAllReasons" class="toggle">Select All</label> <input name="reasons" id="iwokeup" type="checkbox" value="iwokeup" /> <label for="iwokeup">I woke up</label> <input name="reasons" id="health" type="checkbox" value="health" /> <label for="health">My health</label> <input name="reasons" id="family" type="checkbox" value="family" /> <label for="family">My family</label> <input name="reasons" id="sunshine" type="checkbox" value="sunshine" /> <label for="sunshine">The sun is shining</label> </fieldset>
Target the toggle directly using its class
attribute and the :checkbox
selector. Then cycle through each
toggle found, determine the associated checkboxes using .siblings()
, and attach
the change
event listeners:
// find the "Select All" toggle in a fieldset, cycle through each one you find $('fieldset .toggle:checkbox').each(function(){ // these are used more than once, let's store them to be more efficient // the toggle checkbox var $toggle = $(this); // the other checkboxes var $checkboxes = $toggle.siblings('input:checkbox'), // listen for the change event on the toggle $toggle.change(function(){ if ( this.checked ) { // if checked, select all the checkboxes $checkboxes.attr('checked','checked'), } else { // if not checked, deselect all the checkboxes $checkboxes.removeAttr('checked'), } }); // listen for the change event on each individual checkbox (not toggle) $checkboxes.change(function(){ if ( this.checked ) { // if this is checked and all others are checked, select the toggle if ( $checkboxes.length == $checkboxes.filter(':checked').length ) { $toggle.attr('checked','checked'), } } else { // if not checked, deselect the toggle $toggle.removeAttr('checked'), } }).eq(0).trigger('change'), // close change() then trigger change on first checkbox only }); // close each()
Using .eq(0).trigger('change')
immediately
executes the .change()
event
for the first checkbox. This sets the state of the
toggle and protects against Firefox and other browsers that hold on to
radio and checkbox states when the page is refreshed. The .eq(0)
is used to only trigger the first checkbox’s change
event. Without .eq(0)
, the .trigger('change')
would be executed for
every checkbox, but since they all share the same toggle, you only
need to run it once.
What will happen when JavaScript is disabled? You should hide the toggle checkbox and label by default in CSS. Then use JavaScript to add a class name to a parent element that would override the previous CSS rule:
<style type="text/css" title="text/css"> .toggle { visibility:hidden; } .jsEnabled .toggle { visibility:visible; } </style> ... // when the HTML DOM is ready $(document).ready(function(){ $('form').addClass('jsEnabled'), });
You have a drop-down box for colors and want to add new colors to it, as well as remove options from it.
<label for="colors">Colors</label> <select id="colors" multiple="multiple"> <option>Black</options> <option>Blue</options> <option>Brown</options> </select> <button id="remove">Remove Selected Color(s)</button> <label for="newColorName">New Color Name</label> <input id="newColorName" type="text" /> <label for="newColorValue">New Color Value</label> <input id="newColorValue" type="text" /> <button id="add">Add New Color</button>
To add a new option to the drop-down box, use the .appendTo()
method:
// find the "Add New Color" button $('#add').click(function(event){ event.preventDefault(); var optionName = $('#newColorName').val(); var optionValue = $('#newColorValue').val(); $('<option/>').attr('value',optionValue).text(optionName).appendTo('#colors'), });
To remove an option, use the .remove()
method:
// find the "Remove Selected Color(s)" button $('#remove').click(function(event){ event.preventDefault(); var $select = $('#colors'), $('option:selected',$select).remove(); });
I use the .attr()
and .text()
methods to populate the <option>
element:
$('<option/>').attr("value",optionValue).text(optionName).appendTo('#colors'),
However, the same line could be rewritten so that the <option>
element is built in one step,
without using the methods:
$('<option value="'+optionValue+'">'+optionName+'</option>').appendTo('#colors'),
Concatenating all the <option>
data like that would be a
fraction of a millisecond faster, but not in any way noticeable by a
human. I prefer using the .attr()
and .text()
methods to populate the
<option>
element because I
think that it is more readable and easier to debug and maintain. With
the performance issue being negligible, using one approach or the
other is the developer’s preference.
What would happen with JavaScript disabled? You would need to provide a server-side alternative that processes the button clicks, and the user would have to wait for the resulting page reloads.
You have a form for allowing users to register a product online, and you require the user to enter a serial number printed on the installation discs. This number is 16 digits long and separated across four input fields. Ideally, to speed the user along in their data entry, as each input field is filled up, you’d like to automatically focus the next input field until they’re finished typing the number:
<fieldset class="autotab"> <legend>Product Serial Number</legend> <input type="text" maxlength="4" /> <input type="text" maxlength="4" /> <input type="text" maxlength="4" /> <input type="text" maxlength="4" /> </fieldset>
Inside <fieldset
class="autotab">
, find all the <input>
elements. Use jQuery’s
.bind()
method to listen for the keydown
and keyup
events. We exit the bound function for
a handful of keys that we want to ignore, because they aren’t
meaningful for automatically tabbing forward or backward. When an
<input>
element is full,
based on the maxlength
attribute,
we .focus()
the next <input>
element. Conversely, when
using the Backspace key, if an <input>
element is made empty, we
.focus()
the previous <input>
element:
$('fieldset.autotab input').bind('keydown keyup',function(event){ // the keycode for the key evoking the event var keyCode = event.which; // we want to ignore the following keys: // 9 Tab, 16 Shift, 17 Ctrl, 18 Alt, 19 Pause Break, 20 Caps Lock // 27 Esc, 33 Page Up, 34 Page Down, 35 End, 36 Home // 37 Left Arrow, 38 Up Arrow, 39 Right Arrow, 40 Down Arrow // 45 Insert, 46 Forward Delete, 144 Num Lock, 145 Scroll Lock var ignoreKeyCodes = ',9,16,17,18,19,20,27,33,34,35,36,37,38,39,40,45,46,144,145,'; if ( ignoreKeyCodes.indexOf(',' + keyCode + ',') > −1 ) { return; } // we want to ignore the backspace on keydown only // let it do its job, but otherwise don't change focus if ( keyCode == 8 && event.type == 'keydown' ) { return; } var $this = $(this); var currentLength = $this.val().length; var maximumLength = $this.attr('maxlength'), // if backspace key and there are no more characters, go back if ( keyCode == 8 && currentLength == 0 ) { $this.prev().focus(); } // if we've filled up this input, go to the next if ( currentLength == maximumLength ) { $this.next().focus(); } });
Why do we bind both keydown
and keyup
events?
You could use just the keydown
event. However, when the user is
done filling out the first input, there would be no visual indication
that their next keystroke would focus the second input. By using the
keyup
event, after the first input
is filled, the second input gains focus, the cursor is placed at the
beginning of the input, and most browsers indicate that focus with a
border or some other highlight state. Also, the keyup
event is required for the Backspace
key to focus the previous input after the current input is
empty.
You could use just the keyup
event. However, if your cursor was in the second input and you were
using the Backspace key to clear it, once you removed all characters,
the focus would be shifted into the first input. Unfortunately, the
first is already full, so the next keystroke would be lost, because of
the maxlength
attribute, and then
the keyup
event would focus the
second input. Losing a keystroke is a bad thing, so we perform the
same check on keydown
, which moves
the cursor to the next input before the character is lost.
Because the logic isn’t CPU intensive, we can get away with
binding both the keydown
and
keyup
events. In another situation,
you may want to be more selective.
You’ll notice that the ignoreKeyCodes
variable is a string. If we
were building it dynamically, it would be faster to create an array
and then use .join(',')
or .toString()
JavaScript methods. But since
the value is always the same, it’s easier to simply code it as a
string from the very beginning. I also start and end the ignoreKeyCodes
variable with commas, because
I am paranoid about false positives. This way, when searching for a
keyCode
flanked by commas, you are
guaranteed to find only the number you’re looking for—if you look for
9
, it won’t find 19
, or 39
.
Notice there is no code to prevent $this.next().focus()
from executing when on
the last <input>
element. I’m
taking advantage of the jQuery chain here. If $this.next()
finds nothing, then the chain
stops—it can’t .focus()
what it
can’t find. In a different scenario, it might make sense to precache
any known .prev()
and .next()
elements.
What will happen when JavaScript is disabled? Nothing. The user will have to manually click from one text input field to the next.
Your company has a contact form on its website. This
form has a <textarea>
element
to allow users to express themselves freely. However,
you know time is money, and you don’t want your staff reading short
novels, so you would like to limit the length of the messages they
have to read. In the process, you’d also like to show the end user how
many characters are remaining:
<textarea></textarea> <div class="remaining">Characters remaining: <span class="count">300</span></div>
Target all .remaining
messages, and for each find the associated <textarea>
element and the maximum
number of characters as listed in the .count
child element. Bind an update
function to the <textarea>
to capture when the user
enters text:
// for each "Characters remaining: ###" element found $('.remaining').each(function(){ // find and store the count readout and the related textarea/input field var $count = $('.count',this); var $input = $(this).prev(); // .text() returns a string, multiply by 1 to make it a number (for math) var maximumCount = $count.text()*1; // update function is called on keyup, paste and input events var update = function(){ var before = $count.text()*1; var now = maximumCount - $input.val().length; // check to make sure users haven't exceeded their limit if ( now < 0 ){ var str = $input.val(); $input.val( str.substr(0,maximumCount) ); now = 0; } // only alter the DOM if necessary if ( before != now ){ $count.text( now ); } }; // listen for change (see discussion below) $input.bind('input keyup paste', function(){setTimeout(update,0)} ); // call update initially, in case input is pre-filled update(); }); // close .each()
The preceding code is generic enough to allow for any number of
“Character remaining” messages and <textarea>
elements on a given page.
This could be useful if you were building a content management or data
entry system.
To protect against when the user attempts to copy and paste data
into the <textarea>
using a
mouse, we need to bind both the input
and paste
events. The mouseup
event cannot be used because it is
not triggered when selecting an item from the browser’s contextual
menu. The input
event is part of
HTML5 (Working Draft) and already implemented by Firefox, Opera, and
Safari. It fires on user input, regardless of input device (mouse or
keyboard). Safari, at the time of this writing, has a bug and does not
fire the input event on <textarea>
elements. Both Safari and
Internet Explorer understand the paste
event on <textarea>
elements and understand
keyup
to capture keystrokes.
Attaching keyup
, input
, and paste
is redundant but, in this case,
benign. The update
function is
simple enough that there aren’t any performance issues, and it only
manipulates the DOM when needed, so any redundant update
calls after the first would do
nothing.
An alternative to redundant events would be to use setInterval
when the <textarea>
element has focus. The same
update
function could be called
from the interval, and if it is paired with the keyup
event, you’d get the immediate
updating on key presses and an arbitrary update interval, say 300
milliseconds, for when information is pasted into the <textarea>
element. If the update
function were more complex or costly,
this might be a better alternative.
When binding events to form elements, it is sometimes important
to use a timeout to slightly delay a function call. In the previous
example, Internet Explorer triggers the paste
event before the text from the
clipboard is actually added to the <textarea>
element. Thus, the
calculation for characters remaining would be incorrect until the user
clicks or presses another key. By using setTimeout(update,0)
, the update function is
placed at the end of the call stack and will fire after that browser
has added the text:
$input.bind('input keyup paste', function(){setTimeout(update,0)} );
What will happen when JavaScript is disabled? You should hide the “Characters remaining” message by default in CSS. Then use JavaScript to add a class name to a parent element that would override the previous CSS rule. Also, it’s important to check the length of the message again on the server side:
<style type="text/css" title="text/css"> .remaining { display:none; } .jsEnabled .remaining { display:block; } </style> ... // when the HTML DOM is ready $(document).ready(function(){ $('form').addClass('jsEnabled'), });
Your shopping cart page has a quantity field, and you want to make sure users can only enter numbers into that field:
<input type="text" class="onlyNumbers" />
Find all elements with the onlyNumbers
class, and listen for keydown
and blur
events. The keydown
event handler will prevent users
from typing non-numeric characters into the field. The blur
event handler is a precautionary
measure that cleans any data entered via Paste from the contextual
menu or the browser’s Edit menu:
$('.onlyNumbers').bind('keydown',function(event){ // the keycode for the key pressed var keyCode = event.which; // 48-57 Standard Keyboard Numbers var isStandard = (keyCode > 47 && keyCode < 58); // 96-105 Extended Keyboard Numbers (aka Keypad) var isExtended = (keyCode > 95 && keyCode < 106); // 8 Backspace, 46 Forward Delete // 37 Left Arrow, 38 Up Arrow, 39 Right Arrow, 40 Down Arrow var validKeyCodes = ',8,37,38,39,40,46,'; var isOther = ( validKeyCodes.indexOf(',' + keyCode + ',') > −1 ); if ( isStandard || isExtended || isOther ){ return true; } else { return false; } }).bind('blur',function(){ // regular expression that matches everything that is not a number var pattern = new RegExp('[^0-9]+', 'g'), var $input = $(this); var value = $input.val(); // clean the value using the regular expression value = value.replace(pattern, ''), $input.val( value ) });
The keydown
event is immediate and prevents users from typing non-numeric
characters into the field. This could be replaced with a keyup
event that shares the same handler as the blur
event. However, users would see a
non-numeral appear and then quickly disappear. I prefer just to
prevent them from entering the character in the first place and avoid
the flickering.
The blur
event protects against copying and pasting non-numeric
characters into the text field. In the previous scenario, I’m assuming
the user is either trying to test the limits of the JavaScript
(something that I would do) or trying to copy and paste data from a
spreadsheet. Neither situation requires immediate correction in my
opinion. However, if your situation requires more immediate
correction, please see the “Discussion” section of Recipe 10.8 for more information
about capturing changes from the paste event.
If your situation is different and you expect users to be copying and pasting data from a spreadsheet, keep in mind that the regular expression I use does not account for a decimal point. So, a number like “1,000” would be cleaned to “1000” and the number “10.00” would also be cleaned to “1000” as well.
You’ll notice that the validKeyCodes
variable is a string that
starts and ends with commas. As I mentioned in Recipe 10.7, I did this because
I am paranoid about false positives—when searching for a keyCode
flanked by commas, you are
guaranteed to find only the number you’re looking for.
What will happen when JavaScript is disabled? The user will be able to enter any characters they please. Always be sure to validate code on the server. Don’t rely on JavaScript to provide clean data.
You have a form that you would like to submit using Ajax:
<form action="process.php"> <!-- value changed via JavaScript --> <input type="hidden" name="usingAJAX" value="false" /> <label for="favoriteFood">What is your favorite food?</label> <input type="text" name="favoriteFood" id="favoriteFood" /> <input type="submit" value="Tell Us" /> </form>
Find the <form>
element, and hijack the submit
event:
$('form').submit(function(event){ // we want to submit the form using Ajax (prevent page refresh) event.preventDefault(); // this is where your validation code (if any) would go // ... // this tells the server-side process that Ajax was used $('input[name="usingAJAX"]',this).val( 'true' ); // store reference to the form var $this = $(this); // grab the url from the form element var url = $this.attr('action'), // prepare the form data to send var dataToSend = $this.serialize(); // the callback function that tells us what the server-side process had to say var callback = function(dataReceived){ // hide the form (thankfully we stored a reference to it) $this.hide(); // in our case the server returned an HTML snippet so just append it to // the DOM // expecting: <div id="result">Your favorite food is pizza! Thanks for // telling us!</div> $('body').append(dataReceived) }; // type of data to receive (in our case we're expecting an HTML snippet) var typeOfDataToReceive = 'html'; // now send the form and wait to hear back $.get( url, dataToSend, callback, typeOfDataToReceive ) }); // close .submit()
What will happen when JavaScript is disabled? The form will be
submitted, and the entire page will refresh with the results from the
server-side script. I use JavaScript to alter the value of the
<input type="hidden" name="usingAJAX"
/>
element from false
to true
. This allows the
server-side script to know what to send back as a response—either a
full HTML page or whatever data is expected for the Ajax
response.
You have a form that you would like to validate. To get
started, you’ll want to set up some basic CSS. The only styles that
are really important for this enhancement are the display:none
declaration of the div.errorMessage
selector and the display:block
declaration of the div.showErrorMessage
selector. The rest are just to make things look better:
<style type="text/css" title="text/css"> div.question { padding: 1em; } div.errorMessage { display: none; } div.showErrorMessage { display: block; color: #f00; font-weight: bold; font-style: italic; } label.error { color: #f00; font-style: italic; } </style>
The following HTML snippet is one example of how you might
structure this form. The <div
class="question>
element is purely for layout and not
important for the validation code. Each <label>
element’s for
attribute associates it with the form
element with that identical id
attribute. That is standard HTML, but I wanted to call it out because
the JavaScript will also be using that (in reverse) to find the
correct <label>
for a given
form element. Similarly, you’ll notice the error messages have an
id
attribute of errorMessage_
plus the name
attribute of the associated form
element. This structure may seem redundant, but radio buttons and
checkboxes are grouped by the name
attribute and you’d only want to have one error message per such
group:
<form action="process.php"> <!-- TEXT --> <div class="question"> <label for="t">Username</label> <input id="t" name="user" type="text" class="required" /> <div id="errorMessage_user" class="errorMessage">Please enter your username.</div> </div> <!-- PASSWORD --> <div class="question"> <label for="p">Password</label> <input id="p" name="pass" type="password" class="required" /> <div id="errorMessage_pass" class="errorMessage">Please enter your password.</div> </div> <!-- SELECT ONE --> <div class="question"> <label for="so">Favorite Color</label> <select id="so" name="color" class="required"> <option value="">Select a Color</option> <option value="ff0000">Red</option> <option value="00ff00">Green</option> <option value="0000ff">Blue</option> </select> <div id="errorMessage_color" class="errorMessage">Please select your favorite color.</div> </div> <!-- SELECT MULTIPLE --> <div class="question"> <label for="sm">Favorite Foods</label> <select id="sm" size="3" name="foods" multiple="multiple" class="required"> <option value="pizza">Pizza</option> <option value="burger">Burger</option> <option value="salad">Salad</option> </select> <div id="errorMessage_foods" class="errorMessage">Please choose your favorite foods.</div> </div> <!-- RADIO BUTTONS --> <div class="question"> <span>Writing Hand:</span> <input id="r1" type="radio" name="hand" class="required"/> <label for="r1">Left</label> <input id="r2" type="radio" name="hand" class="required" /> <label for="r2">Right</label> <div id="errorMessage_hand" class="errorMessage">Please select what hand you write with.</div> </div> <!-- TEXTAREA --> <div class="question"> <label for="tt">Comments</label> <textarea id="tt" name="comments" class="required"></textarea> <div id="errorMessage_comments" class="errorMessage">Please tell us what you think.</div> </div> <!-- CHECKBOX --> <div class="question"> <input id="c" type="checkbox" name="legal" class="required" /> <label for="c">I agree with the terms and conditions</label> <div id="errorMessage_legal" class="errorMessage">Please check the box!</div> </div> <input type="submit" value="Continue" /> </form>
The first part of the solution is fairly straightforward. Find
the <form>
element, and
hijack the submit
event. When the
form is submitted, iterate through the required form elements, and
check to see whether the required elements are valid. If the form is
error free, then (and only then) trigger the submit
event:
$('form').submit(function(event){ var isErrorFree = true; // iterate through required form elements and check to see if they are valid $('input.required, select.required, textarea.required',this).each(function(){ if ( validateElement.isValid(this) == false ){ isErrorFree = false; }; }); // Ajax alternatives: // event.preventDefault(); // if (isErrorFree){ $.get( url, data, callback, type ) } // if (isErrorFree){ $.post( url, data, callback, type ) } // if (isErrorFree){ $.ajax( options ) } return isErrorFree; }); // close .submit()
The second part of this solution is where all the real
validation happens. The isValid()
method starts by storing frequently used data from the element we’re
validating. Then, in the switch()
statement, the element is validated. Finally, class names are added to
or removed from the <label>
and div.errorMessage
elements.
var validateElement = { isValid:function(element){ var isValid = true; var $element = $(element); var id = $element.attr('id'), var name = $element.attr('name'), var value = $element.val(); // <input> uses type attribute as written in tag // <textarea> has intrinsic type of 'textarea' // <select> has intrinsic type of 'select-one' or 'select-multiple' var type = $element[0].type.toLowerCase(); switch(type){ case 'text': case 'textarea': case 'password': if ( value.length == 0 || value.replace(/s/g,'').length == 0 ){ isValid = false; } break; case 'select-one': case 'select-multiple': if( !value ){ isValid = false; } break; case 'checkbox': case 'radio': if( $('input[name="' + name + '"]:checked').length == 0 ){ isValid = false; }; break; } // close switch() // instead of $(selector).method we are going to use $(selector)[method] // choose the right method, but choose wisely var method = isValid ? 'removeClass' : 'addClass'; // show error message [addClass] // hide error message [removeClass] $('#errorMessage_' + name)[method]('showErrorMessage'), $('label[for="' + id + '"]')[method]('error'), return isValid; } // close validateElement.isValid() }; // close validateElement object
The validation in this solution is quite simple. It checks the following:
<input
type="text">
, <input
type="password">
, and <textarea>
elements have some data
other than whitespace.
<select>
elements have something other than the default option
selected. Please note that there are two types of <select>
element: “select-one” and
“select-multiple” (see the second code
snippet in this section for HTML code and the previous code
snippet for JavaScript validation). The first <option>
element of the
“select-one” <select>
must have a value=""
in order
for validation to work. The “select-multiple” <select>
is immune from this
requirement because its <option>
elements can be
deselected.
<input
type="radio">
and <input
type="checkbox">
elements have at least one element
checked in their respective name
groups.
The switch(){}
statement
is used because it is more efficient than multiple
if(){}else if(){}
statements. It
also allows for elements with shared validation to be grouped
together, letting the break;
statement separate these groups.
The validateElement
object is
in the global scope with the intention that it might be reused on
other forms. It also keeps the global scope less cluttered by
containing the validation methods—in the future, helper methods could
be added to the validateElement
object without worrying
about global naming collisions. For instance, a stripWhitespace()
method could be implemented like this:
var validateElement = { stripWhitespace : function(str){ return str.replace(/s/g,''), }, isValid : function(element){ //... snipped code ...// case 'text': case 'textarea': case 'password': // if text length is zero after stripping whitespace, it's not valid if ( this.stripWhitespace(value).length == 0 ){ isValid = false; } break; //... snipped code ...// } // close validateElement.isValid() }; // close validateElement object
When showing and hiding error messages, I used the bracket
notation for calling the .addClass()
and .removeClass()
jQuery methods:
// instead of $(selector).method we are going to use $(selector)[method] // choose the right method, but choose wisely var method = isValid ? 'removeClass' : 'addClass'; // show error message [addClass] // hide error message [removeClass] $('#errorMessage_' + name)[method]('showErrorMessage'), $('label[for="' + id + '"]')[method]('error'),
The previous code in bracket notation is functionally identical to the dot notation:
if (isValid) { $('#errorMessage_' + name).removeClass('showErrorMessage'), $('label[for="' + id + '"]').removeClass('error'), } else { $('#errorMessage_' + name).addClass('showErrorMessage'), $('label[for="' + id + '"]').addClass('error'), }
When we validate on submit, the dot notation is cleaner and more
readable. However, let’s extend the bracket-notation solution to allow
elements to revalidate (after an initial validation) using the
change
event. This would give the
user immediate feedback that their new answers are in fact valid,
without requiring them to click the submit button. The following code
does not work as expected (see the next paragraph for the real
solution), but it illustrates where to .unbind()
and .bind()
the change
event:
// instead of $(selector).method we are going to use $(selector)[method] // choose the right method, but choose wisely var method = isValid ? 'removeClass' : 'addClass'; // show error message [addClass] // hide error message [removeClass] $('#errorMessage_' + name)[method]('showErrorMessage'), $('label[for="' + id + '"]')[method]('error'), // after initial validation, allow elements to re-validate on change $element .unbind('change.isValid') .bind('change.isValid',function(){ validateElement.isValid(this); });
Because we are unbinding and binding the change
event with each validation, I added
the .isValid
event namespace to
target it more directly. This way, if a form element has other
change
events bound, they will
remain.
The problem with the previous code isn’t syntax but logic.
You’ll note that the radio buttons in the HTML have the class="required"
attribute. This means that
when the entire form is validated, each radio button is validated, and
(more importantly) each radio button’s <label>
has a class added or removed
to indicate the error. However, if we allow for a revalidation to
occur using the element-specific change
event, only that particular radio
button’s <label>
will be
updated—the others would remain in an error state. To account for
this, a single change
event would
have to look at all radio buttons and checkboxes in that name
group to affect all the <label>
classes simultaneously:
// instead of $(selector).method we are going to use $(selector)[method] // choose the right method, but choose wisely var method = isValid ? 'removeClass' : 'addClass'; // show error message [addClass] // hide error message [removeClass] $('#errorMessage_' + name)[method]('showErrorMessage'), if ( type == 'checkbox' || type == 'radio' ) { // if radio button or checkbox, find all inputs with the same name $('input[name="' + name + '"]').each(function(){ // update each input elements <label> tag, (this==<input>) $('label[for="' + this.id + '"]')[method]('error'), }); } else { // all other elements just update one <label> $('label[for="' + id + '"]')[method]('error'), } // after initial validation, allow elements to re-validate on change $element .unbind('change.isValid') .bind('change.isValid',function(){ validateElement.isValid(this); });
If the preceding code were to be rewritten using dot-notation
syntax, it would have twice again as many lines. And on a separate
note, with this new logic in place, only one radio button (or
checkbox) in a name
group would
need to have the class="required"
in order for all the other elements in that group to be adjusted
correctly.
What will happen when JavaScript is disabled? The form will be submitted without client-side validation. Always be sure to validate code on the server. Don’t rely on JavaScript to provide clean data. If the server-side code returns the form with errors, it can use the same classes, on the same elements, in the same way. There is no need to use inline style tags or write custom code to handle the server-side errors differently.
13.59.107.152