reptile7's JavaScript blog
Saturday, July 29, 2006
 
See Me, Feel Me, Touch Me, Validate Me
Blog Entry #46

Suppose you maintain a Web site that solicits input from visitors via one or more forms. We discussed the determination of form control input in Blog Entry #17, but still, how do you ensure that a user interacts with your form(s) as you desire? For example, how do you ensure that a "required" control is not left blank, or that the various fields are filled out properly? HTML Goodies' JavaScript Primers #29, "Putting it all together: Form Field Validation," our focus today, takes us a small step towards sorting out this situation. Scopewise, Primer #29 limits itself to the validation of user data entered into <input type="text"> fields and addresses two aspects of that data:
(1) Some text fields require responses of a given length: has the user entered the correct number of characters?
(2) Some text fields require responses that are restricted to a particular character set; does the user's input conform to the character set on a character-by-character basis?

We know from Blog Entry #17 that a user's input into a text object corresponds to that text object's value property, and we noted in Blog Entry #22 that text object values function in effect as JavaScript String objects. Accordingly, the String object's length property and various methods will be our workhorses in parsing a user's text field input.

The Primer #29 Script and its first name field

Joe alternately judges the Primer #29 Script "a rough one, no doubt about it" and "quite basic" in the primer's "Concept" and script deconstruction, respectively; I myself vote the latter. As can be seen at Joe's demo page, the script codes a form that asks the user for a first name and a zip code; the script establishes certain criteria for the user's input and pops up various alert( ) messages if these criteria are not met. The script's code is given below:

<html><head>
<script type="text/javascript">
function validfn(fnm) {
fnlen=fnm.length;
if (fnlen == 0) {
alert("First name is required");
document.dataentry.fn.focus( ); } }
function validZip(zip) {
len=zip.length;
digits="0123456789";
if (len != 5 && len != 10) {
alert("Zip is not the correct length");
document.dataentry.zip.focus( ); }
for (i=0; i<5; i++) {
if (digits.indexOf(zip.charAt(i))<0) {
alert("First five digits must be numeric");
document.dataentry.zip.focus( );
break; } } }
</script></head>
<body>
<form name="dataentry">
<h2>Form Field Validation</h2>
Enter First Name:<br>
<input type="text" name="fn" onblur="validfn(fn.value);">
<script type="text/javascript">
document.dataentry.fn.focus( );
</script><p>
Enter Zip Code (99999-9999):<br>
<input type="text" name="zip" size="10"><p>
<input type="button" value="Submit" onclick="validZip(zip.value);">
</form></body></html>
<!--The <form> tag is not closed in the primer.-->

When the document first loads, the following code in the document body:

<script type="text/javascript">
document.dataentry.fn.focus( );
</script>

puts an insertion point cursor (a blinking vertical line) in the "Enter First Name" text field; we previously demonstrated the focus( ) method of the text object in the "Method Examples" section of Blog Entry #22. It's important that this code is placed after the <input name="fn"> element in the document source; otherwise, you'll get an error.

Now, about that "Enter First Name" field: there is, to my knowledge, nothing we can do to stop the user from entering, say, Qwkopgg, Zxinmf, or some other obviously-not-a-name into the box, but we can at least make sure that the user doesn't leave it blank. Here's a sequence of events for Joe's approach thereto:

(1) If the user doesn't enter a first name and clicks outside the "Enter First Name" field or tabs to the "Enter Zip Code" field, then the onblur="validfn(fn.value);" code in the <input type="text" name="fn" onblur="validfn(fn.value);"> tag triggers the validfn(fnm) function at the start of the document head script. Joe's onBlur link (to http://www.htmlgoodies.com/jsp/hgjsp_4.html) in the script deconstruction's 2nd <li> is broken and in any case points to the wrong primer; the onBlur event handler was discussed in Primer #5, and we treated it here in Blog Entry #9.

(2) When validfn(fnm) is called, the user's (non)input, fn.value, is passed to validfn(fnm) and assigned to the variable fnm. We for our part first passed a text object value to a function in Blog Entry #34; Joe for his part did so in his Primer #28 Assignment answer script. I was unaware that an object, fn in this case, can be self-referenced with its name - I would have used the "this" keyword, i.e., onblur="validfn(this.value);" - but Joe's demo works OK, so fair enough.

Moving now to the validfn(fnm) function:

(3) The fnlen=fnm.length command line assigns the value of the length property of fnm - as noted above, fnm=fn.value is a string, an empty string ("") to be precise, although we noted in the "Literals and strings" section of Blog Entry #33 that an empty string is still a string - to the variable fnlen.

(4) The subsequent if statement:

if (fnlen == 0) {
alert("First name is required"); document.dataentry.fn.focus( ); }

then compares fnlen and 0, asking, "Are they equal?" The if condition returns true, so a "First name is required" alert( ) message pops up; after the user clicks the "OK" button on the alert( ) box, a document.dataentry.fn.focus( ) command returns focus to the "Enter First Name" field in most cases. (On my computer when using Netscape 7.02, if fn's original focus is blurred via the tab key, then the document.dataentry.fn.focus( ) command does not reroute the transfer of focus from fn to the "Enter Zip Code" field.)

(5) The if conditional does not have an else part, and no commands execute (nothing happens) if the user does type something in the fn field.

Other code formulations are possible here, of course. For example, instead of testing the length of fnm, the user's input, we can compare in the if condition fnm itself with an empty string:

if (fnm == "") {alert("First name is required"); document.dataentry.fn.focus( );}

In this regard, we can even remove the validfn( ) function altogether if we recode the fn field as:

<input type='text' name='fn'
onblur='if (this.value == "") {alert("First name is required"); this.focus( );}'>

Let's turn now to the "Enter Zip Code" field. If we wanted to, we could use the approach(es) above to ensure that the zip field isn't left blank either, but we have bigger fish to fry this time...

The zip code field

In the primer concept, Joe lists two basic data validation objectives for an "Enter Zip Code" field:
(1) Lengthwise, the user should enter an input of 5 or 10 characters corresponding to a ##### or #####-#### zip code, respectively.
(2) Characterwise, the user's first five input characters - and to be sure, most users will enter a 5-digit and not a 9-digit zip code - should be numeric digits and not include letters, nonalphanumeric (!, @, #, etc.) characters, nor whitespace.

Let's set the stage, then, for some zip code validation that satisfies these objectives. Having entered a name into the "Enter First Name" field, the user navigates to the "Enter Zip Code (99999-9999)" field and enters a zip code and then clicks the Submit button, whose code is:

<input type="button" value="Submit" onclick="validZip(zip.value);">

Proceeding as we did above, the onclick="validZip(zip.value);" code triggers the validZip(zip) function in the document head script. The user's input, zip.value, is passed to validZip(zip) and assigned to the variable zip. Moving to the validZip(zip) function, the len=zip.length command line assigns the value of the length property of zip to the variable len.

Two lines down, an if statement then tests if len is both (a) not equal to 5 and (b) not equal to 10:

if (len != 5 && len != 10) {
alert("Zip is not the correct length"); document.dataentry.zip.focus( ); }

We've seen both the != comparison operator and the && logical/Boolean AND operator before. Joe first used the != operator in the Primer #26 Script (exasperatingly, he waits until the Primer #29 Assignment to comment directly on its meaning); the && operator briefly cropped up in the previous post.

If the user has entered a length-inappropriate zip code, then the if condition returns true, and a "Zip is not the correct length" alert( ) message pops up (contra the script deconstruction's 10th <li>, the alert( ) message does not announce that "the first five digits must be numeric"); after the user clicks the "OK" button on the alert( ) box, a document.dataentry.zip.focus( ) command returns focus to the "Enter Zip Code" field. One might expect validZip(zip) to plug its zip argument into the document.dataentry.zip.focus( ) command - such a substitution should throw an error, because the String object (zip is a string) does not have a focus( ) method - but this evidently doesn't happen.

Making sure that the user's first five characters are digits is a bit more involved. We begin by creating a character set of all ten digits - i.e., a set of the ten allowable numeric choices for each character that the user enters into the zip field - as a string and assigning it to the variable digits. The digits character set is declared locally on the second command line of the validZip( ) function, as shown above.

We then sequentially compare the first five characters of the user's zip input with digits via a for loop, which I've recast below in a somewhat expanded form:

for (i=0; i<5; i++) {
x=zip.charAt(i);
y=digits.indexOf(x);
if (y<0) {alert("First five digits must be numeric");
document.dataentry.zip.focus( ); break; } }

The loop runs for five iterations, specifically, for counter variable values of i=0, 1, 2, 3, and 4. The loop's increment-expression heralds the first use in the HTML Goodies JavaScript Primers series of the ++ increment operator, which Joe does not comment on but which we briefly discussed in Blog Entry #40.

The loop body introduces two new methods of the String object: charAt( ) and indexOf( ). Neither of these methods appears on the end-of-primer "Click Here For What You've Learned" page, but they are both listed in the HTML Goodies JavaScript methods keyword reference.

In JavaScript, "[t]he characters in a string are indexed from left to right with the first character indexed as 0 and the last as String.length-1," quoting DevGuru, and this indexing underlies both methods. The charAt(i) method takes an index number argument and returns the character corresponding to the ith position of the string on which it acts. Complementarily, the indexOf("some_string") method takes a string argument and returns (a) the index number of the first occurrence of "some_string" in the string on which it acts or (b) -1 if "some_string" is not found. Optionally, the indexOf("some_string", j) method can also take a second, index number argument that specifies a character position j at which the search for "some_string" begins.

Getting back to the expanded for loop above, suppose the user enters 92083 into the zip field; for the for loop's first iteration, we generate the following values of x and y:

For i=0: x=9 (9 is the 0th character in 92083), y=9 (9 appears at the 9th position in the digits character set);
Similarly:
For i=1: x=2, y=2;
For i=2: x=0, y=0; etc.

This brings us to the for loop's if statement; for y values of 9, 2, 0, 8, and 3, the if condition, y<0, is uniformly false and the browser thus skips over the subsequent if {commands}.

And what if the user's zip code were to contain one or more not-a-number characters? Ah, I'm sure you've got it sorted out...suppose the user enters 9w0u3 into the zip field. For the for loop's second iteration, x=w but y=-1 because w does not appear in the digits character set. The if condition returns true in this case, so a "First five digits must be numeric" alert( ) message pops up; after the user clicks the "OK" button on the alert( ) box, a document.dataentry.zip.focus( ) command returns focus to the zip field. Without comment, Joe also tacks on a break statement, which terminates the for loop and which we discussed in Blog Entry #40; without the break statement, a 9w0u3 zip code would generate two alert( ) messages, one for the second for iteration and one for the fourth for iteration.

In the actual script, Joe combines the charAt( ) method, the indexOf( ) method, and the if condition all on one command line:

if (digits.indexOf(zip.charAt(i))<0)

but the effect is the same as that described above.

Names without numbers

Had Joe declared the digits character set globally, then he could have used it to ensure that the user's "Enter First Name" input contains no numbers by inserting the following code in the validfn(fnm) function:

for (i=0; i<fnlen; i++) {
charac=fnm.charAt(i);
charindex=digits.indexOf(charac);
if (charindex != -1) {
window.alert("Names do not contain numbers! Please enter a proper name."); document.dataentry.fn.focus( ); break; } }

The if condition returns true if any of fnm's characters appears in digits.

Forms, controls, and names

Finally, a nitpicking comment on something Joe says in the last deconstruction <li>: "When you use forms with JavaScript, each form item must be given a name that links it to the sections of the JavaScript that will act upon it." Actually, we learned in Blog Entry #16 that names are unnecessary for the referencing of forms and their controls, and we can certainly use document.forms[0].elements[0] and document.forms[0].elements[1] to respectively reference the fn and zip fields if we so choose.

I've got a bit more to say about data validation and I'll do that in the next entry; we also may take a stab at applying the data validation techniques of regular expressions to the Primer #29 Script - stay tuned!

reptile7

Comments: Post a Comment

<< Home

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)