-
Get a monthly update on best practices for delivering successful software.
The HTML5 custom data proposal received a lot of attention last month, but I forgot to post about it during my preparations for Web 2.0 Expo. Reviewing the proposal now, I'm excited all over again. After doing lots of jQuery work lately, I'm sick and tired of the poor HTML "class" attribute getting used and abused for unobtrusive JavaScript. I think HTML5 custom data gives us a graceful way out.
First, a little background. Simon Willison, as usual, boils the issue down to its essence, succinctly quoting the proposal and nailing its implications in two sentences:
"Every HTML element may have any number of attributes starting with the string 'data-' specified, with any value." [T]his will be incredibly useful for unobtrusive JavaScript where there’s no sensible place to store configuration data as HTML content. It will also mean Dojo has an approved method for adding custom attributes to declaratively instantiate Dojo widgets.
For further background, including great debate in the comments, check out these blog posts and official documents:
If you've never used custom properties (sometimes known as expandos) on your HTML elements, it's worth looking at how the technique has evolved over time. (Old pros can stop reading here.)
As an example, let's say you're writing a JavaScript validation routine that parses a form, determines which fields are required and asks the user to fill them out using a friendly, human-readable name for each field.
You could simply hard-code the custom data for each form field into your JavaScript, leaving the HTML completely untouched. Circa 2001, your code would have looked something like this:
<form id="myform"> First Name<br /> <input type="text" name="firstName" id="firstName" /> Middle Name<br /> <input type="text" name="middleName" id="middleName" /> Last Name<br /> <input type="text" name="lastName" id="lastName" /> Phone Number<br /> <input type="text" name="phone" id="phone" /></form><script type="text/javascript"> var requiredFields = [['firstName','First Name'] ,['lastName','Last Name'] ,['phone','Phone Number'] ]; for (var i = 0, j < requiredFields.length; i < j; i++) { var fld = document.getElementById(requiredFields[i][0]) fld.required = true; fld.displayText = requriedFields[i][1]; } var theForm = document.getElementById("myform"); for (var i = 0, j < theForm.length; i < j; i++) { var theField = theForm[i]; if (theField.required && theField.value == "") { alert("Please fill in field " + theField.displayText); } }</script>
But that's a brittle approach, one that doesn't lend itself to templatized UI code. The code that associates meta-data with HTML elements lives in a different place than the field itself, which is difficult to maintain and prone to errors.
Another approach would be to include a script block after each form element. That way, despite a lot of extra typing (and wasted bandwidth for the user), you could keep a form field and its meta-data in the same modular JSP/PHP/ASP template. Circa 2003, your code might have looked like this:
<form id="myform"> <!--module 1--> First Name<br /> <input type="text" name="firstName" id="firstName" /> <script type="text/javascript"> fld = document.getElementById("firstName"); fld.required = true; fld.displayText = "First Name"; </script> <!--/module 1--> <!--module 2--> Middle Name<br /> <input type="text" name="middleName" id="middleName" /> <script type="text/javascript"> fld = document.getElementById("middleName"); fld.displayText = "Middle Name"; </script> <!--/module 2--> <!--module 3--> Last Name<br /> <input type="text" name="lastName" id="lastName" /> <script type="text/javascript"> fld = document.getElementById("lastName"); fld.required = true; fld.displayText = "Last Name"; </script> <!--/module 3--> <!--module 4--> Phone Number<br /> <input type="text" name="phone" id="phone" /> <script type="text/javascript"> fld = document.getElementById("phone"); fld.required = true; fld.displayText = "Phone Number"; </script> <!--/module 4--> <script type="text/javascript"> var theForm = document.getElementById("myform"); for (var i = 0, j < theForm.length; i < j; i++) { var theField = theForm[i]; if (theField.required && theField.value == "") { alert("Please fill in field " + theField.displayText); } } </script></form>
Then there's the expando approach, which takes advantage of the fact that modern rendering engines will ignore HTML attributes they don't understand. Circa 2007, your code might have looked like this:
<form id="myform"> First Name<br /> <input type="text" name="firstName" id="firstName" metadata="{required: true, displayText: 'First Name'}"/> Middle Name<br /> <input type="text" name="middleName" id="middleName" metadata="{displayText: 'Middle Name'}"/> Last Name<br /> <input type="text" name="lastName" id="lastName" metadata="{required: true, displayText: 'Last Name'}"/> Phone Number<br /> <input type="text" name="phone" id="phone" metadata="{required: true, displayText: 'Phone Number'}"/></form><script type="text/javascript"> var theForm = document.getElementById("myform"); for (var i = 0, j < theForm.length; i < j; i++) { var theField = theForm[i]; if (theField.required && theField.value == "") { alert("Please fill in field " + theField.displayText); } }</script>
As you can see, we're setting the non-standard attribute of "metadata" and using a JSON string to populate it. The advantages are clear: brevity and expressiveness. But the disadvantages - non-validating HTML or the need to create a custom DTD and validate against that - are daunting.
This fear of violating web standards has led to the current state of the art: the overloading of existing HTML attributes - especially "class" - with meta-data:
<form id="myform"> <label for="firstName">First Name</label> <input type="text" name="firstName" id="firstName" class="required"/> <label for="middleName">Middle Name</label> <input type="text" name="middleName" id="middleName" id="Middle_Name" /> <label for="lastName">Last Name</label> <input type="text" name="lastName" id="lastName" class="required"/> <label for="phone">Phone</label> <input type="text" name="phone" id="phone" class="required"/></form><script type="text/javascript"> var theForm = document.getElementById("myform"); for (var i = 0, j < theForm.length; i < j; i++) { var theField = theForm[i]; theField.displayText = theField.getPreviousSibling('label').getInnerText(); if (theField.hasClass("required") { theField.required = true; } } for (var i = 0, j < theForm.length; i < j; i++) { var theField = theForm[i]; if (theField.required && theField.value == "") { alert("Please fill in field " + theField.displayText); } }</script>
Sure, there are some advantages here. The ascendency of semantic markup (the label element) has freed us from the need to store our human-readable strings as meta-data. And thanks to the advent of JavaScript frameworks, we've got nice convenience methods (such as my imaginary getPreviousSibling and getInnerText functions) to grab content from neighboring DOM nodes.
But we've also got a huge disadvantage: the need to find existing HTML attributes in which to sock away those properties that are truly meta-data (such as our required flag). This isn't such a problem when you're using only a few "magic" classes to trigger your unobtrusive JavaScript.
But in more complex code, you end up with 10 different classes applied to the same element, each one triggering a different piece of JavaScript functionality. Sometimes you have to get creative with other properties, too, such as the "rel" and "title" attributes of link elements.
It goes without saying that such attributes don't lend themselves very well to complex values. Let's say you wanted to associate an element with the regular expression that would be used to validate it. It's pretty hard to set a class of "/^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?(?:[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d*))?)?([Zz])?(?:([+-])(\d{2}):(\d{2}))?$/" - no?
Assuming the HTML5 custom data proposal gets ratified (or just supported by the major browsers), our future code could look like this:
<form id="myform"> <label for="firstName">First Name</label> <input type="text" name="firstName" id="firstName" data-required="true"/> <label for="middleName">Middle Name</label> <input type="text" name="middleName" id="middleName" id="Middle_Name" /> <label for="lastName">Last Name</label> <input type="text" name="lastName" id="lastName" data-required="true"/> <label for="phone">Phone</label> <input type="text" name="phone" id="phone" data-required="true" data-valpattern="/\$\{([^\}]+)\}/g"/></form><script type="text/javascript"> var theForm = document.getElementById("myform"); for (var i = 0, j < theForm.length; i < j; i++) { var theField = theForm[i]; if (theField.dataset.required && theField.value == "") { alert("Please fill in field " + theField.getPreviousSibling('label').getInnerText()); } }</script>
Voila: declarative instantiation; markup that validates; the ability to associate complex data, rather than simple strings, with an element; and a class attribute that only gets set when you've got styles to apply. Isn't that a relief?
Related posts:
Topics: Ajax Frameworks, Web Standards
[...] Pathfinder Development » HTML5 custom data: A possible savior for the poor, overloaded class attrib… The HTML5 custom data proposal received a lot of attention last month, but I forgot to post about it during my preparations for Web 2.0 Expo. (tags: http://www.pathf.com 2008 mes4 dia25 at_home html5 attributes *****) [...]
Pingback by rascunho » Blog Archive » links for 2008-05-26, Monday, May 26, 2008 @ 3:36 pm
I know this post is old, but I wanted to mention a Rails plugin I just finished that mimics “The slightly less crusty old-school method” mentioned in your post. I implemented it using the somewhat recent data storage methods of jQuery, Prototype and Mootools.
http://github.com/CodeOfficer/js-data-helper/tree/master
Comment by Russell Jones, Sunday, May 10, 2009 @ 9:17 pm