reptile7's JavaScript blog
Tuesday, February 22, 2011
 
Further Adventures in Form Validation, Part 5
Blog Entry #206

Up to this point I have diplomatically refrained from making any editorial comments about HTML Goodies' "Bring Your Forms to Life With JavaScript" tutorial and its scripts - we'll take the gloves off today.

Over the course of the tutorial, the author waxes enthusiastic about the use of a custom JavaScript object to validate the fields of the liveForm form; for example, at the bottom of the tutorial's first page, he says, [I]t would be useful to have a JavaScript object that allows us to validate all our forms without having to rewrite code every time we create a new form. I suppose I would agree that the liveforms/ package is impressive vis-à-vis the breadth of the features it employs - we sure have slogged through quite a range of code in the last few entries, haven't we? - but if truth be told I am rather underwhelmed by it all. Indeed, there is such a disconnect between the complexity of the tutorial scripts and the simple validations they ultimately carry out that I almost wonder if the author is playing some sort of joke on the WebReference.com/HTML Goodies readership. The whole shebang cries out for an overhaul...

Image visualization/preloading

Let's begin with the check.gif/x.gif images. I see no point in creating new <img src="check.gif"> and <img src="x.gif"> elements every time we blur one of the liveForm form's required fields. We'll be inserting these images come what may, so a much more sensible approach to image visualization would be to (a) hard-code an img placeholder after each required field

<p>Your first name: *<br /><input type="text" id="firstName" name="firstName" onblur="testField(this);" />
<img id="firstNameImage" class="imageClass" src="" alt="" /></p>
<!-- For ease of referencing, we will give each placeholder a selector.name+"Image" id value. -->


(b) initially zero out the placeholders by setting their CSS display properties to none

.imageClass { display: none; }

(c) visibilize each placeholder later by JavaScriptically switching its display value to inline

var selectorImage = document.getElementById(selector.name + "Image");
selectorImage.style.display = "inline";


and finally (d) set each placeholder's src property to check.gif or x.gif, as appropriate.

So much for the utils.js script's ce( ), rc( ), and InsertAfter( ) functions. Nor am I convinced that Validator's preload( ) method actually preloads the check.gif/x.gif images as, again, the currentSelector.validImage and currentSelector.invalidImage objects are created on the fly. I'm not sure these images need to be preloaded as they're only 4 KB each, but if you want to do so just to be on the safe side, then the following statements, to be executed as top-level (unfunctionized) code in the document head, will do the trick:

var checkImage = new Image(19, 19);
checkImage.src = "img/check.gif";
var xImage = new Image(20, 21);
xImage.src = "img/x.gif";


Field validation

Inspection of the Validator.js code prompts a fundamental question - you're thinking it, I'm thinking it too:

"Where are the regular expressions?"

Regular expressions can and should be used to validate the firstName, lastName, email, and zip fields in the liveForm form. We previously discussed the regular expression-based validation of names, email addresses, and ZIP codes in Blog Entries #48, #50, and #49, respectively.

firstName, lastName

For the firstName/lastName fields, we definitely don't need to settle for a selector.value test that merely checks if they are blank or not. For example, you can limit these fields' inputs to alphabetic characters and require them to respectively contain at least one vowel via the validCondition below:

var normalName = /^[a-z]+$/i;
var vowel = /[aeiouy]/i;
var validCondition;
if (selector.name == "firstName" || selector.name == "lastName")
    validCondition = normalName.test(selector.value) && vowel.test(selector.value);


You might want to set the bar lower here - after all, names can contain white space, hyphens, apostrophes, etc. (within reason, they can even legitimately contain digits and symbolic characters) - however, it is reasonable to require the firstName/lastName inputs to contain some (one or more) alphabetic characters, in which case the above validCondition can be simplified to:

validCondition = /[a-z]+/i.test(selector.value);

email

Borrowing from another WebReference.com resource - the "A Feedback Form" page of WebReference.com's "JavaScript Regular Expressions" tutorial - we can duplicate the various tests of Validator's validateEmail( ) method (all eleven of them) with the following four lines of code:

var emailOK = /^\S+@[a-z0-9.-]+\.[a-z]{2,4}$/i;
var emailNotOK = /(@.*@)|(\.\.)|(@\.)|(\.@)|(^\.)/;
if (selector.name == "email")
    validCondition = emailOK.test(selector.value) && !emailNotOK.test(selector.value);


The emailOK pattern has been adapted from the reg2 pattern on the "Feedback Form" page. The original reg2 permits white space in the local-part of an email address and does not accommodate four-letter top-level domains, so I've modified those parts of the pattern; less importantly, I've also subtracted the reg2 clauses that pertain to email addresses with IP addresses (vis-à-vis domain names) after the @ separator and have made a few other cosmetic changes. The also-renamed emailNotOK pattern, which in collaboration with the ! operator is used to exclude several types of invalid email addresses (those with two or more @ characters, those with consecutive 'dots', etc.), was taken verbatim from the "Feedback Form" page.

zip

This is an easy one:

var zc = /^\d{5}$|^\d{5}-\d{4}$/; // Matches a ZIP or ZIP+4 code
if (selector.name == "zip")
    validCondition = zc.test(selector.value);


There's no need at all for those bizarre maxLength-based tests - chuck them out.

street, state

There's far too much variation in street addresses to write a meaningful regular expression for them, so for the street text field and also the state selection list we will stick with the 'don't leave it blank' validation of the original code:

if (selector.name == "street" || selector.name == "state")
    validCondition = selector.value;


An historical note

We first discussed form field validation in Blog Entry #46 in the course of working through HTML Goodies' JavaScript Primers #29. Somewhat like "Bring Your Forms to Life With JavaScript", Primer #29 presents a script that validates "Enter First Name:" and "Enter Zip Code (99999-9999):" text fields. Without getting into the details, Primer #29's "First Name" validation, which tests if the field is blank or not, is equivalent to what the Validator.js script does for the firstName, lastName, street, and state fields but is carried out in a much more straightforward manner, whereas its "Zip Code" validation, which allows the entry of ZIP+4 codes and actually checks if the first five input characters are digits, is distinctly superior to what Validator.js does for the zip field. Take a bow, Andree and Joe.

Form validation and related functionality

For validating the liveForm form as a whole, we'll still need fieldNumValidated and fieldNumToValidate, which can be defined as 'freestanding' variables (OK, I recognize that this effectively makes them properties of the window object):

var fieldNumValidated = 0, fieldNumToValidate = 6;

We'll also still need to equip the to-be-validated selector with an isValid property:

if (!selector.isValid) selector.isValid = false;

We can now condense Validator's valid( ) and AllFieldsValidated( ) methods to an if block:

if (validCondition) {
    selectorImage.src = checkImage.src;
    if (!selector.isValid) fieldNumValidated++;
    selector.isValid = true;
    if (fieldNumValidated == fieldNumToValidate) document.getElementById("submit").disabled = false; }


Validator's invalid( ) method can similarly be replaced by a complementary else clause. Most of this if...else code can be further compacted via the conditional (?:) operator:

selectorImage.src = validCondition ? checkImage.src : xImage.src;
if (validCondition && !selector.isValid) fieldNumValidated++;
else if (!validCondition && selector.isValid) fieldNumValidated--;
selector.isValid = validCondition ? true : false;
document.getElementById("submit").disabled = (fieldNumValidated == fieldNumToValidate) ? false : true;


We can use an onload-triggered function expression to initially (a) disable the submit button and (b) clear the liveForm form:

window.onload = function ( ) {
    document.getElementById("submit").disabled = true;
    document.getElementById("liveForm").reset( ); }


For MSIE users, we can choke off the submit-the-form-using-enter effect that was discussed at the end of the previous post via a separate onsubmit-triggered function expression:

document.getElementById("liveForm").onsubmit = function ( ) {
    if (fieldNumValidated != fieldNumToValidate) {
        window.alert("Your required data is not complete."); return false; }
    else window.alert("Thank you!"); }


Demo

The div below holds a liveForm-validation demo that incorporates the code presented in this entry. For each of the demo text fields, feel free to enter whatever you want, and then blur the field (i.e., click outside the field or tab to the next field).

* = required field
Registration form
Your first name: *


Your last name: *


Your email: *


Company:

Street address: *

State: *


Zip code: *


The demo form's action attribute is set to an empty string and its method attribute has been toggled to get, so you may safely click the button once all of the required fields have been filled out correctly - your data will be submitted to this page and will be appended as a query string to the page's URL.

We're not 100% finished with "Bring Your Forms to Life With JavaScript": in the name of completeness, I would still like to discuss (1) the non-default clauses of the first switch statement of Validator's Validate( ) method and (2) the utils.js ac( ) function, and I'll do so in the following entry.

reptile7

Thursday, February 10, 2011
 
Further Adventures in Form Validation, Part 4
Blog Entry #205

Before we get started...

In the previous entry, I provided links to Mozilla's pages for several DOM features employed by the utils.js script but negligently didn't say anything about the history of those features, so let me do so now. The parentNode property, the removeChild( ) method, the insertBefore( ) method, and the nextSibling property all go back to the DOM Level 1 Core and are all part of the DOM's Node interface. According to irt.org, support for these features began on the Microsoft side with MSIE 5 and on the Netscape/Mozilla side with Netscape 6, strongly suggesting that they were introduced by the W3C and were not previously implemented proprietarily. Working in the SheepShaver environment, I can confirm that MSIE 5 and Netscape 6 both run the tutorial scripts (upon a bit of code cleanup with the former, as-is with the latter) but MSIE 4.5 and Netscape 4.61 do not.

At this point in our discussion of HTML Goodies' "Bring Your Forms to Life With JavaScript" tutorial, we have effectively validated most of the required fields in the liveForm form. The validations of the firstName text field, the lastName text field, the street text field, the state selection list, and the zip text field are all handled by the default clause of the first switch statement of the Validator object's Validate( ) method, which we dissected in detail two entries ago. In today's post we turn our attention to the remaining elements[2] email field

<p>Your email: * <br /><input type="text" id="email" name="email" onblur="Validator.Validate(this, 'email');" /></p>

which is custom-validated by a validateEmail( ) method in the Validator.js script - after all, for a "Your email: *" field we really ought to be setting the bar a bit higher than merely checking if the field is blank or not, yes? (OK, we did at least require the zip field to contain five characters, but that's not exactly setting the bar high either.)

We begin by entering an email address - say, kris@krishadlock.com - into the email field. We tab to the elements[3] company field, giving rise to a blur event, which calls Validator's Validate( ) method.

Validator.Validate = function(selector, inputType) { ... }

Like the other required fields, the email field passes to Validate( ) a this self-reference, which is again renamed selector; unlike the other required fields, the email field feeds to Validate( ) a second parameter, namely the string email, which is given an inputType identifier.

All of Validate( ) except for the concluding if (isEmpty) this.invalid( ); else this.valid( ); conditional applies to the email field. À la the other required fields:
(1) this.currentSelector = selector; associates the email field as a child object with the Validator object.
(2) Validator's preload( ) method is called to create objects/elements for the check.gif and x.gif images and also to define an isValid property for the email field.
(3) The isEmpty variable is initialized to true.
(4) Assuming that we are using a browser for which selector.maxLength defaults to -1 (e.g., Firefox, Opera), isEmpty is toggled to false by the default clause of Validate( )'s first switch statement.

Validate( )'s first switch statement is followed by another switch statement that is specifically meant for the email field:

switch(inputType) {
    case 'email':
        this.validateEmail( );
        return; }


This switch statement comprises a single case clause with an email label. The statement's controlling expression is inputType, which evaluates to email for the email field (vide supra) and to undefined for the other required fields; as a result, the statement's email clause fires for the email field and only for that field. Unlike the first switch statement, the second switch statement doesn't have a default clause, but if it did, then that clause would fire for the other required fields.

A related aside:
The first switch statement's first case clause is case 'undefined': break; - if it were present in the second switch statement, this clause would not fire for the non-email required fields because the undefined label is specified as a string.

As shown above, the email clause calls a validateEmail( ) method and then induces an exit from the Validate( ) method via a return; statement. (Mozilla's return statement page strangely now makes no mention of the use of return to stop the execution of a function, but à la Microsoft's corresponding page.)

Validator.validateEmail = function( ) { ... }

validateEmail( ) is the third method defined for Validator in the Validator.js script; it probes the email field's value and then calls Validator's valid( ) or invalid( ) method if that value is perceived to be valid or invalid, respectively. validateEmail( ) begins with a series of variable declarations:

var str = this.currentSelector.value;
var at = "@";
var dot = ".";
var lat = str.indexOf(at);
var lstr = str.length;
var ldot = str.indexOf(dot);


These variabilizations are followed by a series of if statements meant to flag various types of invalid email addresses. Here's the first of these statements:

if (str.indexOf(at) == -1) {
    this.invalid( );
    return false; }


The str.indexOf(at) == -1 condition flags email addresses lacking an @ separator, more specifically, it runs through the characters of str, to which this.currentSelector.value (the email field value) was assigned above, and returns true if @ (variabilized as at) isn't one of them, in which case the statement calls Validator's invalid( ) method. Subsequently, validateEmail( )'s execution is terminated via a return false; statement, the false part of which is unnecessary (recall that all of this action was triggered by a blur event, which is not cancelable).

The next if statement

if (str.indexOf(at) == -1 || str.indexOf(at) == 0 || str.indexOf(at) == lstr) {
    this.invalid( ); return false; }


sports three subconditions (there's no need to parenthesize these subconditions as the equality operators have a higher precedence than does the logical-OR operator):
(a) str.indexOf(at) == -1 is tested again and necessarily returns false (if this expression returned true for the first if statement, then we won't even reach the second if statement as we will have exited validateEmail( ));
(b) str.indexOf(at) == 0 flags email addresses beginning with an @;
(c) str.indexOf(at) == lstr would flag email addresses ending with an @ had the lstr variable been set to str.length - 1 and not str.length, which indexOf( )-wise is one position beyond the end of str, and thus this subcondition necessarily returns false for all email inputs irrespective of the @ character.

Anyway, if the middle subcondition returns true, then this.invalid( ) is called and validateEmail( ) is exited. (In stringing together || operations in this manner, as long as one of the operands returns true, then the overall expression will return true.)

Analogously, the third if statement

if (str.indexOf(dot) == -1 || str.indexOf(dot) == 0 || str.indexOf(dot) == lstr) {
    this.invalid( ); return false; }


catches email addresses lacking a dot (a period/full stop character) or beginning with a dot, but not ending with a dot.

The fourth if statement catches email addresses containing two or more @ characters - the lat variable represents the location of the at separator:

if (str.indexOf(at, (lat + 1)) != -1) {
    this.invalid( ); return false; }


The fifth if statement catches email addresses with a dot immediately before or after the at separator:

if (str.substring(lat - 1, lat) == dot || str.substring(lat + 1, lat + 2) == dot) {
    this.invalid( ); return false; }


The sixth if statement catches email addresses that don't have a dot after the lat + 1 position:

if (str.indexOf(dot, (lat + 2)) == -1) {
    this.invalid( ); return false; }


A final if statement flags email inputs containing one or more space characters:

if (str.indexOf(" ") != -1) {
    this.invalid( ); return false; }


(The @-preceding local-part of an email address can be in the form of a quoted-string that can circumvent some of these if 'prohibitions' - e.g., "abc@. xyz"@example.net is a valid email address according to the ABNF productions in the IETF's "Internet Message Format" specification - but I have to say that I've never seen this type of email address in real life.)

If all of the above if conditions return false, then the email field's value is deemed to be valid and Validator's valid( ) method is called via a final

this.valid( );

command. You can now see why all those return [false] statements are necessary: without them, this last line will also be executed if any of the if conditions are true, in which case a false positive results, i.e., the check.gif image is placed next to the email field even though the field's value is invalid. Alternatively, formulating validateEmail( )'s conditional code as an if...else if...else cascade

if (str.indexOf(at) == -1) this.invalid( );
else if (str.indexOf(at) == 0 || str.indexOf(at) == lstr - 1) this.invalid( );
else if (str.indexOf(dot) == -1 || str.indexOf(dot) == 0 || str.indexOf(dot) == lstr - 1) this.invalid( );
...
else this.valid( );


would allow us to throw out the return statements. Moreover, there's nothing stopping us from combining the various subconditions to give one giant a || b || c || ... condition (each || operand could be placed on its own line for the sake of readability) for which only one this.invalid( ) call would be needed.

Form submission

If we have correctly filled out all of the required fields in the liveForm form - if Validator's fieldNumValidated property has incremented to 6 and its AllFieldsValidated( ) method returns true - then the following line in Validator's valid( ) method

gebid(this.submitId).disabled = false;

sends to the utils.js gebid( ) function this.submitId, which was set to submit, the id of the form's submit button, by Validator's Initialize( ) method; gebid( ) then 'gets' the button so that it can be activated by setting its DOM disabled property to false.

The liveForm form is equipped with an

onsubmit="if(!Validator.AllFieldsValidated()) return false;"

event handler that might seem redundant at first glance - if the button has been activated, then we shouldn't need to check in with the AllFieldsValidated( ) method again, right? But there is actually a use for the onsubmit attribute: upon removing it, I find when using MSIE* that entering a value into any of the liveForm form's text fields (including the non-required company field) and hitting the enter/return key causes the form to prematurely submit - we previously encountered this phenomenon in HTML Goodies' "Submit The Form Using Enter" tutorial - a call to AllFieldsValidated( ) and a false return will cancel that submission.

*With every other OS X GUI browser on my computer, the submit-the-form-using-enter effect is suppressed if there's more than one text field in the form, for whatever reason.

We'll wrap up our look at "Bring Your Forms to Life With JavaScript" with some concluding commentary in the following entry.

reptile7


Powered by Blogger

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