Friday, October 24, 2008

Cross-Browser JavaScript: Creating DOM Nodes That Set The name Property

Creating and appending DOM nodes to a form can be tricky. Internet Explorer needs special care when you create a DOM node and then set the name property. This handy JavaScript gets around these cross browser issues efficiently, and without browser detection.

Web browsers have come a long way. We can create Document Object Model nodes at will using the universally supported document.createElement function, then you try creating form fields in Internet Explorer and you hit a snag.

Form fields, in order to be useful, need a name attribute in HTML. In JavaScript, you set the name property. It's two simple lines of code. Nothing special. No hoops to jump through:

// Create a new form input to hold a first name:
var el = document.createElement("input");
el.name = "fname";

You append this new input to your form and then try to access it using the form.elements.field_name syntax and you can't find the "fname" field. Turns out, Internet Explorer requires a little fudging:

// Create a new form input for Internet Explorer:
var el = document.createElement("<input name=fname>");

In one fell swoop, you must create the new element AND assign the name property before Internet Explorer will append this element to the form's document object model, making it available through the form.elements.field_name syntax. The problem is, standards compliant browsers, i.e. "every other browser developed within the last 8 years," will throw a JavaScript error if they try executing that code.

You could try browser detection, which usually involves parsing the user agent string of the browser. Most browsers allow you to change the user agent string, so this is no longer a viable method. Plus, what do you do when a new browser or browser version comes out? You'd have to change your function. Turns out, a simple try-catch block is the answer.

Creating a named DOM node the coss browser way

We can create our own createElement function to account for the difference in Internet Explorer's implementation of document.createElement:

function createElement(nodeName, name) {
var node;
try {
node = document.createElement("<"+nodeName+" name="+name+">");
} catch (e) {
node = document.createElement(nodeName);
node.name = name;
}
return node;
}

It works fine and dandy. Every time you call the function, it tries doing it Internet Explorer's way, but you take a performance hit because of the try-catch block that executes every time the createElement function executes. With a little runtime retooling, we can get around this using three functions instead of one:

/**
* Cross browser method for creating new DOM nodes with name attributes. This
* gets around an Internet Explorer bug that prevents that get the name property
* assigned to them and their values aren't sent when the form submits.
*
* @param string required Node name, like "div" or "input"
* @param string required Value of the name attribute
* @return object New DOM node
*/
function createElement(nodeName, name) {
var node;
try {
node = createElementMsie(nodeName, name);
createElement = createElementMsie;
} catch (e) {
node = createElementStandard(nodeName, name);
createElement = createElementStandard;
}
return node;
}

/**
* Code required by Internet Explorer when creating a new DOM node with the
* name attribute set.
*/
function createElementMsie(nodeName, name) {
return document.createElement("<"+nodeName+" name="+name+">");
}

/**
* Code required by all other browsers that support web standards.
*/
function createElementStandard(nodeName, name) {
var node = document.createElement(nodeName);
node.name = name;
return node;
}

When the page first loads, you've got three functions. The createElement function, which is what you will always use, has a try-catch block that calls two other functions. The createElementMsie function attempts to create the new DOM node using Internet Explorer's implementation. If the function call succeeds, the createElement function is reassigned to be the createElementMsie function. All subsequent calls to createElement will run the code in createElementMsie.

In the catch clause, the browser has failed to execute createElementMsie, and therefore doesn't support Internet Explorer's implementation of document.createElement. Here we assume it is a standards compliant browser and doesn't need any hand holding. It calls createElementStandard, and when that succeeds it reassigns the createElement function to be the createElementStandard function. All subsequent calls to createElement run the code in createElementStandard.

You still take a performance hit with these three functions because the try-catch block is executed, but only for the first time you call createElement. From the second time you call createElement onwards, it directly executes the code the browser supports. The try-catch block doesn't exist anymore, and you've gotten around Internet Explorer's faulty implementation of the document.createElement function. You can now create form fields at will and feel safe in knowing Internet Explorer will actually send those field values to the server when the form is submitted.

Using createElement:
var el = createElement("input", "fname");
// append the new INPUT to the form.

Tuesday, October 7, 2008

JavaScript Zebra Stripes: Alternate Background Colors on Tables and Lists

Love it or hate it, alternating background colors is a common design request for web designers. If you maintain a site of static HTML files, this "zebra striping" effect can become time consuming and tedious. Even if you use a content management system or a server side language, automation is your friend. This script is the automation you want.

There are tons of similar scripts on the Internet, and all do a great job. I've found they have some short comings. I don't always just want to alternate background colors on table rows. I often do not want cells in the table header and footer to have alternating colors either. Things get real messy when I want alternating colors on list items, and especially definition lists. This script package seeks to solve all these problems.

Browser Support

All standards compliant browsers are supported, as well as Internet Explorer version 5.0 and newer. If the browser supports the *.getElementsByTagName method, then the browser supports this script. This is also safe to use with any pre existing JavaScript library or framework. It packages all the related functions into an object called stripes. As long as you don't have a global variable or function named "stripes," this is safe to use, and since it utilizes only standard DOM API calls, no additional JavaScript libraries are required.

Using the stripes functions

Usually you'll want some sort of JavaScript event to kick things off. I recommend the onload event if you don't use a JavaScript framework like Mootools or jQuery. A simple function call in the onload attribute of the body tag will suffice:

<body onload="stripes.execute();">

For those who like unobtrusive JavaScript, you can add this to the window.onload function:

window.onload = function() {
stripes.execute();
};

The stripes.execute() function will parse the entire document object model. For very large HTML files, you might not want the function parsing the entire DOM tree. You can specify a root DOM element from which to start searching by passing an HTML tag Id or DOM node reference to the stripes.execute() function. If no argument is passed, or the Id passed does not exist in the HTML document, then the document element is used.

// Only search in <div id="content">

window.onload = function() {
stripes.execute( "content" );
};

/* --- OR --- */

window.onload = function() {
var contentNode = document.getElementById( "content" );
stripes.execute( contentNode );
};

How it works

The only thing this script package does is change class names on DOM node elements. For tables, a class name is assigned to the <tr> tag. For unordered and ordered lists, the class name is assigned to the <li> tag. Lastly, for definition lists the class name is assigned to the <dt> and <dd> tags. All you need to do is add style rules to your CSS.

The script comes preprogrammed with two class names. Even rows or list items are given the class "rowA". Odd rows or list items are given the class "rowB". You can easily change the class names in the JavaScript source code. Below is a snippet showing you where you can change these default class names:

window.stripes = {

stripeRowEven : "rowA",
stripeRowOdd : "rowB",
stripeParentClass : "stripes",


...

};

The stripeRowEven and stripeRowOdd properties contain the class names assigned to table rows, list items, and definition terms and descriptions. The third property, stripeParentClass, is the class name the script will search for when making the zebra stripes. As mentioned earlier, this script works for tables, unordered lists, ordered lists and definition lists:

HTML Structure Supported for TABLEs
<table cellpadding="5" cellspacing="0" border="1" class="stripes">
<thead>
<tr>
<th>Heading 1</th>
<th>Heading 2</th>
<th>Heading 3</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Footer 1</th>
<th>Footer 2</th>
<th>Footer 3</th>
</tr>
</tfoot>
<tbody>
<tr>
<td>Cell 1.1</td>
<td>Cell 1.2</td>
<td>Cell 1.3</td>
</tr>
<tr>
<td>Cell 2.1</td>
<td>Cell 2.2</td>
<td>Cell 2.3</td>
</tr>
</tbody>
</table>
HTML Structure for Lists
<ul class="stripes">
<li>List Item 1</li>
<li>List Item 2</li>
</ul>

<ol class="stripes">
<li>List Item 1</li>
<li>List Item 2</li>
</ol>

<dl class="stripes">

<dt>Definition Term 1</dt>
<dd>Description 1.1</dd>
<dd>Description 1.2</dd>

<dt>Definition Term 2</dt>
<dd>Description 2.1</dd>
<dd>Description 2.2</dd>
<dd>Description 2.3</dd>

</dl>

You'll notice that with the <dl> tags you can specify more than one <dt>, and more than one <dd> per definition term. After the script runs, the tables and lists will be given this HTML structure:

Tables, after scripts.execute()
<table cellpadding="5" cellspacing="0" border="1" class="stripes">
<thead>
<tr>
<th>Heading 1</th>
<th>Heading 2</th>
<th>Heading 3</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Footer 1</th>
<th>Footer 2</th>
<th>Footer 3</th>
</tr>
</tfoot>
<tbody>
<tr class="rowA">
<td>Cell 1.1</td>
<td>Cell 1.2</td>
<td>Cell 1.3</td>
</tr>
<tr class="rowB">
<td>Cell 2.1</td>
<td>Cell 2.2</td>
<td>Cell 2.3</td>
</tr>
</tbody>
</table>
Lists after stripes.execute()
<ul class="stripes">
<li class="rowA">List Item 1</li>
<li class="rowB">List Item 2</li>
<li class="rowA">List Item 3</li>
</ul>

<ol class="stripes">
<li class="rowA">List Item 1</li>
<li class="rowB">List Item 2</li>
</ol>

<dl class="stripes">

<dt class="rowA">Definition Term 1</dt>
<dd class="rowA">Description 1.1</dd>
<dd class="rowA">Description 1.2</dd>

<dt class="rowB">Definition Term 2</dt>
<dd class="rowB">Description 2.1</dd>
<dd class="rowB">Description 2.2</dd>
<dd class="rowB">Description 2.3</dd>

</dl>

Styling the zebra stripes with CSS

With a small amount of CSS, you can style your zebra stripes.

.rowA {
background-color: #f0f0f0;
}

.rowB {
background-color: #d0d0d0;
}

You can change how tables and lists are displayed by using different CSS selectors:

tr.rowA {
background-color: #f0f0f0;
}

tr.rowB {
background-color: #d0d0d0;
}

li.rowA {
background-color: #fccccc;
}

li.rowB {
background-color: #dccccc;
}

dt.rowA {
background-color: #000;
color: #fff;
}

dd.rowA {
background-color: #505050;
color: #fff;
}

dt.rowB {
background-color: #ccc;
color: #000;
}

dd.rowB {
background-color: #d0d0d0;
color: #000;
}

Download the stripes JavaScript source code

  1. Click on the box below to highlight the code.
  2. Copy it.
  3. Paste it into a blank text file.
  4. Save it as package.stripes.js
  5. Include it in your HTML documents using a <script src=""> tag.

Who can use this script

The short answer? Anyone. It's released under the LGPL license and is free for commercial and non commercial use. Let's face it, this isn't ground breaking stuff here, so copy it. Distribute it. Use it. But always include some form of recognition and never claim it as your own. If you feel so inclined, link back to this page. If you have comments, suggestions or problems, post them below.