The Lowdown
Here's a quick recap for those just joining us:
- — Stop generating JavaScript.
- — Instead, write your element relationships semantically in the DOM.
- — Use JavaScript to add behavior and interactions.
If that's not enough to get you going, go back and read Part I.
HTML5 data- attributes
Back in the days when most people were too busy hating JavaScript to write frameworks for it, the Dojo project pioneered a technique that embedded arbitrary custom attributes in HTML elements in order to apply advanced behaviors through JavaScript. At the time, this technique was largely panned, because we were all just starting to get our heads around the idea of web standards, which meant vehemently opposing anything that even remotely smacked of tag (or attribute) soup. ("That page doesn't validate? Horrors!")
As of a couple years ago, however, the W3C has blessed this technique in the form of
HTML5 data- attributes.
Finally, we can embed arbitrary data in HTML tags in a semantically-valid way. Partially because
this was added Internet-decades ago, and partially because it doesn't have the same glitz and
glamor (or associated drama) as <video />, <audio /> or
<canvas />, it's somewhat fallen by the wayside of public mindshare
compared to other HTML5 features. Make no mistake, however: this is a pretty big deal.
Starting with the example from Part I, suppose we wanted certain links to trigger animated
transitions when they updated their targets. We can add, for example, a
data-transition attribute to the <a /> element:
<?=$this->html->link("Hello World", array("Posts::view", "id" => 5), array(
"target" => "#display",
"data-transition" => "slideDown"
)); ?>
<div id="display"></div>
Again, since we have one global behavior rule, we simply update it in one place, and any matched element will inherit the behavior:
<script type="text/javascript">
$("a[target]").click(function(e) {
var a = this;
var transition;
$.get(a.href, function(data) {
$(a.target).html(data);
if (transition = $(a).attr('data-transition')) {
$(a.target).hide()[transition]();
}
});
e.preventDefault();
});
</script>
Using these illustrations, we can begin to see how it is possible to move away from manually coding specific interactivity for individual page elements, and towards a more structured, architected approach that focuses on planning out the general "classes" of behavior that your application will use.
Conveniently, this actually helps to force you into planning your app's various interactions out ahead of time. This not only saves on development and maintenance time, but improves the consistency of the user's experience.
From a maintenance and extensibility standpoint, this last example in particular demonstrates where the techniques of architecting semantic markup really excel compared to using classes or IDs, which are ill-suited to the key/value data structures necessary to describe more complex relationships and interactions. Furthermore, these attributes are fully supported by jQuery for querying and manipulation.
Next steps
These are some techniques I'm still experimenting with, but my goal is to find the appropriate balance between markup-embedded data coupled with generic behavior code, and purpose-specific code.
One example in my own work is a user registration form containing a jQuery UI slider control
that users can manipulate to indicate their income level. This slider has two behaviors: the
first is to update a <span /> element that shows the value of the slider; the
second is to animate a pile of money that changes as the user moves the slider up and down. The
markup listing is as follows:
<div class="income_wrapper">
<div class="income_slider_wrapper">
<div id="income_slider"></div>
<div id="income_value_wrapper">
$<span id="income_value"></span> USD a year.
</div>
</div>
<div class="income_visual">
<div class="income_base"></div>
<div class="income_2"></div>
<div class="income_3"></div>
<div class="income_4"></div>
</div>
</div>
And the code that creates the slider and configures the animation:
<script type="text/javascript">
$('#income_slider').slider({
min: 0,
max: 140000,
step: 10000,
value: 20000,
slide: function(event, ui) {
$("#income_value").html(ui.value);
},
change: function(event, ui) {
data.income = ui.value;
var v = ui.value;
if (v >= 30000){
$('.income_2').fadeIn();
} else {
$('.income_2').fadeOut();
}
if (v >= 60000){
$('.income_3').fadeIn();
} else {
$('.income_3').fadeOut();
}
if (v >= 70000){
$('.income_4').fadeIn();
} else {
$('.income_4').fadeOut();
}
}
});
</script>
The first round of refactoring is simple: we can add a target attribute to
#income_slider to move the relationship between the slider and the display out of
JavaScript:
...
<div id="income_slider" target="#income_value"></div>
<div id="income_value_wrapper">
$<span id="income_value"></span> USD a year.
</div>
...
<script type="text/javascript">
// ...
slide: function(event, ui) {
$($(this).attr('target')).html(ui.value);
},
// ...
</script>
The next round is a bit more complex. We can add data- attributes for each of the
slider's properties (min, max, etc.), but handling the animation isn't
as obvious. The technique I'm currently experimenting with is a key/value map of JavaScript
functions that keeps all purpose-specific behavior related to a page or set of pages in one
place. Here's the entire markup listing, redone with data- attributes:
<div class="income_wrapper">
<div class="income_slider_wrapper">
<div
id="income_slider"
target="#income_value"
data-input-type="slider"
data-min="0"
data-max="140000"
data-step="1000"
data-value="20000"
data-trigger="slider.income"
></div>
<div id="income_value_wrapper">
$<span id="income_value"></span> USD a year.
</div>
</div>
<div class="income_visual">
<div class="income_base"></div>
<div class="income_2"></div>
<div class="income_3"></div>
<div class="income_4"></div>
</div>
</div>
Trigger-ing behavior
Note the data-trigger attribute: we'll refer back to it later when we tie the
slider back to the animation function. Here, the JavaScript listing has split into two parts,
with the irrelevant portions omitted:
<script type="text/javascript">
var Triggers = {
'slider.income': function(event, ui) {
data.income = ui.value;
var v = ui.value;
var anim = [
{ selector: '.income_2', value: 30000 },
{ selector: '.income_3', value: 60000 },
{ selector: '.income_4', value: 70000 }
];
for (var i = 0; i < anim.length; i++) {
$(anim[i].selector)[v >= anim[i].value ? 'fadeIn' : 'fadeOut']();
}
},
// ...
};
// ...
$('[data-input-type=slider]').each(function() {
$this = $(this);
trigger = $this.attr('data-trigger');
change = (typeof Triggers[trigger] != "undefined") ? Triggers[trigger] : null;
$this.slider({
min: parseInt($this.attr('data-min')),
max: parseInt($this.attr('data-max')),
step: parseInt($this.attr('data-step')),
value: parseInt($this.attr('data-value')),
change: change,
slide: function(event, ui) {
$($(this).attr('target')).html(ui.value);
}
});
$($this.attr('target')).html($this.attr('data-value'));
});
</script>
First, note the Triggers object. This is essentially a hash map containing our
custom animation function (which has itself been rewritten as a hash map and a loop, so as to
eliminate any code duplication), and any other custom code to be associated with page elements.
You'll notice that the key name matches the value from data-trigger in the markup
listing.
This is looking quite a bit better, however, we now have two problems. The first is the repetitive pattern used to map attributes to slider options, and the second is that, for each new option to be mapped, we have to go back and write additional code. To solve this, I wrote a simple jQuery plugin to extract out these attributes by name, or the entire array, in a single call. It also detects boolean, integer and floating-point values, and casts them accordingly. The entire listing is as follows:
Editor's note: Since this post was originally written (yeah, it was a while ago), jQuery
itself has introduced support for HTML5 data- attributes in its
data() method. Including the below,
it also supports the conversion of complex values, such as objects, arrays and
null. As such, the below should be considered for educational purposes only.
(function($) {
$.fn.dataAttrs = function() {
var attrs = $(this).get(0).attributes;
var result = {};
var value;
for (var i = 0; i < attrs.length; i++) {
if (attrs[i].name.indexOf('data-') === 0) {
value = attrs[i].value;
if (value === 'true') {
value = true;
} else if (value === 'false') {
value = false;
} else if (value.match(/^[0-9]+\.[0-9]+$/)) {
value = parseFloat(value);
} else if (value.match(/^[0-9]+$/)) {
value = parseInt(value);
}
result[attrs[i].name.replace(/^data-/, '')] = value;
}
}
return result;
}
})(jQuery);
As you can see, it's pretty straightforward. The function iterates over an element's properties,
finding all the ones prefixed with data-, and attempting to coerce the contents of
each to native values. It then returns them in a map, with prefixes stripped. Again, you
don't really want to use this. All subsequent code examples have been updated to make use of
jQuery's new APIs.
Now, since the slider's properties are completely data-driven in a far more generic way, our custom code is greatly simplified:
$('[data-input-type=slider]').each(function() {
$this = $(this);
var options = $this.data();
var key = options.trigger;
var handlers = {
change: (key && Triggers[key]) ? Triggers[key] : null,
slide: function(event, ui) {
$($(this).attr('target')).html(ui.value);
}
};
$this.slider($.extend({}, handlers, options));
if (options.value) {
$($this.attr('target')).html(options.value);
}
});
Instead of mapping each option individually, all options are extracted and coerced to native types in one fell swoop. The only thing left is to set up the event handlers, but even this is mostly a simple mapping job. Again, by wiring event handlers this way, we've created a consistent development experience for front-end engineers, who now have a "framework" for creating other interface elements of the same type.
You can never eat just one
But what happens when conventions are too constraining? Suppose an instance occurs where changes
in a slider's value need to update multiple independent things on a page? What if we need to
handle more than one event? Since data-trigger is just one key, we need a new
convention, or set of conventions, for making our ability to specify interactions more
flexible.
One possible solution is to have multiple data-trigger- keys, each named for the
event binding associated. This is a big improvement, since we can now take advantage of the full
spectrum of the events exposed by an element.
Next, instead of each key being a single value, we expand out to allow
each key to be an array, specifying mulitple independent events.
<div
data-input-type="slider"
target="#income_value"
data-min="0"
data-max="140000"
data-step="1000"
data-value="20000"
data-trigger-slidechange='["slider.income", "profile.update"]'
data-trigger-slide="slider.value"
></div>
Here, we've replaced the data-trigger key with two new, independent keys:
data-trigger-slidechange and data-trigger-slide. Also note that
because the data-trigger-slidechange attribute is a JSON array, it uses single
quotes to wrap the value. In order for the data() method to parse JSON values
properly, they must be well-formed, which means using double quotes for the JSON-encoded values
in the array. Also note that the key name is slidechange, as opposed to
change. This is to match the jQuery UI slider's bind() API, so that we
can separate event binding from object creation, as we'll see later.
You'll also notice that within our trigger naming scheme, we've introduced a non-slider-specific
event, "profile.update". This event will be shared by mutiple independent UI
elements, and will update our (hypothetical) user profile display.
Since previously, we were simply wiring up events by name using a single hard-coded key, we now
need some additional logic to handle the new key binding conventions. Up till now, the
Triggers object has been a simple container for named events. However, it's also
the ideal place to start adding event-binding logic. We'll add a bind() method
which will take one parameter: the object being handled. It'll use the object's
data-trigger- attributes to figure out what events to attach.
<script type="text/javascript">
var Triggers = {
// ...
bind: function($object) {
var triggers = this;
$.each($object.data(), function(key, val) {
if (key.indexOf('trigger-') !== 0) {
return;
}
var name = key.replace('trigger-', '');
if (!$.isArray(val)) {
return $object.bind(name, triggers[val]);
}
for (var i = 0; i < val.length; i++) {
$object.bind(name, triggers[val[i]]);
}
});
}
};
// ...
</script>
What we're doing here is pretty straightforward. The bind() method iterates over
the object's data- attributes, picks out the trigger-'s, matches up
the values to named functions within the Triggers object, and binds them to the
object. An added benefit of this abstraction is that we no longer have to manually tie in the
code that updates the slider's label with the code that creates the slider: it's just another
function in the Triggers collection.
<script type="text/javascript">
var Triggers = {
// ...
'slider.value': function(event, ui) {
$($(this).attr('target')).html(ui.value);
},
// ...
};
// ...
</script>
Finally, we come back to the slider-creation selector, which is just a shadow of its former self:
<script type="text/javascript">
// ...
$('[data-input-type=slider]').each(function() {
var $this = $(this);
var options = $this.data();
$this.slider(options);
Triggers.bind($this);
if (options.value) {
$($this.attr('target')).html(options.value);
}
});
</script>
Gotta keep 'em separated
What's been demonstrated here is one of the most important principles of good software design: the separation of concerns. Even though this is a subject that deserves several blog posts on its own, it's worth mentioning here. For now, an easy way to use this principle to improve your code is to remember that one thing should do one thing.
This is especially important when dealing with jQuery, as it makes it easy for one thing to do many things, as we saw in the first couple iterations of the slider code. In addition to first setting, then manually parsing configuration values, it also handled creating and binding events.
Fundamentally, the job of the slider-creating code is just that: to create sliders. In the final iteration, the slider code falls within its proper, well-defined role, and other tasks like the lookup and parsing of values, the lookup and binding of events, etc., have been delegated either to jQuery, or to other objects. This also extends to the event handlers, which have been cleanly separated into multiple independent named triggers. In addition to improving coherence and the self-describing nature of the code, it also means less code duplication, as smaller triggers can be mixed and matched in more places.
In conclusion
Hopefully this has given you a good understanding of how to really use the DOM to your advantage when designing your app front-ends, and how a little bit of planning pays dividends in code you don't have to write.
And hopefully now, whenever someone asks you, "semantic markup or death?", you'll be able to confidently answer...
Uh, death, please. No, markup, markup, sorry!You said death first, haaaah! Death first!No, I meant markup!Ohhh, alright. You're lucky I'm Church of England!
Seriously, you have to watch the video.
Feedback
If you have comments, questions, corrections, suggested improvements, etc. on this post, feel free to @ me. If you have critiques of the ideas presented, or you'd like to share your own strategies, write a blog post, send me a tweet with the link, and I'll post it below. Thanks again to John Kary for his improvements on Part I.