Attributes > Classes: Custom DOM Attributes for Fun and Profit

:: Development
January 9, 2007 - over 3 years ago
Platform: We like Rails
Tested Browsers: Firefox 1.5+, IE 6+, Safari 2+
Tag As: JavaScript, Ajax, DOM, scripting
Editor: Liz Clayton
Technical Review: Jeff Hardy,
Dan Grigsby,
Illustration: Anthony Watts
Spread The Word: Digg,
del.icio.us
Javascript Download: here - version 1.00
When we learned HTML, we read that it was a subset of
SGML.
This detail was presented as a historical footnote. Another subset of SGML, this scary thing
called XML — Big Enterprises use it — rolled out in the 1990s. When we moved to
XHTML Transitional, there was never any sense that something might have been
lost in the translation, or that there could be a bigger payoff if we stopped to
appreciate the big picture. There's more to XHTML than lower case text
and religiously closing tags.
Getting designers to value web standards in their work has been about a lot more than whatever gratification comes from seeing your work pass W3C validation tests. This current second era of web growth seems to be anchored by people that understand once-esoteric concepts like the separation of content and style.
Yet it is strange that in a web so self-obsessed with meta-data — "my tags have RSS feeds!" — we are averse to using one of the most powerful features of XHTML: the ability to extend the nodes we use with customized, semantically meaningful attributes. As long as we continue to see our markup language as static and inflexible, we're still bringing our bad habits and assumptions with us. To embrace XHTML is to realize that we can bend the nature of our markup. If Rails applications are domain-specific languages for the server, then XHTML empowers us to be the masters of our own client-side domain as well.
I remain unconvinced.
My JavaScript weapon of choice is Prototype, and like most toolkits, it has embraced the class attribute. There is a moment of Zen when you realize that an XHTML element can have multiple classes at the same time — simply specify a space-separated list. Classes can correspond to CSS classes on a 1:1 basis, or they could be keywords that imply meta-data. You can use a combination of both to implement rich functionality on the client. Prototype provides a set of tools to add, remove, query and reset the classes attached to a node. Life is grand — right?
To date, I have not drunk the class Kool-Aid. Molly Holzschlag assures me that the class attribute is to be used for visual as well as non-visual classes, yet I still have three primary reasons for questioning this approach:
- If you use classes to imply application functionality, you cannot tell at a glance if your class is defined in CSS as a style, or a facet of your application-specific code. This will confuse your design team tomorrow and your future self in six months. Attribute name value pairs are easier to read and understand. CSS and attributes, working together at last.
- Classes can be used to allow grouping of arbitrary elements that are
unconnected by hierarchy. Attributes allow the same logical grouping, but in a
name/value pair format that follows the syntax of native attributes like
id and value. In short: only attributes allow you
to associate a value with your meta-data. The ambiguous subtotal class
requires currency parsing:
<input type="text" class="subtotal" value="$1,234" />
The subtotal attribute provides logical grouping and an integer value:
<input type="text" subtotal="1234" value="$1,234" /> - The methods Prototype provides to manage the classes attached to an element are nothing more than abstraction wrappers that split the value of the class attribute and build an array every time you need to access it. To me, this sounds and feels dirty.
After all of Dan Webb's hard work promoting the concept of Unobtrusive JavaScript — where we eschew inline event handlers in favor of event listeners defined in code — why are we taking a step backwards, by putting application logic inside of something most web developers associate with being a visual characteristic?
Extending XHTML elements in my projects has made me think of how to solve problems in new and better ways. It has made me a stronger JavaScript artisan.
I'm still here. Show me awesome stuff, now.
As an example, here we can iterate through INPUT elements, which are to be sub-totaled. By storing the price amounts as an integer (or float) we can operate on, sort, or select elements without having to parse out any displayed currency formatting:
<input type="text" subtotal="13488" value="$13,488" />
<input type="text" subtotal="9534" value="$9,534" />
<script type="text/javascript">
var total = 0;
document.getElementsByAttribute('subtotal').pluck('subtotal').each(function(value) {
if (!isNaN(parseInt(value)))
total += parseInt(value);
});
alert(total);
</script>
Of course, you'll quickly see shortcuts you might enjoy:
function $P(property) {
return document.getElementsByAttribute(property).pluck(property);
}
$P('subtotal') // => ["13488","9534"]
$P('subtotal').sum() // => 23022
Another example would be a list of neighbourhoods in a SELECT element. The OPTION nodes could feature an attribute called city that would store the correct database identity value of the city where they are located. After loading the cities into a second SELECT element, your application can then update the city SELECT to reflect the currently chosen neighbourhood.
Interesting — keep going.
I did promise problem-domain-specific excitement, remember? I'm no tease.
Implementing unobtrusive event handlers is a snap. Why not take it a step further and use application-specific semantics to define what events you wish to observe?
In this example, we start to build an interface based on the Live Filter pattern. Different form elements have different events that are desirable to catch:
<select livefilter="change"><option>1</option><option>2</option></select>
<input type="text" livefilter="keypress" />
<button type="button" livefilter="click">Action!</button>
<script type="text/javascript">
document.getElementsByAttribute('livefilter').each(function(obj) {
Event.observe(obj, obj.getAttribute('livefilter'), refresh, false);
});
function refresh(e) { alert(e.target.nodeName + ' triggered!'); }
</script>
Each element can opt-in to the Live Filter pattern and specify which event should be monitored. Nifty and logical!
How does this work?
Custom attributes work in all browsers that support XHTML. You do not have to define a custom DTD or sacrifice any children to use them.
We do have to make some changes to the way Prototype works, though. With the addition of a few functions to our application.js, we suddenly have the same flexibility that we had with Element.classNames.
document.getElementsByAttribute = function(attribute, parentElement) {
var children = ($(parentElement) || document.body).getElementsByTagName('*');
return $A(children).inject([], function(elements, child) {
if (child.getAttribute(attribute))
elements.push(Element.extend(child));
return elements;
});
}
Object.extend(Enumerable, {
pluck: function(property) {
var results = [];
this.each(function(value, index) {
results.push(value.getAttribute ? value.getAttribute(property) || value[property] : value[property]);
});
return results;
}
});
Object.extend(Array.prototype, Enumerable);
We have to be able to obtain an array of XHTML elements that have an attribute defined. document.getElementsByAttribute is derived from Prototype's document.getElementsByClassName and inherits the ability to accept an optional second parameter to start recursion below a specified element, for performance reasons.
Pluck is a really cool method that is part of Prototype's Enumerable class. In its original implementation, it iterates through an array and returns a new array containing the specified attributes from each element. We love pluck — but we need it to operate on our custom attributes in addition to the native attributes.
The power of JavaScript really shines through here; much like in Ruby, we can "monkey patch" functions in the Prototype library. We can add the functionality we crave without modifying the original script. This approach means that we can continue to upgrade to new versions and not have to update our code or step on anyone's toes.
We finish by appending the newly extended Enumerable class onto the basic Array type. All future arrays will now support plucking our custom attributes — and we didn't need anyone's permission to do it.
In conclusion, custom attributes help separate the concerns of behavior and presentation. They are easier for humans to parse, and thanks to a few quick additions to Prototype, easy to implement.
Holy hell! My tag clouds have blogs!
You need help.
Javascript Download: here - version 1.00
Spread The Word: Digg,
del.icio.us
