Friday, April 10, 2009

Dynamic DOM Node Element Creation in JavaScript

Lately I've been maintaining and upgrading a browser-based web app that consists mostly of JavaScript files. There's a lot of dynamic creation of DOM elements and I was getting tired of looking at line after line like this:

var elem=document.createElement('div');
elem.id = "mydiv";
elem['className'] = "padded label";
elem.appendChild(document.createTextNode("Customer Information "));
contentDiv.appendChild(elem);


That's just to create a single text node inside a div.

Or this kind of thing:

div.innerHTML="<b class="'smallBold'">Line Info:</b><br />" +
"<table class="'smallLeftTable'">" +
"<tr><th>Property</th>" + headers + "</tr>" +
"<tr><td>Site:</td>" + siteHTML + "</tr>" +
"<tr><td>Route:</td>" + routeHTML + "</tr>" +
"<tr><td>Pair #:</td>" + pairNumberHTML + "</tr>" +
"<tr><td>Pair Type:</td>" + pairTypeHTML + "</tr>" +
"<tr><td>Status:</td>" + statusHTML + "</tr>" +
"</table>";


That's even worse, I think. There's something buried deep within my psyche that makes me loathe using innerHTML.

So I decided I needed a slicker way to create DOM elements in my code. I remember reading that someone somewhere had created a function named $E()--in the spirit of prototype.js--for creating elements. I couldn't find that one though, so I wrote my own version of $E().

The HTML file below contains the code for $E() and some example usage, but it boils down to specifying the type of element to create, all of its attributes, and all of its child nodes in a single call. $E() then recursively builds the whole tree. The div with text, above, would be created like this:


var elem = $E('div', {id:"mydiv", className:"padded label"}, "Customer Information");


Remember when specifying a css class as an attribute, don't use the keyword "class". Use "className" instead.

Here's the HTML file containing $E(), its documentation, and examples. Go ahead and copy and paste it to a file on your system, pull it up in a browser, and start modifying and playing with it. I think you'll like how much smaller your .js files can be using $E().

<html>
<head><title>$E() Test</title>
<style type="text/css">
table.styled {
border-collapse: collapse;
background-color: yellow;
}
</style>
<head>
<body>

<!--
Here's the example table, created the old-fashioned way.
-->
<table id="table1" border="1">
<caption>Cups of coffee consumed by each senator</caption>
<tr>
<th>Name</th>
<th>Cups</th>
<th>Type of Coffee</th>
<th>Sugar?</th>
<tr>
<td>T. Sexton</td>
<td>10</td>
<td>Espresso</td>
<td>No</td>
<tr>
<td>J. Dinnen</td>
<td>5</td>
<td>Decaf</td>
<td>Yes</td>
</table>
<br/>
<br/>


<!--
This div is a root for the dynamically created tables.
-->
<div id="mydiv">
</div>


<!--
The rest of this file is the script that runs our demonstration.
-->
<script language="javascript">

//
// Here it is: $E()!
//
// Returns a DOM Element or an entire tree of DOM Elements.
// Usage:
// $E(text) -> Text node element
// $E(Element) -> Element
// $E(tag, attributes, children) -> Tree of DOM Elements
// Where:
// text = string
// Element = DOM Element
// tag = String -- The type of Element, e.g. "div"
// attributes = object -- The attributes of the Element, e.g. {id:"mydiv", height:"50px"}
// children = text | elem | list
// Where:
// text = String -- Appended as a Text node.
// elem = Element -- Appended
// list = Array -- A list of Strings or Elements (or both), appended
//
function $E() {
if (arguments.length == 1) {
if (typeof arguments[0] === 'string') {
return document.createTextNode(arguments[0]);
} else {
return arguments[0];
}
}
var elem = document.createElement(arguments[0]);
if (arguments[1]) {
for (key in arguments[1]) {
elem[key] = arguments[1][key];
}
}
if (arguments[2]) {
if (arguments[2] instanceof Array) {
for (var i = 0; i < arguments[2].length; i++) {
elem.appendChild($E(arguments[2][i]));
}
} else {
elem.appendChild($E(arguments[2]));
}
}
return elem;
}


//
// Begin the demonstration by getting a handle to the root
// div we created above.
//
var mydiv = document.getElementById("mydiv");


//
// This demonstrates coding the same table as above using $E()
// and nested calls to $E().
//
mydiv.appendChild(
$E("table", {id:"table2", border:"1"},
[$E("caption", null, "Same table using $E()"),
$E("tr", {}, [$E("th", null, "Name"),
$E("th", {}, "Cups"),
$E("th", {}, "Type of Coffee"),
$E("th", {}, "Sugar?")
]
),
$E("tr", {}, [$E("td", {}, "T. Sexton"),
$E("td", {}, "10"),
$E("td", {}, "Espresso"),
$E("td", {}, "No")
]
),
$E("tr", {}, [$E("td", {}, "J. Dinnen"),
$E("td", {}, "5"),
$E("td", {}, ["Decaf"]),
$E("td", {}, "Yes")
]
)
]));

// A little white space for looks.
mydiv.appendChild($E("br", {}, null));
mydiv.appendChild($E("br", null, []));


//
// Now the good stuff.
// Dynamically generate a table filled with data from an array.
//

//
// The data
//
data = [["T. Sexton", "10", "Espresso", "No"],
["J. Dinnen", "5", "Decaf", "Yes"],
["A. Johnson", "3", "Latte", "Yes"],
["B. Brown", "1", "Cappucino", "No"],
["C. Jones", "7", "Mocha", "Yes"]];

//
// Initialize the table
// Notice we use "className" (not "class") to set css class.
//
var table = $E("table", {id:"table2", border:"1", className:"styled"},
[$E("caption", null, "Same table using data from an array, "+
"with a few more rows and some style."),
$E("tr", {}, [$E("th", null, "Name"),
$E("th", {}, "Cups"),
$E("th", {}, "Type of Coffee"),
$E("th", {}, "Sugar?")
]
)]);

//
// Populate the table from our data array
//
for (var i = 0; i < data.length; i++) {
var tr = $E("tr", null, null);
for (var j = 0; j < data[i].length; j++) {
tr.appendChild($E("td", null, data[i][j]));
}
table.appendChild(tr);
}

//
// Append the newly created table to mydiv
//
mydiv.appendChild(table);

// A little more white space.
mydiv.appendChild($E("br", null, null));
mydiv.appendChild($E("br", null, null));

</script>
</body>
</html>

No comments: