Article
Well-Behaved DHTML: A Case Study
Now we’ve implemented a much more professional version of our original labels script. It doesn’t exclusively bind to event handlers, and we made the script more modular by implementing it as a series of functions. Because of this, the script will be more flexible to work with and easier to maintain.
But what about the coupling between the DHTML and the code that processes the form? If we leave the form field empty and press the Submit button, “Username” will be submitted to the server-side process. We still need to solve this problem.
Each form has an onsubmit event that’s fired just before its values are submitted to the server. We simply need to loop through each form on the page and add our event handler to this event. A good place to do this is in our setup function:
function setupLabels() {
// get all the labels on the entire page
var objLabels = document.getElementsByTagName("LABEL");
var objField;
for (var i = 0; i < objLabels.length; i++) {
// if the label is supposed to be dynamic...
if ("dynamicLabel" == objLabels[i].className) {
// get the field associated with it
objField = document.getElementById(objLabels[i].htmlFor);
// add event handlers to the onfocus and onblur events
addEvent(objField, "focus", focusDynamicLabel);
addEvent(objField, "blur", blurDynamicLabel);
// save a copy of the label text
objField._labelText = objLabels[i].firstChild.nodeValue;
// initialize the display of the label
objField.value = objField._labelText;
}
}
// for each form in the document, handle the onsubmit event with the
// resetLabels function
for (var i = 0; i < document.forms.length; i++) {
addEvent(document.forms[i], "submit", resetLabels);
}
}
To implement the resetLabels function, we do the opposite of what we did in the setup: loop through each label in the form and check to see if it’s a dynamic label. If it is, and it’s displaying the label text, we reset its value to an empty string.
function resetLabels(event) {
var elm = getEventSrc(event);
// get all label elements in this form
var objLabels = elm.getElementsByTagName("LABEL");
var objField;
for (var i = 0; i < objLabels.length; i++) {
// if the label is dynamic…
if ("dynamicLabel" == objLabels[i].className) {
// get its associated form field
objField = document.getElementById(objLabels[i].htmlFor);
// if the field is displaying the label, reset it to empty string
if (objField._labelText == objField.value) {
objField.value = "";
}
}
}
}
Example D shows our work at the end of Step 2. We’ve successfully transformed our original structured document into the dynamic effect we wanted. It’s no longer coupled to the code that processes the form, it works well with other scripts, and it’s well-modularized code.
Step 3: Identify All User-Agent Requirements
This step is easy: we just look through the code from in step 2 and identify all the objects, features and other browser requirements we used. We will use this information to create a JavaScript function that weeds out all the browsers that don’t meet these requirements.
In the labels script, we used many different DOM technologies, but we really need to test for three only:
document.getElementByIdwindow.attachEventorwindow.addEventListener
We can do it with this simple function:
function supportsDynamicLabels() {
// return true if the browser supports getElementById and a method to
// create event listeners
return document.getElementById &&
(window.attachEvent || window.addEventListener);
}
The reason we don’t need to test for more properties is that all the DOM functions we’re using are either from DOM Level 1 HTML or DOM Level 2 Events. Once we see that the current browser supports one of the methods from each recommendation, we can assume that it implements the remainder of that recommendation (at least superficially).
We’re only using a small subset of each recommendation, so we don’t need to go into more detail in our testing. As your scripts grow more complex, you’ll find that some browsers only partially support certain recommendations, and that you need to test for more and more specific features.
The W3C recommendations actually propose a way for a browser to indicate which levels of the DOM it supports, through the hasFeature method. Ironically, this method is not well-supported.
The reality of DHTML will probably always include partially and wrongly implemented specifications. It’s up to the developer to make sure that they test properly for the required features.
Step 4: Transform the Logical Structure when the Agent Requirements Are Met.
After the feature check function, the next thing to do is write the code that will actually transform the structure from the logical code you wrote in step 1 to the dynamic code in step 2.
In each place where a transformation is made, you should first check whether the current browser is supported. This way, the effect will either be completely implemented, or not implemented at all.
The two major places where we made changes to the logical structure of our document were the addition of the style rule to turn off the display of the HTML labels, and the setup function that runs in the window’s onload event. We simply need to prevent those two transformations from occurring if the browser is not supported.
For the style rule, we will change our code so that JavaScript is used to actually write the rule out to the document. This is an elegant solution that I often use because it is so reliable. The best way to make sure the document structure is only changed when JavaScript is present is to use only JavaScript to change the document structure.
We remove the stylesheet rule we added in Step 2, and replace it with the following JavaScript:
if (supportsDynamicLabels()) {
document.writeln('<style type="text/css">');
document.writeln('label { display:none; }');
document.writeln('</style>');
}
We move the setup function into the “if” branch as well, because we want it to run only if our requirements are met:
if (supportsDynamicLabels()) {
document.writeln('<style type="text/css">');
document.writeln('label { display:none; }');
document.writeln('</style>');
addEvent(window, "load", setupLabels);
}
Example E shows the completed effect.