Tuesday, February 5, 2013

Unobtrusive JavaScript


What it used to mean

The meaning of the phrase unobtrusive JavaScript has changed since jQuery came around. To understand this change, let’s first go back to when we used to write code like this:
<form class="validated_form" onsubmit="return validateForm(this)">
This is definitely obtrusive JavaScript, since the code is directly embedded in the HTML tags.
In order to make this unobtrusive, we pulled the JavaScript out of the HTML document and put into a separate JS file. But, as soon as we removed the JavaScript code from the onsubmit attribute, we had to write more code to accomplish the same thing. Before we could bind to the submit event, we first had to find that form tag, most likely from within an onload event-handler.
While separating the JavaScript behavior from the HTML is an improvement, we’re doing a lot more work to find the form when the page loads. This shift in where the JavaScript was located forced us to think about DOM traversal – figuring out how to find the elements that we wanted to bind the event to. However, the benefit of separating the JavaScript and HTML outweighed the performance hit of the DOM actions that were happening when the page loaded.

What it means today

In today’s world of JavaScript development, we can use jQuery to easily find elements in the DOM and attach event-handlers with a simple API. So the complex code in our separate JS file can now be written in jQuery like this:
$(function(){
  $('.validated_form').submit(function(){
    return validateForm(this);
  });
});
Although this looks innocent because of the simple jQuery syntax, it still has to search the DOM for the matching element when the page loads (DOM-ready). Essentially it’s doing same thing as our previous onload code, except that jQuery allows us to find the form element and bind events to it using much less code.
Because our code is dependent on how well jQuery performs the DOM traversal, I believe the term obtrusive now refers to the amount of work that has to be done before the user can interact with the page. Every new event-handler that needs to get bound to an element means another DOM scan, which means the user has to wait that much longer before they can use your page. The more DOM work and event-binding that occurs, the more obtrusive your JavaScript has become.

The solution

Fortunately, we can avoid this initial DOM searching by using event delegation. To make our JavaScript unobtrusive, we will essentially adopt an event-driven approach, by only binding a single event-handler to the document. In that case, nothing will happen until a submit event occurs, when it will then bubble up to the document, where the event-handler will respond. The jQuery changes slightly, to look like this:
$(document).on('submit', '.validated_form', function(){
  return validateForm(this);
});
At first glance, it doesn’t look like a whole lot has changed, but it’s actually doing something completely different. Binding event-handlers on the document is a significant change in how the JavaScript is written and how it behaves.
The jQuery on function was introduced in version 1.7, but the same thing can be accomplished in version >= 1.4.2 using the delegate function.

The foundation: event delegation

Since a reference to the document is immediately available, we don’t have to wait for an onload or DOM-ready event to attach an event-handler. As soon as the script executes, it will listen to any submit event that fires in the document and only run the callback if the triggering element matches the specified selector. When an event occurs, it will not only fire the event on the source element, but will also bubble up to its parent, and that element’s parent, firing the event along the way. It will keep bubbling up the DOM, firing the event on all ancestor elements until it reaches the document. At that point, our bound event-handler will run, but will still have access to the source element that fired the event. This process is called event delegation, since execution of the event-handler is delegated to an ancestor element. This makes our code unobtrusive because the work to determine which element triggered the event is deferred until the event fires.
Another benefit of this approach is that the submit event will also be executed for forms that don’t yet exist in the DOM. This means that if you add more forms to the document via AJAX, for example, you don’t have to worry about binding submit event-handlers to those forms. When the submit event occurs on those form elements, they’ll just bubble up to the document, where our existing event-handler will respond.
Any ancestor element can have events delegated to it, not just the document.

Creating an unobtrusive widget

Using event delegation, we can then identify common patterns and build a single reusable widget. I’ll use a keycapture widget as an example. After setting up many textareas in our application, we discovered that we kept writing the same onkeydown event-handlers, to limit input or respond to certain keystrokes. To avoid duplicating this, we created a single unobtrusive keycapture widget that listens to keydown events on the document. The basic setup looks like this:
$(document).on('keydown', 'textarea', function(e){
  var $textarea = $(this);
  // do something with this keydown event
});
Now that we can react to keydown events in a single location, how do we know what to do with that keydown event? We’ll look to the element for hints on what to do.

Markup-driven behavior

Each unobtrusive widget can be configured using HTML5 data- attributes on the element. The sole purpose of these attributes is to provide information to the JavaScript code. For now, let’s set up our keycapture widget to blur the textarea whenever the Escape key is pressed. All we have to do is add a data-escape="blur" attribute to the textarea:
<textarea name="description" data-escape="blur"></textarea>
With this embedded data on the element, the JavaScript code can now make a decision on what to do:
if ((e.which === 27) && ($textarea.data('escape') === 'blur')) {
  $textarea.blur();
}
As of version 1.4.3, jQuery automatically makes all data-* attributes available via the data function.
Now any textarea at any time can use this keycapture widget by simply adding the data-escape attribute to the tag – no additional JavaScript necessary!

Summary

Creating reusable widgets with unobtrusive JavaScript techniques gives you the following benefits:
  • Avoids duplication of JS code by having a single event-handler on the document
  • Avoids expensive DOM searches when the page loads
  • Automatically handles elements that are dynamically added to the DOM
  • Separates HTML markup from JS behavior, but provides means to configure widgets via data-* attributes

Custom Events

In the next post, I’ll discuss how we can hook into these widgets even further, using custom events in jQuery.

No comments:

Post a Comment