reptile7's JavaScript blog
Thursday, September 01, 2011
Wizard Nouveau
Blog Entry #225

In this post we will revamp the code of the JavaScript Wizard Example section of HTML Goodies' "Making a Wizard with JavaScript" tutorial. À la the previous post's deconstruction, we'll first deal with the code's HTML and then take on its JavaScript.

Structure (+ some presentation)

I noted last time that the "Making a Wizard with JavaScript" wizard resembles one big table - excepting the Step 1 introduction text, everything you see in the wizard is table element content - and it initially seemed like a good idea to code the wizard header, body, and footer as thead, tbody, and tfoot elements, respectively. Upon blithely applying a #HeaderTable td { width: 20%; } style rule to an id="HeaderTable" thead element (more precisely, to the td children thereof), however, it immediately became apparent that the thead layout would end up directing the tbody and tfoot layouts, and I didn't like that. Yes, we can give the tbody/tfoot elements their own style rules so as to override the thead layout, but I thought, "Do I really want to do that, or would I rather work with three completely independent zones of code?" I decided to go the latter route.

And why are we using all those tables in the first place? I'm sure you are aware that the W3C frowns on the use of tables for layout purposes:
Tables should not be used purely as a means to lay out document content as this may present problems when rendering to non-visual media. Additionally, when used with graphics, these tables may force users to scroll horizontally to view a table designed on a system with a larger display. To minimize these problems, authors should use style sheets to control layout rather than tables.
Accordingly, I next threw out the Steps 2/3/4 tables plus their span containers and replaced them with analogous div elements. Here's my new Step 2, for example:

#Step2, #Step3, #Step4 { display: none; text-align: right; }
label, input { margin: 5px; }
<div id="Step2">
<label for="TextFirstName">First name:</label>
<input id="TextFirstName" name="FirstName" type="text" /><br />
<label for="TextMiddleName">Middle name:</label>
<input id="TextMiddleName" name="MiddleName" type="text" /><br />
<label for="TextLastName">Last name:</label>
<input id="TextLastName" name="LastName" type="text" />

As shown, the control labels are marked up as label elements; if desired, the controls and their labels can be slightly pushed apart via the CSS margin property in lieu of the tables' cellpadding="5" attributes.

What about the align="right" attributes for the labels in the Steps 2/3/4 tables? Like most block-level elements, the div element has an effective width of 100%, that is, its width spans the width of the viewport; as a result, simply replacing the align="right" attributes with a text-align:right; style declaration would shift the Steps 2/3/4 labels and controls as div content to the right side of the viewport - not good. One approach to this problem is to set specific widths for the Steps 2/3/4 divs. Alternatively and more easily, we can give each div a shrink-to-fit width when we render it by setting its CSS display property to inline-block - note that the CSS text-align property applies to inline blocks as well as block-level elements. (We are not concerned with the flowed as a single inline box aspect of the inline-block value as there is no content to the right of the divs.)

As for Step 5, I decided to hold onto its table - I felt it was 'semantically appropriate' to do so because the Step 5 table does actually organize a table of information - but did at least discard the table's span container.

I replaced the header table with an id="dhead" div of five spans, whose borders and background-colors were set via conventional style rules; I kept the Step1/Step2/Step3/Step4/Step5 ids for the wizard body divs and the final review table; I replaced the footer table with an id="dfoot" div. I vertically separated the header div, the body divs/table, and the footer div with the following style rule sets:

#Step1, #Step2, #Step3, #Step4, #Step5 { position: relative; top: 25px; }
#dfoot { position: relative; top: 40px; }


We move now to the wizard's JavaScript. The handleWizardNext( ) and handleWizardPrevious( ) functions tediously spell out each operation for the four forward step transitions and the four reverse step transitions. Is it really necessary to do this? For that matter, do we need to have separate handleWizardNext( ) and handleWizardPrevious( ) functions? In both cases the answer is no.

So, let us begin building a common handleWizard( ) function that will drive both forward and reverse movement through the wizard. This function will use a boolean-like direction variable to determine the direction of movement and a step variable to track the user's progress through the wizard.

var step = 1; // The wizard begins at Step 1.
function handleWizard(direction) { ... }
<div id="dfoot">
<input id="ButtonPrevious" type="button" value="Previous" disabled="disabled" name="reverse" onclick="handleWizard(;" />
<input id="ButtonNext" type="button" value="Next" name="forward" onclick="handleWizard(;" /> ...

The handleWizardNext( ) and handleWizardPrevious( ) operations can be divided into 'old' operations - operations pertaining to the wizard step we are leaving - and 'new' operations - operations pertaining to the step we are going to. Not counting the navigation button name change business, which the step index is superseding, there are two old operations that are carried out for all eight step transitions: (1) zeroing out the old step and (2) silverizing the old step's header in the header table. The step containers have 'ordinalized' ids (Step1, Step2, ...) as do the step headers (HeaderTableStep1, HeaderTableStep2, ...), allowing us to generalize the old-for-all-transitions operations via the step index:

document.getElementById("Step" + step).style.display = "none";
document.getElementById("HeaderTableStep" + step).style.backgroundColor = "silver";

There are two complementary new operations that apply to all eight step transitions: (1) displaying the new step and (2) yellowizing the new step's header in the header table. These operations can again be generalized via the step index:

document.getElementById("Step" + step).style.display = "inline-block";
document.getElementById("HeaderTableStep" + step).style.backgroundColor = "yellow";

To go from the old operations to the new operations requires us to increment step when moving forward and to decrement step when moving backward; we can effect either step change with a single statement that ties the change to the value of the direction variable:

direction == "forward" ? step++ : step--;
/* The ?: conditional operator is documented here in the Mozilla JavaScript Reference. */

The commands that un/disable the footer buttons and the loadStep5Review( ) function call are what remains; these are all new operations that pertain to a specific step transition and can be specified via step-based conditionals:

// Transition-specific operations going forward:
if (step == 2 && direction == "forward")
    document.getElementById("ButtonPrevious").disabled = false;
if (step == 5) {
    document.getElementById("ButtonNext").disabled = true;
    document.getElementById("SubmitFinal").disabled = false;
    loadStep5Review( ); }
// Transition-specific operations going backward:
if (step == 1)
    document.getElementById("ButtonPrevious").disabled = true;
if (step == 4 && direction == "reverse") {
    document.getElementById("ButtonNext").disabled = false;
    document.getElementById("SubmitFinal").disabled = true; }

For the step == 2 and step == 4 conditionals, the direction == "forward" and direction == "reverse" subconditions are unnecessary in the sense that at Step 2 we will want an enabled button and at Step 4 we will want an enabled button and a disabled button whether we are going forward or backward. Still, my preference is to test the wizard direction vis-à-vis executing redundant statements.

More behavior

We lastly address the loadStep5Review( ) function, which can also be significantly streamlined. The first four loadStep5Review( ) statements write the user's Steps 2/3 First name, Middle name, Last name, and Email inputs to the Step 5 review table cells whose ids are ReviewFirstName, ReviewMiddleName, ReviewLastName, and ReviewEmail, respectively. These commands differ only in the <input>-name substrings of their getElementById( ) arguments and are thus easily automated:

var fieldName = ["FirstName", "MiddleName", "LastName", "Email"];
for (i = 0; i < fieldName.length; i++)
    document.getElementById("Review" + fieldName[i]).innerHTML = document.getElementById("Text" + fieldName[i]).value;

As for the subsequent conditionals that, per the user's Step 4 checkbox selections, load Yes or No into the Step 5 table cells whose ids are ReviewHtmlGoodies, ReviewJavaScript, and ReviewWdvl, it has probably occurred to you that those statements can be condensed via the ?: operator, e.g.:

document.getElementById("ReviewHtmlGoodies").innerHTML = document.getElementById("CheckboxHtmlGoodies").checked ? "Yes" : "No";

The checkbox conditionals can also be automated à la the FirstName/MiddleName/LastName/Email commands:

var fieldName2 = ["HtmlGoodies", "JavaScript", "Wdvl"];
for (i = 0; i < fieldName2.length; i++)
    document.getElementById("Review" + fieldName2[i]).innerHTML = document.getElementById("Checkbox" + fieldName2[i]).checked ? "Yes" : "No";

I was going to finally note that adding a "Password" element to the above fieldName array will display the Step 3 Password input in unasterisked form in the review table's id="ReviewPassword" cell, thereby allowing the user to check the accuracy (and not just the length) of that input, but this now strikes me as something we shouldn't be doing; after all, passwords are supposed to be as private as possible, and it should be the responsibility of the user, and not the Webmaster, to maintain the integrity of a password.

Demo #2

The wizard demo in the div below incorporates the code discussed in this entry.

Step 1: Getting StartedStep 2: NameStep 3: Account AccessStep 4: Select subscriptionsStep 5: Finalize & Submit
Welcome to our Subscription Wizard!

This wizard simulates subscribing for access to website content. Each step is highlighted in the header.
This step is intended to provide the user with everything they need to know to get started.

Next up in the Beyond HTML : JavaScript sector is "Web Developer Tutorial: Build Your Own Image Scrollbar", which we'll tuck into in the following entry.


Comments: Post a Comment

<< Home

Powered by Blogger

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