8.2. Performing Common Text Field Validations

NN 2, IE 3

8.2.1. Problem

You want to verify that a text box contains one of the following: any text, a number, a string of a fixed length, or an email address.

8.2.2. Solution

Apply the library of text field validation routines shown in the Discussion (Example 8-1 and Example 8-2) to your form. The library includes the following functions:

isNotEmpty( )

The field has one or more characters in it.

isNumber( )

The value is a number.

isLen16( )

The field contains exactly 16 characters.

isEMailAddr( )

The field contains a likely email address.

For real-time validation of text box entries, use an onchange event handler in the input element and pass a reference to the element by way of the this keyword. For example, the following input element could be used for an email address:

<input type="text" size="30" name="eMail" id="eMail" 
    onchange="isEMailAddr(this)" />

See Recipe 8.3 for an example of how these validation functions can be linked together in batch validation prior to submitting the form. The return values from the validation functions are vital for successful operation triggered by the form's onsubmit event handler.

8.2.3. Discussion

Example 8-1 shows a set of fully backward-compatible text validation functions. All of these functions are to be invoked by both the onchange event handler of the text box and a batch validation function triggered by the onsubmit event handler of the enclosing form. All functions are passed references to the form control invoking the event handler.

Example 1. Backward-compatible text field validation functions

// validates that the field value string has one or more characters in it
function isNotEmpty(elem) {
    var str = elem.value;
    if(str = = null || str.length =  = 0) {
        alert("Please fill in the required field.");
        return false;
    } else {
        return true;
    }
}
   
// validates that the entry is a positive or negative number
function isNumber(elem) {
    var str = elem.value;
    var oneDecimal = false;
    var oneChar = 0;
    // make sure value hasn't cast to a number data type
    str = str.toString( );
    for (var i = 0; i < str.length; i++) {
        oneChar = str.charAt(i).charCodeAt(0);
        // OK for minus sign as first character
        if (oneChar = = 45) {
            if (i = = 0) {
                continue;
            } else {
                alert("Only the first character may be a minus sign.");
                return false;
            }
        }
        // OK for one decimal point
        if (oneChar = = 46) {
            if (!oneDecimal) {
                oneDecimal = true;
                continue;
            } else {
                alert("Only one decimal is allowed in a number.");
                return false;
            }
        }
        // characters outside of 0 through 9 not OK
        if (oneChar < 48 || oneChar > 57) {
            alert("Enter only numbers into the field.");
            return false;
        }
    }
    return true;
}
   
// validates that the entry is 16 characters long
function isLen16(elem) {
    var str = elem.value;
    if (str.length != 16) {
        alert("Entry does not contain the required 16 characters.");
        return false;
    } else {
        return true;
    }
}
   
// validates that the entry is formatted as an email address
function isEMailAddr(elem) {
    var str = elem.value;
    str = str.toLowerCase( );
    if (str.indexOf("@") > 1) {
        var addr = str.substring(0, str.indexOf("@"));
        var domain = str.substring(str.indexOf("@") + 1, str.length);
        // at least one top level domain required
        if (domain.indexOf(".") = = -1) {
            alert("Verify the domain portion of the email address.");
            return false;
        }
        // parse address portion first, character by character
        for (var i = 0; i < addr.length; i++) {
            oneChar = addr.charAt(i).charCodeAt(0);
            // dot or hyphen not allowed in first position; dot in last
            if ((i = = 0 && (oneChar =  = 45 || oneChar =  = 46))  || 
                (i = = addr.length - 1 && oneChar =  = 46)) {
                alert("Verify the user name portion of the email address.");
                return false;
            }
            // acceptable characters (- . _ 0-9 a-z)
            if (oneChar = = 45 || oneChar =  = 46 || oneChar =  = 95 || 
                (oneChar > 47 && oneChar < 58) || (oneChar > 96 && oneChar < 123)) {
                continue;
            } else {
                alert("Verify the user name portion of the email address.");
                return false;
            }
        }
        for (i = 0; i < domain.length; i++) {
            oneChar = domain.charAt(i).charCodeAt(0);
            if ((i = = 0 && (oneChar =  = 45 || oneChar =  = 46)) || 
                ((i = = domain.length - 1  || i =  = domain.length - 2) && oneChar =  = 46)) {
                alert("Verify the domain portion of the email address.");
                return false;
            }
            if (oneChar = = 45 || oneChar =  = 46 || oneChar =  = 95 || 
                (oneChar > 47 && oneChar < 58) || (oneChar > 96 && oneChar < 123)) {
                continue;
            } else {
                alert("Verify the domain portion of the email address.");
                return false;
            }
        }
        return true;
    }
    alert("The email address may not be formatted correctly. Please verify.");
    return false;
}

Regular expression versions of the validation functions are more compact when the validation is complex (as in the case of email addresses), but they require great care and stress testing to make sure they are doing what you expect. Example 8-2 shows the equivalent validation functions using regular expressions.

Notice that the validation done in these functions provides the user with less detailed information about the more complex data entries than the backward-compatible versions. It is possible to provide more information, but this involves pulling apart the regular expressions to test for subsets of matches. In the first one, isNotEmpty( ), the regular expression pattern looks for a string with one or more characters of any kind (.+). To test for a number in isNumber( ), the pattern looks for a string that begins with (^) zero or one minus signs ([-]?), followed by zero or more numerals (\d*), zero or one decimals (\.?), and zero or more numerals (\d*) on the tail end ($). A fixed-length string pattern in isLen16( ) looks for word boundaries on both ends (\b) and any characters (.) appearing 16 times ({16}). To ensure the user keeps to the 16-character length, limit the text-type input element to a maximum length of 16.

The gnarled email pattern inside isEMailAddr( ) looks for a match that begins (^) with one or more letters, numerals, underscores, or hyphens ([\w-]), followed by zero or more combinations of a period, letter, numeral, underscore, or hyphen ((\.[\w-]+)*), followed by the @ sign, followed by one or more computer or domains (([\w-]+\.)+), followed by two to seven upper- or lowercase letters for the top-level domain name ([a-zA-Z]{2,7}) on the tail end ($). The pattern does not match the use of straight IP addresses for the portion after the @ sign, but the email message specification (Internet Engineering Task Force RFC 822) frowns on such usage anyway.

In addition to individual validation routines, you sometimes need to cascade them. For example, none of the functions that validate numbers, fixed-length strings, or email addresses perform any checking that assures the field has something in it. For example, if the email address field is a required field in the form, you would wire the onchange event handler for that input element to pass the values first to the isNotEmpty( ) function and then the isEMailAddr( ) function—but in such a way that if the first fails, the second one does not execute. That's where the returned Boolean values of the functions come into play:

<input type="text" size="30" name="eMail" id="eMail" 
    onchange="if (isNotEmpty(this)) {isEMailAddr(this)}" />

Not shown among the text field validation routines here is one that validates a date entry. Validating date entries is tricky business due to the wide range of date formats and sequences of numbers used around the world. Except for intranet application where everyone standardizes on a single date format, I recommend implementing date input as three distinct fields (or select elements) for entry of month, date, and year. Use the onsubmit event handler to combine the values into a single string for a hidden input element whose value the server can pass directly to the database. You can also use the pop-up calendar shown in Recipe 15.9 to help a user select a date, leaving the formatting up to your scripts. Or, if all of your users follow a fixed date format, you can try the date validation techniques described in Recipe 2.12.

8.2.4. See Also

Recipe 8.3 for a batch validation structure that uses the functions just described; Recipe 8.4 for automatically focusing and selecting a text field that fails validation; Recipe 2.12 for date validation ideas.