Extending the JavaScript DOM Interfaces in Mozilla and Other Modern Browsers.
Back in the day, when writing JavaScript for a sassy DHTML feature, I often found that about 80 percent of my coding time was spent writing DOM traversal methods… you know what I’m talking about:
/**
* @param Event evt
* @return void
*/
function linkClicked(evt) {
var source = getCrossPlatformEventSource(evt);
var parent = source;
while (parent = parent.parentNode) {
if (hasClassName(parent,'myclass')) {
break;
}
}
doSomethingWithThisNode(parent);
}
Oh the tedium! See that messy algorithm in the middle that’s searching for the ancestor with a specific HTML class attribute? How many times have you written those same five lines in your life? Too many! So, eventually, I created a library of reusable DOM algorithms. The benefits? Code reuse, code clarity, more productivity, and fewer bugs. See…
/**
* @param Element target
* @param string className
* @return Element
*/
function getFirstAncestorByClassName(target,className) {
var parent = target;
while (parent = parent.parentNode) {
if (hasClassName(parent,className)) {
return parent;
}
}
return null;
}
/**
* @param Event evt
* @return void
*/
function linkClicked(evt) {
var source = getCrossPlatformEventSource(evt);
var parent = getFirstAncestorByClassName(source,'myclass');
doSomethingWithThisNode(parent);
}
Doesn’t that read so much more nicely? Quick diversion: What’s that hasClassName() function all about? Glad you asked… the HTML spec allows any element’s class attribute to contain multiple space-separated class names. These are usually used as hooks to CSS styles, or, as above, helper flags for JavaScript DOM traversal. Since any element is allowed multiple class names, it’s not truely safe to test the className property of a JavaScript Element instance as you often see:
if ('myClass' == element.className) {
doSomethingWithThisNode(element);
}
If myClass is just one of multiple class names associated with this element through it’s HTML class attribute, the JavaScript expression will evalutate to false, and the if statement will fail in a very non-obvious way! You’re looking at the source of an insidious bug that could linger for weeks or longer.
Here’s another instance where creating a reusable function makes sense… Extract the logic to check the element’s className property (which is of type string) for the existance of the class name of interest:
function hasClassName(element,className) {
if (element.className.indexOf(className) > -1) {
return true;
}
return false;
}
Explicitly returning false at the end of this function is not strictly necessary in JavaScript, but the Java developer in me tells me it’s a good idea for code clarity. Now you have a beautifully reusable little chunk of code.
Okay, so any decent programmer reading about this code reuse and shortening of methods, and is like, “Um, that’s nice… but this is programming 101 stuff.” But here’s the really cool part… in Mozilla you can extend the JavaScript DOM interfaces like
Node, Element, Document, NodeList, etc, by adding new methods just like any other JavaScript class…. by adding functions to the interface’s prototype object:
/**
* @param string className
* @return Element
*/
Element.prototype.getFirstAncestorByClassName = function (className) {
var parent = this;
while (parent = parent.parentNode) {
if (hasClassName(parent,className)) {
return parent;
}
}
return null;
};
Then, you can invoke this method on any Element reference anywhere in your JavaScript code:
var target = document.getElementById('myId');
var nodeOfInterest =
target.getFirstAncestorByClassName('coolClassName');
WOW! Now that’s cool… Here’s the bad news… it only works in Gecko-based browsers (you know… Firefox, Mozilla, Chimer– I mean Camino and the like). The good news is that there is a way to make this work in a few other modern browsers (Safari and Opera 7 to be exact). Just add the method to the JavaScript base Object class’ prototype… This way, the methods are inherited by all objects, including the implementation classes for the DOM interfaces. Granted, you’re now adding the DOM traversal methods to every object ever, but c’mon, this is JavaScript! Do you really expect things to be that elegant?
/**
* @param string className
* @return Element
*/
Object.prototype.getFirstAncestorByClassName = function (className) {
var parent = this;
while (parent = parent.parentNode) {
if (hasClassName(parent,className)) {
return parent;
}
}
return null;
};
The memory overhead for this trick may not be as bad as you think… since JavaScript uses prototype-based inheritance, every object instance created will not contain it’s own copy of this method. Instead, the method still lives in only one place… in the base Object class’ prototype object. I can’t necessarily confirm this, and it may vary between JavaScript interpreters in the different browsers, but this explanation seems most likely. I’ll leave it as an exercise to the reader to discover this for sure. Any ideas on how to test this?
So here’s the bad news: This trick doesn’t work in IE 6… it seems that the DOM interfaces like Element don’t extend the base Object class like every other class in JavaScript according to the ECMA spec. Therefore, they don’t inherit methods from Object’s prototype object. I’ve tried several hacks to make this work in IE 6, but so far, no luck. Any more ideas?
There are a couple of other interesting things you can do with this idea though…
So the JavaScript DOM bindings are pretty naughty when you think about it… What’s wrong with this statement?
var val = myNode.nodeValue;
var el = document.documentElement;
If you’re a Java developer, that line has probably left you twitching… direct access to member variables? Are you kidding? What is this? JavaScript?
The much more refined Java bindings for DOM look like this:
String val = myNode.getNodeValue();
Element el = document.getDocumentElement();
That’s more like it! Well, now you can have these niceties in JavaScript too! (Assuming your userbase doesn’t include IE 6, and that should never be a problem… wait, nevermind)
/**
* @return string
*/
Node.prototype.getNodeValue = function () {
return this.nodeValue;
};
/**
* @return Element
*/
Document.prototype.getDocumentElement = function () {
return this.documentElement;
};
var val = myNode.getNodeValue();
var el = document.getDocumentElement();
That’s neat! And remember that nice hasClassName() method we created earlier? Let’s make that a little more object-oriented:
/**
* @param className string
* @return boolean
*/
Element.prototype.hasClassName(className) {
if (this.className.indexOf(className) > -1) {
return true;
}
return false;
};
var el = document.getElementById('myId');
if (el.hasClassName('myClassName')) {
doSomethingWithThisElement(el);
}
The possibilities are endless, and your JavaScript code becomes more elegant, more object-oriented, less vulnerable to bugs, and more readable. That is… so long as you don’t have to support IE
About this entry
You’re currently reading “Extending the JavaScript DOM Interfaces in Mozilla and Other Modern Browsers.,” an entry on Todd Ditchendorf’s Blog.
- Published:
- 02.19.05 / 9pm
- Category:
- Java, JavaScript/DHTML, XML
14 Comments
Jump to comment form | comments rss [?] | trackback uri [?]