It’s no secret that over the last few years DHTML has been used almost exclusively for evil purposes. Users associate the technology with intrusive advertisements and error-prone pages, while developers associate it with browser detection and hideous hacks.
This assumption is unfortunate. Browser technology has made great advancements over the last couple years. When it’s done right, DHTML can improve the users’ experience of many Webpages. And the hacks that used to be required to make it all work are now practically nonexistent.
When working with modern DHTML, I often reminisce about the old days of Web development, when I first became interested in the technology. Despite my best intentions, many of my first scripts now represent examples of what today’s DHTML coders should avoid — they rely too heavily on specific browser features and throw errors or degrade improperly when those requirements are not met. They don’t work well with other scripts on the same page, and they sometimes create dependencies upon other technologies.
When I encounter scripts like this, I think of them as badly-behaved. They have the potential to be good; all the tools are there. They simply are not doing what they should.
As I’ve grown as a Web developer, I have come to appreciate the value of well-behaved DHTML. I can always sell a client on the fact that the script will either work, or it will gracefully not work, in any browser. They don’t always appreciate obvious generalizations like practically everyone has DHTML enabled these days quite as much as code that degrades gracefully in situations where it’s not supported.
I’ve also noticed that, when developing this kind of DHTML, I tend to follow the same five-step process over and over again. What follows is an example of using this process to create a very basic DHTML script. Once you understand the principles, you can apply this process to most DHTML effects with repeatedly impressive results.
The code examples provided here do assume you have some familiarity with JavaScript, HTML, and the DOM. However, any Web developer or designer should be able to get some value out of familiarity with this process.
The Labels Script
A common use of DHTML on the Web is to create what I’ll call a Dynamic Label. A Dynamic Label is used to label a form field. However, the text for the label is rendered inside the form field, instead of adjacent to it (which would be more usual).
When the form field receives attention, the label disappears so that the user may type. If the user does not type anything, the label is restored as soon as the user clicks or tabs away from the field.
Dynamic labels save space, look sharp, and feel slick. They can be a nice improvement over a basic form label in the right situations.
A naïve developer might implement a dynamic label script like this:
<input type="text" name="username" value="username"
onfocus="if (this.value == 'username') this.value = '';"
onblur="if (this.value == '') this.value = 'username';" />
Example A displays this type of implementation.
It’s a valid first step, but that’s all. DHTML like this is an example of the badly designed scripts of yesteryear and should never make it into any production Website.
Let’s look at the problems one by one:
- Relies on JavaScript
The effect does not work if JavaScript is disabled. In this case, the label will still actually show up because it was hard-coded into the value attribute of the field. However, when the user focuses the form nothing happens. The user experience is badly broken – probably worse than it would be if there had simply been a normal textual label beside the field.
Coupling is a term used in programming circles to indicate when two components’ implementations are tied tightly together — usually a very bad thing. Coupling means that when one component’s code changes, the other component’s code might also have to change.
In our case, the JavaScript that creates our effect is tightly coupled to the server code that processes the forms. The server code must be aware of what the labels are for each form field and be able to filter them out of the form submission. This is because we have put the label’s text in the value attribute of each field. If the user does not type anything into one (or any) of these fields, the label will actually be submitted instead.
To see an example of this in action, just click Submit without typing anything into Example A.
A common hitch among novice DHTML scripts is that they set the values of elements’ event properties directly. You can do this through attributes of an element, or in JavaScript with properties. Directly setting JavaScript events is generally a bad idea because only one block of code can use each event. If you start to run more than one script on a page, the various scripts’ event handlers can overwrite each other. This type of DHTML is harder to maintain and can result in errors that are difficult to debug.
In modern browsers, we can use Event Listeners to bind more than one function to a specific event. Avoid the old style of event handling except when it’s absolutely required.
This script is not modularly designed. If we decide to implement another dynamic label, we have no choice but to copy and paste our current code into that box’s event handlers and change the various places the label text shows up.
If we discover a bug in the script, or want to make a change, we have to remember to make the changes for each label. If we decide to change the label text, we have to change it in three places. Non-modularly designed programs are difficult to maintain and develop because they are so error-prone. It’s easy to make mistakes and hard to debug them.
Now that we’ve analyzed the problems in our first dynamic labels script, we have a good idea of what our goals should be in the script’s next iteration. In short, we want a dynamic label script that:
- Does not rely on JavaScript
- Does not couple with any other component
- Does not exclusively bind to any events
- Is modularly designed
5 Steps to Writing Well-Behaved DHTML
Our goals for the production Dynamic Label script are not unlike the goals for most DHTML enhancements to Webpages. In fact, almost all scripts I write share these same goals.
Over time, I’ve discovered that there’s a simple process that can be followed for almost any DHTML effect to ensure that these goals are met:
- Identify the underlying logical structure of the effect.
- Create a full working example of the effect.
- Identify all user agent requirements.
- Write code to transform the logical structure when the agent requirements are met.
- Thoroughly test each target platform.
Step 1: Identify The Underlying Logical Structure of the Effect
One of our primary goals is to avoid any reliance on JavaScript. A common, but ultimately flawed approach to this problem is to try to detect “supported” browsers on the server. If a browser is supported, it is sent the dynamic version of the code. Otherwise, it is sent a simpler version.
The problem is that it’s practically impossible to unambiguously detect browser type and version on the server. Even if you could, you wouldn’t be able to detect whether JavaScript was actually enabled for a particular user. Browsers simply don’t send the server enough information to reliably identify themselves, or their configuration.
The best way to avoid JavaScript reliance is to build DHTML effects on top of a simple, logical document structure that doesn’t require it. The effect will become enabled dynamically on the client, if it is supported. If not, the user will see the basic document.
The logical structure for our dynamic label works out nicely, thanks to the existence of the label
HTML element.
The label element structurally links a form element to its textual label. In most visual browsers, the only tactile difference between using the label element and any other element (or no element at all) is that clicking the label focuses the form on the field with which that label is associated.
However, at this point we are interested in simply building the most logical underlying structure for our effect, so we will use the label element. Example B shows our work.
Clearly there’s nothing fancy here, and that’s precisely what we want. The code from this step is the lowest-common-denominator view of our effect. Ideally, this document should make sense whether it is viewed in the latest version of Mozilla, or on a cell phone. This is the document users will see if their browser doesn’t have the features our effect requires, or doesn’t have them enabled.
Step 2: Create A Full Working Example of the Effect in a Best-Case Environment
The next thing to do, once you’ve got the logical structure in place, is to modify it to create a full working example of the effect. Don’t worry about how the script will degrade at this point, just make it work with the assumption that every feature you require will be available and turned on.
Looking at our work from Step 1, it’s easy to see the high-level tasks we’ll have to accomplish for each dynamic label to display our effect:
- Hide the regular HTML label element.
- Attach JavaScript functions to the onfocus and onblur events of the associated field that show and hide the label at the right times.
The simplest way to complete the first task is with a CSS rule like so:
<style type="text/css">
label {
display:none;
}
</style>
If you’re not familiar with CSS, you can get a quick primer here at SitePoint.com, or at the W3C.
The problem with a simple CSS rule like this is that it will turn off the display of every label on the page. We’ll have to modify the rule when we want to use it on a page that has label elements we want displayed in the regular way, without the effect. This wouldn’t be a very modular design at all.
The solution, of course, is to give a special class to the labels we want to behave dynamically:
<style type="text/css">
label.dynamic {
display:none;
}
</style>
The second task essentially requires that we loop over all the label elements on the page, check to see whether they have the correct class, and if they do, add event handlers to their associated field. We should also save a copy of the label text in a property of the field for easy access, and initialize the label display while we’re here.
This requires some knowledge of the document object model. If you’re rusty on the details, or never took the time to learn, you can brush up at the W3C. Browser vendors often have good resources as well (e.g. Microsoft, and Mozilla), although these are obviously skewed toward their own implementations.
In a perfect world, once we learned how the DOM works, we could use the following code to perform our task. It uses getElementsByTagName
the getElementById
methods, as well as the className
property. Each of these is defined in DOM Level 1.
This code also uses the addEventListener
method from DOM Level 2 Events.
n 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
objField.addEventListener("focus", focusDynamicLabel, false);
objField.addEventListener("blur", blurDynamicLabel, false);
// save a copy of the label text
objField._labelText = objLabels[i].firstChild.nodeValue;
// initialize the display of the label
objField.value = objField._labelText;
}
}
}
However, this code won’t work for IE/windows because it isn’t fully DOM-compliant; it doesn’t support the DOM Level 2 Events Module. Instead, it supports a proprietary interface that does the same thing. As IE/windows has such a huge user base — and one we’d like to have see our effect — we add a small hack to our script to accommodate its different object model (note the changed lines are bold):
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;
}
}
}
function addEvent(objObject, strEventName, fnHandler) {
// DOM-compliant way to add an event listener
if (objObject.addEventListener)
objObject.addEventListener(strEventName, fnHandler, false);
// IE/windows way to add an event listener
else if (objObject.attachEvent)
objObject.attachEvent("on" + strEventName, fnHandler);
}
We can make this script run once the page is loaded by attaching to the window’s onload event with the same utility function.
addEvent(window, "load", setupLabels);
Now all we have to do is implement focusDynamicLabel
and blurDynamicLabel
. This is easy — it’s just like the original code from our first dynamic label script. The only difference is that it should be generalized so the same function works for each dynamic label on the page.
In a fully DOM-compliant browser, we could use the target property of the event object (Also defined in DOM Level 2 Events) to obtain a reference to the element that fired the event, and manipulate it:
function focusDynamicLabel(event) {
// get the form field that fired this event
var elm = event.target;
// if it is currently displaying the label...
if (elm._labelText == elm.value) {
// ... turn it off
elm.value = "";
}
}
function blurDynamicLabel(event) {
// get the form field that fired this event
var elm = event.target;
// if it's empty...
if ("" == elm.value) {
// ... display the label text
elm.value = elm._labelText;
}
}
But once again, IE/windows implements this functionality slightly differently, using the property srcElement
instead of the standardized target
, and making the event object available through window.event
instead of the standardized way of passing it implicitly to event handler functions.
We’ll need another small hack and helper function:
function focusDynamicLabel(event) {
// get the form field that fired this event
var elm = getEventSrc(event);
// if it is currently displaying the label...
if (elm._labelText == elm.value) {
// ... turn it off
elm.value = "";
}
}
function blurDynamicLabel(event) {
// get the form field that fired this event
var elm = getEventSrc(event);
// if it's empty...
if ("" == elm.value) {
// ... display the label text
elm.value = elm._labelText;
}
}
function getEventSrc(e) {
// get a reference to the IE/windows event object
if (!e) e = window.event;
// DOM-compliant name of event source property
if (e.target)
return e. target;
// IE/windows name of event source property
else if (e.srcElement)
return e.srcElement;
}
Example C shows our work so far.
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.getElementById
window.attachEvent
orwindow.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.
Step 5: Thoroughly Test on All Target Platforms
The importance of careful testing for DHTML effects cannot be understated. The simple fact is that if you’re going to write DHTML, you need to be able to test it personally on the majority of the platforms on which it is intended to run.
For example, a simple Google search will find that Windows IE 5+, Gecko, and Safari all seem to implement the features we need.
However, if you were to run Example E on Safari 1.0, you’d notice a big problem: the effect runs only once! The first time you click in the textbox, the label disappears correctly. But upon blur, nothing happens. The textbox stays blank, and you can never get the label back again.
It turns out that Safari has a bug — it doesn’t fire onblur for a textbox until the next textbox is focused. In our case, this means that if the user simply tabs or clicks away from the textbox without focusing another textbox, our label will not reappear.
Safari’s problem with onblur is an example of an implementation bug that cannot be tested for through simple feature detection. We will need to update our feature test function to test for the Safari Web browser specifically. The following change will do the trick:
function supportsDynamicLabels() {
return
document.getElementById &&
(window.attachEvent || window.addEventListener) &&
null == navigator.appVersion.match(/Safari/d+$/);
}
The added line uses a Regular Expression to test the appVersion
property of the navigator object and return true when the current browser is not Safari.
When testing for a specific browser, it’s often better to test for a specific proprietary property in that browser’s object model. For instance, IE has the window.clientInformation
property, which can be used to distinguish it unambiguously from other browsers.
Safari does not seem to support any proprietary properties, however. We must therefore resort to testing the appVersion
property of that navigator object. You could also test the userAgent
property, but this is less reliable as it can be modified by the users of some browsers.
Example F shows our final work. We’ve successfully transformed our first, badly-behaved dynamic label script into something much better. Our final code is completely modular, doesn’t rely on JavaScript, works well with other scripts, and doesn’t couple to any other components.
During testing, we discovered that Safari has an obscure bug in its handling of focus and blur events on textboxes, which makes it unable to support our effect. We look forward to a release of Safari that fixes this bug, at which time we can easily update our feature test function to test for only the buggy versions.
Most importantly, the five-step process we used to get to this point can easily be applied to any other DHTML effect for a modern Website.
DHTML can be used to supplement the UI of many Web pages, and it can be done so that its support is not required. This style of DHTML coding should not be viewed in the same light as the badly-behaved scripts of the past, but should be considered another worthy tool in the professional Web developer’s arsenal.