Unspace

Home
Work
Projects

Discover
On the Road
Contact
Ruby on Rails Rails Pub Nite

Attributes > Classes: Custom DOM Attributes for Fun and Profit

Bio_pete
:: 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

Discover_attributes_robots 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:

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