Monday, October 29, 2007

Robust DHTML

DHTML is the technique of combining JavaScript and CSS to create dynamic page elements that are not possible with static HTML. For example, it's easy to dynamically show or hide page elements, or move them.

Given the differences in broswers, it is by nature a bit difficult to write clean DHTML. And worse, most free DHTML examples of code on the internet are written poorly, as they have countless little browser checks, and conditional code branches like:


if(isIE){
//do something one way
} else if(isNetscape6){
//do something else another way
} else if(isNetscape4){
//do something else yet another way
//...
}


This is poor design, as it is brittle, hard to maintain, and often breaks when new browsers are released or change. And likely, any browser not in the checks will not supprted correctly either. Also I see a lot of newer DHTML scripts doing more css and in place of javascript, although, I still think css support across browsers is still a bit flakey.

A better way is to design something with these simple principles:


1. sniff for functionality rather than browser name/version
2. create a single wrapper function that encapsulates brower specific code
3. page should "degrade gracefully" if possible (meaning the page will still be readable and functional if javascript or dhtml not supported)


Here is a small example designed with these principles in mind (modified from Apple Developer's site):



// cross-browser dhtml utilities
// wrap blocks of html in div> tags with unique id's

// try to get a style object given its id
function getStyleObject( objectId ) {
if ( document.getElementById &&
document.getElementById( objectId ) ) {
// W3C DOM
return document.getElementById( objectId ).style;

} else if ( document.all && document.all( objectId ) ) {
// MSIE 4 DOM
return document.all( objectId ).style;

} else if ( document.layers &&
document.layers[ objectId ] ) {
// NN 4 DOM.. note: this won't find nested layers
return document.layers[ objectId ];

} else {
return false;
}
}

// a template function for setting two-state style properties
function setStyleBoolean( objectId, booleanValue,
propertyName, valueOn, valueOff ) {
var styleObject = getStyleObject( objectId );

if ( styleObject ) {
if ( booleanValue ) {
styleObject[ propertyName ] = valueOn;

} else {
styleObject[ propertyName ] = valueOff;
}

return true;

} else {
return false;
}
}

// try to show/hide object. a empty visual space will remain in place
function setObjectVisibility( objectId, booleanValue ) {
return setStyleBoolean( objectId, booleanValue,
'visibility', 'visible', 'hidden' );
}

// try to insert/remove object from display. page will redraw and no space will remain in place
function setObjectDisplay( objectId, booleanValue ) {
return setStyleBoolean( objectId, booleanValue,
'display', '', 'none' );
}

// try to move object
function moveObject( objectId, newXCoordinate, newYCoordinate ) {
var styleObject = getStyleObject( objectId );

if ( styleObject ) {
styleObject.left = newXCoordinate;
styleObject.top = newYCoordinate;
return true;

} else {
return false;
}
}



This code works in all javascript-enabled browsers I've tested (FireFox 1+, IE 4+, Netscape 4+, Opera), despite having no checks for any specific browser. Also, all code that interfaces directly with the broswer api is isolated, so it is very easy to extend or debug if there ever is a problem.

To use the code above, for example, save it to a file named "dhtmlutil.js" and use it like:



<html>
<head>
<script language="javascript"
src="dhtmlutil.js"></script>
</head>
<body>

<h2>Clean DHTML example</h2>

<div id="test1">
this is a block of html that can be hidden or removed
</div>

<p>
you can change the properties of the
block above with these buttons:

<p> set display
<input type=button
onclick="setObjectDisplay('test1', false);"
value = "off">
<input type=button
onclick="setObjectDisplay('test1', true);"
value = "on">

<p> set visibility

<input type=button
onclick="setObjectVisibility('test1', false);"
value = "off">
<input type=button
onclick="setObjectVisibility('test1', true);"
value = "on">

</body>
</html>



If you add more functions, put them all together in the head with the included file.

to use hyperlinks, use a dead link with an onclick handler like:


<A HREF="javascript:void(0)" onClick="....">


DEGRADING GRACEFULLY

A fundamental problem with DHTML is figuring out how to design the page so that it degrades gracefully. In other words, if javascript or css aren't supported, the user can still use the page. This is especially difficult for menus, and othter page entitties that are hidden/collapsed by default. A menu is the most important element on the screen, and should be viewable on the lowest common denominator. Even if old browsers are obsolete, there is always a chance it won't be compatible with future broswer releases/bugs (for example, the Netscape 6 to 7 fiasco). The general rule is:


4. Any hidden entity should be visible if DHTML is broken.
5. No core functionality should ever depend on DHTML.


The best way to achieve this is to wrap javascript around css elements in the header, such as:



//write styles via javascript, to degrade gracefully
var idx = 'yourid';
if (document.getElementById ||
document.all || document.layers ){
document.write('<style type="text/css">')
document.write('.switchcontent{display:none;}')
document.write('<\/style>')
document.write('<style type="text/css">')
document.write('#' + idx + '{display:block;}')
document.write('<\/style>')
}



This code, for instance, will hide all members of the class "switchcontent," except for the one with the id "sc1". So, if javascript or css is broken, all these entities will be visible. Note that these tags conflict, but that the style closest to the element takes precedence. Corresponding div tags for this code might look like:



<div id="sc1" class="switchcontent">
something visible by default
</div>
<div id="sc2" class="switchcontent">
something invisible
</div>
<div id="sc3" class="switchcontent">
something invisible
</div>

No comments: