Probably the most popular use for JavaScript with web applications is to improve HTML forms. Standard HTML forms aren't very smart. They simply accept input and pass it on to the web server where all the processing must occur. With JavaScript, however, we can do much more on the client side. JavaScript can validate input before it is sent to the server. Forms can also dynamically react to user input and update fields in order to provide immediate feedback to the user; a dynamic form can often substitute for multiple static forms.
The benefit JavaScript provides for the server is that it shifts some work that might otherwise be done on the server to the client, and it reduces the number of server requests. The benefit JavaScript provides to the user is that it provides immediate feedback without a delay while the browser fetches a new page.
When you create an HTML form, you generally expect the user to fill it out in a particular way. There are numerous types of restrictions a form may have. For example, some of the fields may only accept numbers while others may only accept dates, some fields may only accept a certain range of entries, some fields may be required, and some combinations of fields may not permitted. All of these examples must be handled by only two types of checks: the first is to validate each element user's input as the data is entered; the second is to perform the validation when the form is submitted.
Checking a form element when the user enters a value is most effective for validating the format or range of a particular element. For example, if a field only accepts numbers, you can verify that the user did not enter any non-numeric characters.
To perform this check, we use the onChange event handler. This handler supports the following form elements: Text, TextArea, Password, File Upload, and Select. For each of these elements, we can register an onChange handler and assign it code to execute when the element changes. We register it simply by adding it as an attribute to the HTML tag that creates the element. For example:
<INPUT TYPE="text" name="age" onChange="checkAge( this );">
This runs the function checkAge and passes it a reference to itself via this. checkAge looks like this:
function checkAge ( element ) { if ( element.value != parseInt( element.value ) || element.value < 1 || element.value > 150 ) { alert( "Please enter a number between 1 and 150 for age." ); element.focus( ); return false; } return true; }
This function checks that the age entered is an integer and between 1 and 150 (sorry if you happen to be 152, but we have to draw the line somewhere).
If checkAge determines that the input is invalid, it displays an alert asking the user to enter the value again (Figure 7-1), and moves the cursor back to the age field via element.focus( ) . It then returns true or false depending on whether the check was successful or not. This isn't necessary, but it does help us if we later decide to string multiple function calls together as you'll see later in Example 7-2.
Note that we don't need to call a function to handle onChange. We can assign multiple statements directly to the onChange handler. However, it's often much easier to work with HTML documents when the JavaScript is kept together as much as possible, and functions help us to do this. They also allow us to share code when we have multiple form elements that require the same validation. For functions that you use often, you can go a step further and place these in a JavaScript file that you can include in multiple HTML files. We'll see an example of this in Figure 7-2.
The other way that we can perform data validation is to do so just before the form is submitted. This is the best time to check whether required fields have been filled or to perform checks that involve dependencies between multiple elements. We perform this check with JavaScript's onSubmit handler.
onSubmit works like the onChange handler except that it is added as an attribute of the <FORM> tag:
<FORM METHOD="POST" ACTION="/cgi/register.cgi" onSubmit="return checkForm(this);">
There's another difference you may notice. The onSubmit handler returns the value of the code that it calls. If the onSubmit handler returns false, it cancels the submission of the form after the handler code has run. In any other case, the form submission continues. Return values have no effect on the onChange handler.
Here is the function that checks our form:
function checkForm ( form ) { if ( form["age"].value == "" ) { alert( "Please enter your age." ); return false; } return true; }
This example simply verifies that a value was entered for age. Remember, our onChange handler is not enough to do this because it is only run when the value for age changes. If the user never fills in a value for age, the onChange handler will never be called. This is why we check for required values with onSubmit.
Let's look at a complete example. It seems that more and more web sites want users to register and provide lots of personal information in order to use their web site. We'll create a slightly exaggerated version of a registration form (Figure 7-2).
Note that this form applies only to United States residents. In practice, Internet users come from around the world, so you must be flexible with your validation to accommodate the various international formats for phone numbers, postal codes, etc. However, since the purpose of this example is to demonstrate validation, we'll restrict the formats to one set that can be easily validated. The required formats for phone numbers and social security numbers are shown. In addition, the zip code is a five-digit postal code.
The HTML is shown in Example 7-1.
<html> <head> <title>User Registration</title> <script src="/js-lib/formLib.js"></script> <script><!-- function validateForm ( form ) { requiredText = new Array( "name", "address", "city", "zip", "home_phone", "work_phone", "age", "social_security", "maiden_name" ); requiredSelect = new Array( "state", "education" ); requiredRadio = new Array( "gender" ); return requireValues ( form, requiredText ) && requireSelects( form, requiredSelect ) && requireRadios ( form, requiredRadio ) && checkProblems ( ); } // --> </script> </head> <body bgcolor="#ffffff"> <h2>User Registration Form</h2> <p>Hi, in order for you to access our site, we'd like first to get as much personal information as we can from you in order to sell to other companies. You don't mind, do you? Great! Then please fill this out as accurately as possible.</p> <p>Note this form is for U.S. residents only. Others should use the <a href="intl_registration.html">International Registration Form</a>.</p> <hr> <form method="POST" action="/cgi/register.cgi" onSubmit="return checkValues( this, requiredText ) && checkMenus"> <form method="GET" onSubmit="return validateForm( this );"> <table border=0> <tr><td> Name: </td><td> <input type="text" name="name" size="30" maxlength="30"> </td></tr> <tr><td> Address: </td><td> <input type="text" name="address" size="40" maxlength="50"> </td></tr> <tr><td> City: </td><td> <input type="text" name="city" size="20" maxlength="20"> </td></tr> <tr><td> State: </td><td> <select name="state" size="1"> <option value="">Please Choose a State</option> <option value="AL">Alabama</option> <option value="AK">Alaska</option> <option value="AZ">Arizona</option> . . . <option value="WY">Wyoming</option> </select> </td></tr> <tr><td> Zip Code: </td><td> <input type="text" name="zip" size="5" maxlength="5" onChange="checkZip( this );"> </td></tr> <tr><td> Home Phone Number: </td><td> <input type="text" name="home_phone" size="12" maxlength="12" onChange="checkPhone( this );"> <i>(please use this format: 800-555-1212)</i> </td></tr> <tr><td> Work Phone Number: </td><td> <input type="text" name="work_phone" size="12" maxlength="12" onChange="checkPhone( this );"> <i>(please use this format: 800-555-1212)</i> </td></tr> <tr><td> Social Security Number (US residents only): </td><td> <input type="text" name="social_security" size="11" maxlength="11" onChange="checkSSN( this );"> <i>(please use this format: 123-45-6789)</i> </td></tr> <tr><td> Mother's Maiden Name: </td><td> <input type="text" name="maiden_name" size="20" maxlength="20"> </td></tr> <tr><td> Age: </td><td> <input type="text" name="age" size="3" maxlength="3" onChange="checkAge( this );"> </td></tr> <tr><td> Gender: </td><td> <input type="radio" name="gender" value="male"> Male <input type="radio" name="gender" value="female"> Female </td></tr> <tr><td> Highest Education: </td><td> <select name="education" size="1"> <option value="">Please Choose a Category</option> <option value="grade">Grade School</option> <option value="high">High School Graduate (or GED)</option> <option value="college">Some College</option> <option value="junior">Technical or Junior College Degree</option> <option value="bachelors">Four Year College Degree</option> <option value="graduate">Post Graduate Degree</option> </select> </td></tr> <tr> <td colspan=2 align=right> <input type="submit"> </td></tr> </table> </form> </body> </html>
You don't see much JavaScript here because most of it is in a separate file that is included with the following pair of tags on line 5:
<script src="/js-lib/formLib.js" ></script>
The contents of formLib.js are shown in Example 7-2.
// formLib.js // Common functions used with forms // // We use this as a hash to track those elements validated on a per element // basis that have formatting problems validate = new Object( ); // Takes a value, checks if it's an integer, and returns true or false function isInteger ( value ) { return ( value == parseInt( value ) ); } // Takes a value and a range, checks if the value is in the range, and // returns true or false function inRange ( value, low, high ) { return ( !( value < low ) && value <= high ); } // Checks values against formats such as '#####' or '###-##-####' function checkFormat( value, format ) { var formatOkay = true; if ( value.length != format.length ) { return false; } for ( var i = 0; i < format.length; i++ ) { if ( format.charAt(i) == '#' && ! isInteger( value.charAt(i) ) ) { return false; } else if ( format.charAt(i) != '#' && format.charAt(i) != value.charAt(i) ) { return false; } } return true; } // Takes a form and an array of element names; verifies that each has a value function requireValues ( form, requiredValues ) { for ( var i = 0; i < requiredValues.length; i++ ) { element = requiredText[i]; if ( form[element].value == "" ) { alert( "Please enter a value for " + element + "." ); return false; } } return true; } // Takes a form and an array of element names; verifies that each has an // option selected (other than the first; assumes that the first option in // each select menu contains instructions) function requireSelects ( form, requiredSelect ) { for ( var i = 0; i < requiredSelect.length; i++ ) { element = requiredSelect[i]; if ( form[element].selectedIndex <= 0 ) { alert( "Please select a value for " + element + "." ); return false; } } return true; } // Takes a form and an array of element names; verifies that each has a // value checked function requireRadios ( form, requiredRadio ) { for ( var i = 0; i < requiredRadio.length; i++ ) { element = requiredRadio[i]; isChecked = false; for ( j = 0; j < form[element].length; j++ ) { if ( form[element][j].checked ) { isChecked = true; } } if ( ! isChecked ) { alert( "Please choose a " + form[element][0].name + "." ); return false; } } return true; } // Verify there are no uncorrected formatting problems with elements // validated on a per element basis function checkProblems ( ) { for ( element in validate ) { if ( ! validate[element] ) { alert( "Please correct the format of " + element + "." ); return false; } } return true; } // Verifies that the value of the provided element has ##### format function checkZip ( element ) { if ( ! checkFormat( element.value, "#####" ) ) { alert( "Please enter a five digit zip code." ); element.focus( ); validate[element.name] = false; } else { validate[element.name] = true; } return validate[element.name]; } // Verifies that the value of the provided element has ###-###-#### format function checkPhone ( element ) { if ( ! checkFormat( element.value, "###-###-####" ) ) { alert( "Please enter " + element.name + " in 800-555-1212 " + "format." ); element.focus( ); validate[element.name] = false; } else { validate[element.name] = true; } return validate[element.name]; } // Verifies that the value of the provided element has ###-##-#### format function checkSSN ( element ) { if ( ! checkFormat( element.value, "###-##-####" ) ) { alert( "Please enter your Social Security Number in " + "123-45-6789 format." ); element.focus( ); validate[element.name] = false; } else { validate[element.name] = true; } return validate[element.name]; } // Verifies that the value of the provided element is an integer between 1 and 150 function checkAge ( element ) { if ( ! isInteger( element.value ) || ! inRange( element.value, 1, 150 ) ) { alert( "Please enter a number between 1 and 150 for age." ); element.focus( ); validate[element.name] = false; } else { validate[element.name] = true; } return validate[element.name]; }
We use both types of validation in this example: validating elements as they are entered and validating the form as a whole when it is submitted. We create a validate object that we use like a Perl hash. Whenever we validate an element, we add the name of this element to the validate object and set it to true or false depending on whether the element has the correct format. When the form is submitted, we later loop over each element in validate to determine if there are any elements that had formatting problems and were not fixed.
The functions that handle specific field validation are checkZip, checkPhone, checkSSN, and checkAge. They are called by the onChange handler for each of these form elements and the functions appear at the bottom of formLib.js. Each of these functions use the more general functions isInteger, isRange, or checkFormat to check the formatting of the element they are validating. isInteger and isRange are simple checks that return whether a value is an integer or whether it is within a particular numeric range.
checkFormat takes a value as well as a string containing a format to check the value against. The structure of our format string is quite simple: a pound symbol represents a numeric digit and any other character represents itself. Of course, in Perl we could easily do checks like this with a regular expression. For example, we could match social security number with /^\d\d\d-\d\d-\d\d\d\d$/. Fortunately, JavaScript 1.2 also supports regular expressions. Unfortunately, there are still many browsers on the Internet that only support JavaScript 1.1, most notably Internet Explorer 3.0.
When the form is submitted, the onSubmit handler calls the validateForm function. This function builds an array of elements such as text boxes that require values, an array of select list elements that require a selection, and an array of radio button group elements that require a checked value. These lists are passed to requireValues, requireSelects, and requireRadios, respectively, which verify that these elements have been filled in by the user.
Finally, the checkProblems function loops over the properties in the validate object and returns a boolean value indicating whether there are any elements that still have formatting problems. If requireValues, requireSelects, requireRadios, or checkProblems fail, then they display an appropriate message to the user and return false, which cancels the submission of the form. Otherwise, the form is submitted to the CGI script which handles the query like any other request. In this case, the CGI script would record the data in a file or database. We won't look at the CGI script here, although we will discuss saving data like this on the server in Chapter 10, "Data Persistence".
Note that we said that the CGI script would handle a request coming from a page with JavaScript validation just like it would handle any other request. When you do data validation with JavaScript, there's an important maxim you need to keep in mind: Never rely on the client to do your data validation for you. When you develop CGI scripts, you should always validate the data you receive, whether the data is coming from a form that performs JavaScript validation or not. Yes, this means that we are performing the same function twice. The theory behind this is that you should never trust data that comes from the client without checking it yourself. As we mentioned earlier, JavaScript may be supported by the user's browser or it may be turned off. Thus, you cannot rely on JavaScript validation being performed. For a more detailed discussion of why it is a bad idea to trust the user, refer to Chapter 8, "Security".
Thus, we may often write our data validation code twice, once in JavaScript for the client, and again in our CGI script. Some may argue that it is poor design to write the same code twice, and they are right in that avoiding duplicate code is a good principle of designing maintainable code. However, in this situation, we can provide two counter-arguments.
First, we need to do data validation in the CGI script because it is also good programming practice for each component to validate its input. The JavaScript code is part of the client user interface; it receives data from the user and validates it in preparation for sending it to the server. It sends the data on to the CGI script, but the CGI script must again validate that the input it receives is in the proper format because the it doesn't know (nor should it care) what processing the client did or did not do on its end. Similarly, if our CGI script then calls a database, the database will again validate the input that we sent on to it, etc.
Second, we gain much by doing JavaScript validation because it lets us validate as close to the user as possible. If we perform data validation on the client using JavaScript, we avoid unnecessary network connections because if JavaScript notices an invalid entry, it can immediately notify the user who can correct the form before it is submitted. Otherwise, the client must submit the form to the server, a CGI script must validate the input and return a page reporting the error and allowing the user to fix the problem. If there are multiple errors, it may take a few tries to get it right.
In many cases, performing the extra check with JavaScript is worth the trade-off. When deciding whether to use JavaScript validation yourself, consider how often you expect the interface and the format of the data to change and how much extra effort is involved in maintaining JavaScript validation code in addition to CGI script validation code. You can then weigh this effort against the convenience to the user.
Copyright © 2001 O'Reilly & Associates. All rights reserved.