The title of this post is inspired by one of the best monologues in the history of stand-up comedy. If you haven't seen it, and you happen to enjoy comedy, you owe yourself the next 6 minutes. I'll wait.
tl;dr
This entire post, in 3 bullet points:
- — Stop generating JavaScript.
- — Seriously, knock it off. If you find yourself embedding server-side code in blocks of JavaScript, you are almost certainly doing it rong.
- — Instead of hand-coding every single interaction with custom JavaScript, writing general behavior code and describe the details in your markup.
If this sounds interesting, read on.
The problem
Despite the title, this post is actually mostly about JavaScript, or more specifically, the relationship between JavaScript and HTML. Way too often I see stuff like this:
<a href="/posts/view/5" id="view">Hello World</a>
<div id="display"></div>
<script type="text/javascript">
$("#view").click(function(e) {
$.get("/posts/view/5", function(data) {
$("#display").html(data);
});
e.preventDefault();
});
</script>
Essentially, all this code is accomplishing is loading the contents of a URL into a
<div /> when a link is clicked. Pretty simple, right? Unfortunately, not only
does the above happen way too often, but complicating matters is the fact these days, much of
our applications' markup is generated by a templating engine or framework, so you get something
more like this:
<?=$this->html->link("Hello World", array("Posts::view", "id" => 5), array(
"id" => "view"
)); ?>
<div id="display"></div>
<script type="text/javascript">
$("#view").click(function(e) {
$.get(
"<?=$this->url(array("Posts::view", "id" => 5)); ?>",
function(data) {
$("#display").html(data);
}
);
e.preventDefault();
});
</script>
Now, routing is great because it de-couples the application's URL scheme (or
information architecture,
if you will) from its underlying structure. However, the way its written (and this is how I
often see it written) duplicates data, and results in the ugly construct of code-within-code.
Server-side frameworks are great at generating markup, but when it comes to JavaScript, not so
much. While we often mix server-side code and JavaScript, some (myself included) have tried to
write server-side frameworks that actually generate whole blocks of JavaScript. However, this is
pretty widely acknowledged to have been a failed experiment.
Getting back to our refactoring, the simple and obvious win here is to just ask the
<a /> element what it's pointing at, rather than duplicating its URL:
<?=$this->html->link("Hello World", array("Posts::view", "id" => 5), array(
"id" => "view"
)); ?>
<div id="display"></div>
<script type="text/javascript">
$("#view").click(function(e) {
$.get(this.href, function(data) { $("#display").html(data); });
e.preventDefault();
});
</script>
Much better, but there's still one piece of data out of place. We have a relationship (there's that word again) between two elements on a page that is defined via JavaScript. This is a simple, direct, one-way relationship. It should be pretty easy to express in the markup itself, but no obvious solution presents itself. Or so you thought.
The target attribute
An interesting technique I first learned from working with the very talented
Rogie King (yes, his site looks like
Tim Van Damme's; that's on purpose) is to embed this
information in the target attribute of the <a /> element. Many
of you may remember this attribute from the days when we simulated things like this
<airquotes> Ajax </airquotes> with the clever use
of frames. Shortly after we realized what a horrible idea frames were, the W3C went about
deprecating anything and everything having to do with them, including target.
As of HTML5, the target attribute is officially un-deprecated, and
according to the W3C, must be set to a valid
browsing
context name which, aside from a few keywords useful for working with iframes,
basically amounts to a named
window, or i/frame. However, there's no practical limitation on what a
target can or can't be. Therefore, since no one uses frames anymore, we can
overload it to do something useful, i.e. describing the relationship between two elements:
<?=$this->html->link("Hello World", array("Posts::view", "id" => 5), array(
"id" => "view",
"target" => "#display"
)); ?>
<div id="display"></div>
<script type="text/javascript">
$("#view").click(function(e) {
var a = this;
$.get(this.href, function(data) { $(a.target).html(data); });
e.preventDefault();
});
</script>
Turns out, this idea of relationships is pretty important. If we're to think about the front-ends of our applications in terms of the MVC paradigm as the cool kids keep saying we should, then markup becomes our data model (with CSS providing the templating layer and JavaScript adding behavior, i.e. controller logic). There's a bit of an impedence mismatch here, which we'll get back to in a minute, but the important thing to realize is that, since we're now describing relationships within our "data model", not only does our behavior code become greatly simplified (i.e. we don't have to write a new handler for each link), but we can actually stop thinking about JavaScript entirely. Once rewritten generically, we can focus on entity relationships and forget about the behavior entirely; it Just Works™:
<?=$this->html->link("Hello World", array("Posts::view", "id" => 5), array(
"target" => "#display"
)); ?>
<div id="display"></div>
<script type="text/javascript">
// This now works universally for any link with a target attribute
$("a[target]").click(function(e) {
var a = this;
$.get(a.href, function(data) { $(a.target).html(data); });
e.preventDefault();
});
</script>
'But', you say, 'surely this one-size-fits-all approach to JavaScript events can't work in the real world! What about cases where you need to customize behavior? What about exception cases?' Hold your horses, we'll get there in Part II.
Credits
Thanks to John Kary for pointing out that the event handlers
in the JavaScript examples should probably use
event.preventDefault() instead
of return false. Whereas preventDefault() simply disables the browser's
default behavior, returning false also prevents the event from bubbling (see:
stopPropagation()), which
may lead to unexpected behavior. The examples have been updated accordingly.
See? You learn something new every day.