reptile7's JavaScript blog
Monday, January 31, 2011
Further Adventures in Form Validation, Part 3
Blog Entry #204

We continue today our deconstruction of the Validator.js/utils.js scripts of HTML Goodies' "Bring Your Forms to Life With JavaScript" tutorial. In this post we'll get into the details of placing the check.gif and x.gif images respectively next to correctly and incorrectly filled-out fields in the liveForm form. The code described below works A-OK with all of the OS X GUI browsers on my computer.

Sticking with the document.forms["liveForm"].firstName.value = "Kris" example of the last couple of entries, let's say we are surfing with Firefox, for which the Kris input is valid. For all of the liveForm form's required fields excepting the email field, a valid input is signaled by a false value for the isEmpty variable of the Validator object's Validate( ) method. As noted at the end of the previous entry, the isEmpty=false value triggers a valid( ) method via an if (isEmpty) this.invalid( ); else this.valid( ); conditional at the end of the Validate( ) method.

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

valid( ) is the fourth method defined for Validator in the Validator.js script; it keeps tabs on the validation state of a given field and of the liveForm form in general. valid( )'s underlying function kicks off by calling the utils.js script's rc( ) function


and passing thereto this.currentSelector.invalidImage, which points to the object representing the <img src="x.gif"> element created earlier by the utils.js ce( ) function, and which is given a node identifier by the rc( ) declaration. The rc( ) function doesn't do anything in this run of the script but let's look at it briefly anyway:

function rc(node) {
    if (node != null) {
        try {
            node.parentNode.removeChild(node); }
        catch(err) {
            /* no node */ } } }

For the valid( ) method, the rc( ) function removes a previously inserted x.gif image upon changing an invalid field input to a valid field input; complementarily, Validator's invalid( ) method uses rc( ) to remove the check.gif image upon changing a valid field input to an invalid field input. rc( ) works its magic via the DOM's removeChild( ) method, which removes a child node from a parent node.

At this stage, this.currentSelector.invalidImage/node is a free-floating object that is not part of the index.html DOM tree and thus doesn't have a parentNode property; in this case, rc( )'s node.parentNode.removeChild(node); command would ordinarily throw a node.parentNode is null TypeError, which is preempted by placing the command in the try clause of a try...catch statement. (I speculated two entries ago that it wasn't necessary for utils.js's gebid( ) function to have a try...catch statement, but rc( ) definitely needs one.) The if (node != null) { ... } conditional wrapper strikes me as redundant, or at least I can't think of any contingencies in which node would be null, but if it ever were, then the try...catch statement could take care of the situation.

Moving back to the valid( ) function, the rc( ) function call is followed by a call to utils.js's InsertAfter( ) function:

InsertAfter(this.currentSelector.parentNode, this.currentSelector.validImage, this.currentSelector);
function InsertAfter(parent, node, referenceNode) {
    parent.insertBefore(node, referenceNode.nextSibling); }

InsertAfter( ) is the function that actually places the check.gif image next to the Kris-holding firstName field. The InsertAfter( ) function call sends three parameters to InsertAfter( ):
(1) this.currentSelector.parentNode, which points to the p element parent

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

of the firstName field;
(2) this.currentSelector.validImage, which points to the object representing the <img src="check.gif"> element created earlier by the utils.js ce( ) function; and
(3) this.currentSelector, which points to the firstName field (more specifically, to the field's underlying input element).
InsertAfter( ) gives to these parameters parent, node, and referenceNode identifiers, respectively.

InsertAfter( )'s insertBefore( ) command inserts the node image as a child into the calling parent paragraph just before the referenceNode field's nextSibling. As shown above, the referenceNode field doesn't actually have a nextSibling and consequently the referenceNode.nextSibling expression returns null; as a result, the node image is placed at the end (i.e., becomes the last child) of the parent paragraph. Got all that?

Moving back to the valid( ) function once again, the InsertAfter( ) function call is followed by a conditional that increments the value of the fieldNumValidated property of the Validator object:

if (!this.currentSelector.isValid) {
    this.fieldNumValidated++; }

Validator.currentSelector.isValid was earlier initialized to false by Validator's preload( ) method, and therefore the !this.currentSelector.isValid if condition returns true.
Validator.fieldNumValidated was earlier initialized to 0 by Validator's Initialize( ) method, so it's 1 now.

The preceding conditional is followed by another conditional that takes stock of where we are vis-à-vis validation of the form as a whole:

if (Validator.AllFieldsValidated( )) {
    gebid(this.submitId).disabled = false; }

This conditional does or does not activate the liveForm form's submit button depending on the true/false return of Validator's AllFieldsValidated( ) method, which is called by the if condition.

Validator.AllFieldsValidated = function(override) {
    if (this.fieldNumValidated >= this.fieldNumToValidate || override) return true;
    else return false; }

AllFieldsValidated( ) is the seventh (last) method defined for Validator in the Validator.js script; it compares the values of Validator's fieldNumValidated and fieldNumToValidate properties, returning true if the former is greater than or equal to the latter and false otherwise. (Recall that Validator.fieldNumToValidate represents the total number of fields in the liveForm form that must be filled out correctly, and was earlier set to 6 by Validator's Initialize( ) method.)

"What's with the override argument?"

Your guess is as good as mine. AllFieldsValidated( ) is called at two points in the tutorial code: (1) by Validator's valid( ) method and (2) by the liveForm form's onsubmit event handler:

<form id="liveForm" action="" method="post"
onsubmit="if (!Validator.AllFieldsValidated( )) return false;">

In neither case is a parameter passed to AllFieldsValidated( ). However, a detailed look at the this.fieldNumValidated >= this.fieldNumToValidate || override AllFieldsValidated( ) if condition shows that override, which evaluates to undefined in that condition, does not interfere with what AllFieldsValidated( ) is meant to do: green-light or red-light either the activation of the button or the submission of the liveForm form.

Relational operators have a higher precedence than does the logical-OR operator, so the this.fieldNumValidated >= this.fieldNumToValidate operation is carried out first. If we've only filled out the firstName field, then this comparison (1 >= 6) returns false; the overall condition now simplifies to a false || undefined operation. The || operator returns its second operand if its first operand cannot be converted to true, and therefore false || undefined returns undefined. As an if condition, undefined converts to false and consequently the else clause returns false to the AllFieldsValidated( ) function call.

Conversely, if we've correctly filled out all of the required fields in the liveForm form, then this.fieldNumValidated will have incremented to 6 and the this.fieldNumValidated >= this.fieldNumToValidate comparison will return true. A true || undefined operation returns true and consequently the if clause returns true to the AllFieldsValidated( ) function call.

The valid( ) function concludes with the following statements:

this.currentSelector.isValid = true;
this.currentSelector.validated = true;

For our ongoing Kris/firstName example, the first assignment toggles this.currentSelector.isValid to true (it would reassign true to this.currentSelector.isValid if we were changing a valid input to another valid input). The second assignment defines for the firstName field a custom validated property, setting it to true; no subsequent use is made of this property.

It's not valid

Now, what if we were surfing with Safari 5.0.3, for which the Kris/firstName input is not valid? In this case the Validate( ) method calls Validator's invalid( ) method:

Validator.invalid = function( ) {
    InsertAfter(this.currentSelector.parentNode, this.currentSelector.invalidImage, this.currentSelector);
    if (this.currentSelector.isValid) {
        this.fieldNumValidated--; }
    gebid(this.submitId).disabled = true;
    this.currentSelector.isValid = false;
    this.currentSelector.validated = true; }

invalid( ) is the fifth method defined for Validator in the Validator.js script; you should be able to handle its deconstruction as it complements the valid( ) method. Again, it is the utils.js InsertAfter( ) function that will place the x.gif image next to the firstName field.

In the following entry we will check over the Validator.js code that validates the liveForm form's email field.


Saturday, January 22, 2011
Further Adventures in Form Validation, Part 2
Blog Entry #203

We return now to our analysis of HTML Goodies' "Bring Your Forms to Life With JavaScript" tutorial. All of the liveForm form's required fields excepting the email field are validated via a common script pathway, which we'll deconstruct in detail in today's post. In the discussion that follows, we will for now leave to the side the questions "Is this really necessary?" and "Isn't there a much more straightforward way to do this?" and simply state what happens.

A pure-HTML version of the liveForm form appears in "listing 1" on the tutorial's first page; the complete liveForm form plus the rest of the index.html code are reproduced in the first textarea field on the tutorial's third page. The liveForm form's fields can be filled out in any order, so let's begin with the elements[0] firstName field

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

whose label asks us to input a first name. We enter Kris into the firstName field and then tab to the elements[1] lastName field, giving rise to a blur event, which calls the Validate( ) method of the custom Validator object and passes thereto this, an object reference for the firstName field.

Validate( ) is the second method defined for Validator in the Validator.js script file, whose code is reproduced in the textarea field at the end of the tutorial's second page; per its name, Validate( ) will determine whether the firstName field's value is or is not valid. Validate( )'s underlying function is declared to accept two arguments:

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

The this object is assigned to the arguments[0] identifier, selector.

The Validate( ) function first assigns the selector object to Validator's currentSelector property, which was declared earlier by Validator's Initialize( ) method. Next, the function calls and passes selector to Validator's preload( ) method.

this.currentSelector = selector;
Validator.preload = function(selector) { ... }

preload( ) is the sixth method defined for Validator in the Validator.js script file. According to the author, preload( ) preloads the valid and invalid images for the current selector when the Validate( ) method is called; this helps the image show up faster when called upon. preload( ) first checks if selector is convertible to true and then reassigns selector to Validator.currentSelector:

if (selector) {
    Validator.currentSelector = selector;

Both of these operations are unnecessary (selector would be undefined, and hence convertible to false, had we forgotten the this parameter in the Validate( ) method call, but it is of course the author's responsibility to not make these kinds of mistakes). preload( ) next sets up validImage and invalidImage properties for Validator.currentSelector that will respectively point to objects representing img elements for the check.gif and x.gif images. Towards this end, preload( ) initially tests if Validator.currentSelector doesn't have validImage and invalidImage properties (note the ! operators in the if condition):

if (!Validator.currentSelector.validImage && !Validator.currentSelector.invalidImage) { ... }

Validator.currentSelector at present has the various standard properties of a text box object (e.g., name, type, value) but we haven't given it any custom properties, and therefore the if condition returns true (also recall that the Initialize( ) method earlier equipped Validator but not Validator.currentSelector with validImage and invalidImage properties). preload( ) subsequently calls the utils.js script file's ce( ) function, which will create from scratch image objects/elements for the check.gif and x.gif images.

The complete utils.js script code should be reproduced in the second textarea field on the tutorial's third page (just before the Conclusion). However, someone at HTML Goodies forgot to escape the < character of the ac( ) function's for loop, and consequently the browser aborts the code at that point, i.e., the < and everything that follows it do not appear in the field; this mistake does not appear in the version of the tutorial (strangely, the below-the-first-textarea-field content of the third page of the version of the tutorial is now gone altogether).

The ce( ) function is called by the following statements:

Validator.currentSelector.validImage = ce('img', {src: Validator.validImage});
Validator.currentSelector.invalidImage = ce('img', {src: Validator.invalidImage});

Here's ce( ) itself:

function ce(e, obj) {
    var a = document.createElement(e);
    for(prop in obj) {
        a[prop] = obj[prop]; }
    return a; }

Each ce( ) function call sends two parameters to ce( ):
(1) the string img, which is given an e identifier, and
(2) a {src: Validator.validImage} or {src: Validator.invalidImage} Object object, which is given an obj identifier; these objects are created via the object initializer syntax and have a single src property respectively set to Validator.validImage and Validator.invalidImage, which were respectively set to img/check.gif and img/x.gif by Validator's Initialize( ) method.

ce( ) first plugs e into a document.createElement( ) command to create an image object/element (more specifically, object implementing the DOM's Element interface), which is given an a identifier. The createElement( ) method goes back to the DOM Level 1 Core; MSIE has supported it since MSIE 4 and I suspect it was Microsoft (vis-à-vis the W3C) that first implemented it.

ce( ) next transfers the obj object's src property, including the value of that property, to the a object via a loop - the loop runs for one iteration in which the prop variable is set to src as a string.

Finally, ce( ) returns the a object to the preload( ) method, which assigns it to the heretofore-undefined validImage or invalidImage property of Validator.currentSelector.

We're not done with preload( ) yet. preload( ) concludes with a conditional

if (Validator.currentSelector.isValid == undefined) {
    Validator.currentSelector.isValid = false; }

that tests if an isValid property has not been set for Validator.currentSelector (it hasn't) and then initializes Validator.currentSelector.isValid to false. When using MSIE 5.2.3, the Validator.currentSelector.isValid == undefined condition throws an 'undefined' is undefined runtime error; changing the condition to !Validator.currentSelector.isValid quashes the error.

Moving back to the Validate( ) function, the preload( ) function call is followed by the declaration of an isEmpty variable, which is initialized to true, and then by two switch statements:

var isEmpty = true;
switch (selector.type) { ... }
switch (inputType) { ... }

We previously encountered the switch statement in HTML Goodies' "A Case for A Case" tutorial, which we deconstructed in Blog Entry #116. The first Validate( ) switch statement applies to all of the liveForm form's required fields whereas the second Validate( ) switch statement applies only to the email field. The first switch statement comprises four case clauses, whose labels are undefined, radio, select-multiple, and checkbox, respectively, and a concluding default clause. The statement's controlling expression is selector.type, which evaluates to text for all of the required text fields (firstName, lastName, email, street, zip) and to select-one for the state selection list, and therefore the statement's default clause is the one that fires.

    if (selector.value) {
        // safari 3.0 = 1024
        // IE 7.0 = 2147483647
        if (selector.maxLength > 0 && !(selector.maxLength == 1024) && !(selector.maxLength == 2147483647)) {
            if (selector.value.length == selector.maxLength) {
                isEmpty = false; } }
        else {
            isEmpty = false; }

(As the liveForm form does not contain radio buttons, a multiple selection list, or checkboxes, it is left to the reader to examine the clauses for these control types. However, we might take up the radio case later, as the reference expressions in this clause are incorrect.)

The default clause comprises an if statement that toggles isEmpty to false if the selector to be validated has not been left blank; the selector.value condition converts to true for all non-empty user inputs. The if statement itself comprises a nested if...else statement whose if clause probes the zip field

<p>Zip code: * <br /> <input type="text" id="zip" name="zip" maxlength="5" onblur="Validator.Validate(this);" /></p>

and whose else clause takes care of the remaining required fields.

The selector.maxLength > 0 && !(selector.maxLength == 1024) && !(selector.maxLength == 2147483647) condition deserves special comment. This condition does not directly flag the zip field but rather excludes the other required fields on the basis of the values of their DOM maxLength properties.

According to the HTML 4.01 Specification, The default value for [the HTML maxlength] attribute is an unlimited number. In practice, maxLength's default value is not at all infinite (although it could be) and is highly browser-dependent: on my computer, maxLength defaults to -1 with Firefox, Opera, Camino, and Netscape 9; to 524288 (219) with Safari 5.0.3 and Chrome; and to 32000 with MSIE 5.2.3.

The selector.maxLength > 0 subcondition is thus meant to weed out the non-zip required text fields for users using browsers for which the default maxLength value is -1, and it does do that. The two preceding // comments indicate that the !(selector.maxLength == 1024) and !(selector.maxLength == 2147483647) subconditions are similarly meant to weed out the non-zip required text fields for users surfing with Safari 3.0 and MSIE 7.0, respectively, but the 1024 and 2147483647 default maxLength values don't mesh with the most recent versions of Safari and MSIE for the Mac, as noted above; as a result, the overall condition returns true for all of the required text fields when using Safari 5.0.3 and MSIE 5.2.3 and also Chrome. Regarding the state selection list, for which selector.maxLength returns undefined, the overall condition returns false regardless of browser.

If we were validating the zip field, then the selector.value.length == selector.maxLength condition of the furthest-nested if statement would return true, and isEmpty would be switched to false, for any five-character input, whether digits or not. For the other required text fields, we aren't supposed to hit this conditional but Safari 5.0.3, Chrome, and MSIE 5.2.3 do hit it because of the maxLength snafu described above.

For Safari 5.0.3/Chrome and MSIE 5.2.3 non-zip required text field inputs that don't respectively comprise 524288 and 32000 characters - a reasonably safe assumption, yes? - the selector.value.length == selector.maxLength condition will return false and isEmpty won't be switched to false; as a result, these inputs will generate 'false negatives': the Validate( ) function will judge them to be invalid (vide infra) and consequently the x.gif image will be placed next to the fields holding them, even though they should be deemed valid by virtue of having passed the selector.value test of the outermost if statement.

We will discuss the second switch statement in a subsequent post - the non-email required field inputs 'pass through' this statement but are not affected by it.

The Validate( ) function concludes with a conditional that calls either the invalid( ) or valid( ) method of the Validator object depending on whether isEmpty is true or false, respectively:

if (isEmpty) this.invalid( ); // The field input is invalid.
else this.valid( ); // The field input is valid.

Let's sum up for the document.forms["liveForm"].firstName.value = "Kris" assignment that we made at the beginning of the post.
• The Kris input is deemed valid - isEmpty is false and Validator.valid( ) will be called - when using Firefox, Opera, Camino, or Netscape 9.
• The Kris input is deemed invalid - isEmpty is true and Validator.invalid( ) will be called - when using Safari 5.0.3, Chrome, or MSIE 5.2.3.

I warned you this would be 'gory', didn't I? And we haven't even placed the check.gif/x.gif image yet - we'll do that next time.


Wednesday, January 12, 2011
Further Adventures in Form Validation, Part 1
Blog Entry #202

Today's post will return us to the realm of form validation as we take up HTML Goodies' "Bring Your Forms to Life With JavaScript" tutorial, which is authored by Kris Hadlock and, like the "How to Populate Fields from New Windows Using JavaScript" tutorial we've been discussing in the last few entries, is a import.

"Bring Your Forms to Life With JavaScript" offers a liveforms/ file package, downloadable here, that codes a user registration form - the form holds fields for a user's first name, last name, street address, etc. - and individually validates a user's inputs for that form. For a valid field input, a [The 'check' image for a valid input] check.gif image is placed next to the field, whereas for an invalid input, an [The 'x' image for an invalid input] x.gif image is placed next to the field - that's how we'll 'bring the form to life'. When the values of the form's required fields are all in order, then an initially disabled submit button for sending those values to a processing agent is activated.

The author provides a tutorial demo here. As regards the OS X GUI browsers on my computer, the original demo works fine with Firefox, Opera, Camino, and Netscape 9, but is was problematic with Safari, Chrome, and MSIE 5.2.3 for reasons we will elaborate in due course.
November 2016 Update: Inexplicably, the Validator.js and utils.js importations (vide infra) have been removed from the demo.html source; as a result, the demo is now dysfunctional for all JavaScript-enabled browsers.

Code overview

The liveforms/ package comprises an img/ folder, an index.html file, and a js/ folder.
(1) The img/ folder contains the check.gif and x.gif images.
(2) The index.html file contains the registration form code.
(3) The js/ folder contains Validator.js and utils.js script files that jointly carry out the aforedescribed validation of the index.html form (hereafter the liveForm form, per the value of its id attribute).

The liveForm form holds
(a-f) six text boxes for respectively entering a first name, a last name, an email address, a company name, a street address, and a zip code, and also a
(g) 50-option selection list for choosing a state.
The user is required to input a value for all of these fields excepting the company name field. The form concludes with a
(h) submit button that is disabled for JavaScript-enabled users but is enabled for sans-JavaScript users.

The Validator.js script does the lion's share of the validation work via seven methods associated with a custom Validator object. For its part, the utils.js script comprises five auxiliary functions, one of which, ac( ), is never called. These scripts employ an impressive range of JavaScript and DOM features, as we'll see below.

Setting the stage

So, let's say we visit the aforelinked tutorial demo page. The action begins in the document head with the import of the utils.js and Validator.js scripts:

<script type="text/javascript" src="js/utils.js"></script>
<script type="text/javascript" src="js/Validator.js"></script>

At this time, the Validator.js script creates an empty Object object and gives it a Validator identifier

var Validator = new Object( );

and then associates with that object a series of functions, which thereby become methods of that object, to be detailed later; there is no top-level code in the utils.js script. We previously worked with a new Object( ) object in the guitar chord chart script of HTML Goodies' JavaScript Script Tips #56-59; we assigned properties to that object, as we'll also do to Validator, but didn't equip it with any methods. (There are other ways to create custom JavaScript objects, and it is clear from the "Working with objects" chapter of the JavaScript 1.5 Core Guide that the new Object( ) route is not Mozilla's preferred means of doing so, but let's just go with the flow for the time being.)

Let's move now to the bottom of the demo.html page. If we are sans-JavaScript users, we'll see an active button via:

<div id="innerFieldset">
<noscript><input id="submit" type="submit" value="Register" class="action" /></noscript>

• I'm pretty sure we've never discussed the noscript element before - this element allows authors to provide alternate content when a script is not executed. Its use here is unnecessary, at least vis-à-vis a visual browser (I don't know if non-visual browsers give it a special rendering); moreover, the noscript element's %block; content model is violated by the above code.
• No use is made of the input element's class="action" attribute.
• Somewhat amusingly, the button, which lies outside the liveForm fieldset, is horizontally stretched most of the way across the viewport by a width:90% style declaration:

BTW, the button's containing block is the id="innerFieldset" div element and not the noscript element, even though the latter is a block-level element.

And what happens if we are JavaScript-enabled users, as is more likely to be the case? The innerFieldset div element is followed by a script element

<script type="text/javascript">
gebid('innerFieldset').innerHTML = '<input id="submit" type="submit" value="Register" style="width: 100px;" disabled="true" />';

which calls the gebid( ) function of the utils.js script

function gebid(i) {
    try {
        return document.getElementById(i); }
    catch(err) {
        return null; } }

which 'gets' the innerFieldset div element and returns it to the calling script element, which sets its innerHTML to a width:100px, disabled submit button.

• We previously discussed an application of the try...catch statement in the Try me, catch me subsection of Blog Entry #165. Are there any browsers out there that support the try...catch statement but not the getElementById( ) method? I don't think so, but I could be wrong about that.
HTML boolean attributes such as disabled (and selected - cf. the state selection list code) are not set to true/false à la JavaScript and DOM properties; rather, they may legally take a single value: the name of the attribute itself (e.g., selected='selected'). If a document is served as HTML (vis-à-vis XHTML), then a boolean attribute should be specified in its minimized form:

<input id="submit" type="submit" value="Register" style="width: 100px;" disabled />

When the demo.html page has finished loading, an onload event handler

<body onload="Validator.Initialize('liveForm', 6, 'submit', 'img/check.gif', 'img/x.gif');">

calls the Initialize( ) method of the Validator object and passes thereto a set of five parameters.

Validator.Initialize = function(formId, fieldNum, submitId, validImage, invalidImage) {
    Validator.currentSelector = '';
    Validator.currentForm = formId;
    gebid(Validator.currentForm).reset( );
    Validator.fieldNumValidated = 0;
    Validator.fieldNumToValidate = fieldNum;
    Validator.submitId = submitId;
    Validator.validImage = validImage;
    Validator.invalidImage = invalidImage; }

Lots going on here. In the above function expression, an anonymous (nameless) function is defined and then associated with the aforecreated Validator object via an Initialize name - see the "Defining methods" section of the JavaScript 1.5 Core Guide - the association part occurs when Validator.js is imported, but the statements in the function body are not executed until the function is called via the Validator.Initialize( ) command of demo.html's onload event handler.

The function body defines seven custom properties for our custom Validator object:
(1) currentSelector, which will later represent a to-be-validated control object and is initialized to an empty string;
(2) currentForm, which is set to formId (liveForm), the id of the to-be-validated form, and then on the next line is shipped off to the gebid( ) function to get the liveForm form so that it can be reset( ) if necessary;
(3) fieldNumValidated, which will tally the number of liveForm fields that have been filled out correctly and is accordingly initialized to 0;
(4) fieldNumToValidate, which is set to fieldNum (6) and will represent the total number of liveForm fields that must be filled out correctly;
(5) submitId, which is set to submitId (submit), the id of the submit button, and will later be used to un/disable the button;
(6) validImage, which is set to validImage (img/check.gif), the URL path to the check.gif image; and
(7) invalidImage, which is set to invalidImage (img/x.gif), the URL path to the x.gif image.
For these assignments, we could use the keyword this in place of the Validator object reference, i.e.:

this.currentSelector = '';
this.currentForm = formId;

We are finally ready to fill out, and validate, the liveForm form. Beginning at the top, let's say we enter Kris into the "Your first name: *" field and then tab to the "Your last name: *" field. We'll go through the gory details of what happens next in the following entry.


Monday, January 03, 2011
Blog Entry #201

At the end of our last episode, we were taking a hard look at the code of the help.html remote control page of HTML Goodies' "How to Populate Fields from New Windows Using JavaScript" tutorial. As it is desirable for various reasons to keep JavaScript and HTML separate, we would specifically like to specify help.html's user interfaces as normal HTML and not via document.write( ) commands. How might we do that and yet preserve the original code's functionality for both JavaScript-enabled users and sans-JavaScript users?

none to block

Drawing inspiration from HTML Goodies' "Drop-Down Menu" tutorial, which we dissected in Blog Entry #96, it occurred to me that we could initially conceal the help.html user interfaces via a display:none CSS declaration and then visualize them for JavaScript-enabled users by switching the display value to block. Towards this end, let's first
(a) take the interfaces out of the document.write( ) commands that write them, and then
(b) place them, as normal HTML, in div element containers.

<div>[<a href="#" onclick="checkRadio('myForm', 'inputRadio', 0); return false;">Choose input #4</a>]</div>

(On the original help.html page, each interface has a de facto block-level rendering by virtue of being sandwiched between a p element and an h2 element (excepting the Input #3 interface, whose preceding <p>Input #3 is a required field... lacks a closing </p> tag and thus has an inline rendering), so a div container seems appropriate.)

Per the above, the div elements are annihilated (as in "reduced to nothingness") with

<style type="text/css">
div { display: none; }

and then brought back to life via:

<script type="text/javascript">
function visualize( ) {
    invisElements = document.getElementsByTagName("div");
    for (i = 0; i < invisElements.length; i++)
        invisElements[i].style.display = "block"; }
window.onload = visualize;

For the Input #1, Input #2, and Input #7 interfaces, the <form onsubmit="return false;"></form> containers can and should be thrown out.

<label>Fill in input #1: <input id="inputA" type="text" name="myInput" value="Text."></label>
<input type="button" onclick="input('myForm', 'input1', 'inputA');" value="Update">

Although wrapping the Input #[1|2|7] interfaces in form elements allows their buttons to access the myInput/myOtherInput/myTextarea values via this.form.controlName.value expressions, I would nonetheless argue that it is semantically wrong for the forms to be there as we are not submitting the myInput/myOtherInput/myTextarea values to a processing agent but are merely sending them to forms.html's myForm form.

Alternatively, if we were to equip the myInput/myOtherInput/myTextarea fields with id attributes - say, id="inputA", id="inputB", and id="textareaA", respectively - then the buttons could send the id values to a

function input(formName, obj, controlID) {
    opener.document.forms[formName].elements[obj].value = document.getElementById(controlID).value;
    self.close( ); }

function that only needs to reference a field value and not a containing form.

For the Input #6 interface, we can add to the visualize( ) function a statement that will conditionally set the Choose|Deselect input #6 link text:

if (!opener.document.myForm.inputCheck.checked)
    document.getElementById("checkboxA").innerHTML = "Choose input #6";
    document.getElementById("checkboxA").innerHTML = "Deselect input #6";
<div>[<a id="checkboxA" href="#" onclick="check('myForm', 'inputCheck', true); return false;">Choose input #6</a>]</div>

The above code does not set the check( ) arguments[2] value to false for a Deselect input #6 link, but recasting check( ) as

function check(formName, obj, choice) {
    if (opener.document.myForm.inputCheck.checked)
        choice = false;
    opener.document.forms[formName].elements[obj].checked = choice;
    self.close( ); }

will take care of that.

Disable it

The display:none|block approach works well with any semimodern*, CSS-supporting GUI browser whether its "Enable JavaScript" preference is switched on or off. (*The getElementsByTagName( ) method goes back to the DOM Level 1 Core - it's been around for a while.) I've also come up with a lower-end approach for users of browsers that support neither JavaScript nor CSS, e.g., Lynx and some mobile browsers: we can't display:none the help.html interfaces for such users, but we can at least disable the controls of those interfaces via the HTML disabled attribute.

<label>Fill in input #1: <input disabled id="inputA" type="text" name="myInput" value="Text."></label>
<input disabled type="button" onclick="input('myForm', 'input1', 'inputA');" value="Update">

For JavaScript-enabled users, the disabled controls can be undisabled à la the display:none|block approach:

disabledInputs = document.getElementsByTagName("input");
for (i = 0; i < disabledInputs.length; i++)
    disabledInputs[i].disabled = false;
document.getElementById("textareaA").disabled = false;

Ah, but what about the interface links? As far as I know, links cannot be disabled in the same way that controls can. But I don't like the use of links for non-link purposes anyway - we really should convert the interface links to push buttons, shouldn't we?

[<input disabled type="button" onclick="checkRadio('myForm', 'inputRadio', 0);" value="Choose input #4">]

Although select is not a JavaScript reserved word, I find that I am strangely unable to call help.html's select( ) function from an <input type="button"> button - no errors are thrown, nothing happens at all - with the OS X GUI browsers on my computer excepting Internet Explorer, perhaps because this function is being mistaken for the select( ) method of the DOM's HTMLInputElement interface: giving the select( ) function a clean name (chooseOption( ), makeSelection( ), whatever) solves this problem.

If we trade in the Input #6 interface link for an <input type="button"> button, then the label on that button can be toggled via a document.getElementById("checkboxA").value expression - the innerHTML property is of course not relevant to empty elements.

One small change to forms.html

JavaScript-enabled users are able to see the forms.html and help.html pages at the same time; this can be so for sans-JavaScript users as well if we
(a) equip each help icon link with a target="helpWindow" attribute, or equivalently and preferably
(b) add a <base href=currentDocumentURL target="helpWindow"> element to the document head.

Validate it

Because the author went to the trouble of splitting the HTML end-tags written by help.html's document.write( ) commands, and because forms.html and help.html both begin with a document type declaration and are already sitting on the Web at the site, I thought, "Why don't we run forms.html and help.html through the W3C's markup validator and see if they're really valid?" Let's do that now, shall we?
This document passes validation but also generates two "warnings" because of a misspecified public identifier in its document type declaration. For an HTML 4.01 Strict document, the correct public identifier is -//W3C//DTD HTML 4.01//EN and not -//W3C//DTD HTML 4.01 Strict//EN. FYI: Document type declarations for HTML 4.01 Strict, Transitional, and Frameset documents are detailed in the "HTML version information" section of the HTML 4.01 Specification.
This document does not pass validation because of the unescaped textarea element end-tag in the Input #7 section:

document.write('<label>Fill in input #7: <textarea name="myTextarea" value="Text goes here."></textarea></' + 'label>');

More specifically, the validator interprets the </textarea> as non-script markup and correspondingly throws an end tag for element 'TEXTAREA' which is not open error. The error disappears if the </textarea> is split à la the other document.write( )-written end-tags:

document.write('<label>Fill in input #7: <textarea name="myTextarea" value="Text goes here."></' + 'textarea></' + 'label>');

Less importantly, this document's document type declaration also holds an incorrect -//W3C//DTD HTML 4.01 Strict//EN public identifier, which generates the same warnings that it did in the preceding validation but, as noted above, is not sufficient to invalidate the document.

A related aside

HTML Goodies' Getting Started Tutorial sector sports a "So, You Want An HTML Declaration, Huh?" tutorial that deconstructs a system identifier-lacking document type declaration and briefly addresses the age-old question, "Does my document really need a document type declaration in the first place?" I would argue that the latter question can be answered by answering another question: "Just how serious am I about what I'm doing?"

Let's suppose you are starting an e-business selling the next big thing, whatever it is, and you are determined to reach out to as many users as possible, regardless of what types of devices/user agents they might be using. Towards this end, you religiously code your Web site's pages to the W3C's standards and plan to run them through the W3C's markup validator to ensure that they are up to scratch. Do you need document type declarations at the top of your pages? Yep - your pages won't validate without them. Conversely, if what you're doing isn't so crucial - let's say you are a 'weekend silicon warrior' who merely wants to create a page showing off your vacation photos - and standards are less of an issue for you (you can't ignore them completely, but you know what I mean), then you can forgo the declaration.

In the following entry, we'll move on to the next Beyond HTML : JavaScript sector tutorial, "Bring Your Forms to Life With JavaScript".


Powered by Blogger

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