At the Forge - Unobtrusive JavaScript

by Reuven M. Lerner

JavaScript has gone through a number of changes in the past few years. Implementations have become faster, more standardized and more stable. The development and growth of open-source JavaScript libraries, such as Prototype and Dojo, has helped mask many of the remaining differences between JavaScript implementations, such as with AJAX and event handling. The final change has occurred in the minds of developers (including myself), who now approach JavaScript as a serious application development language, rather than as a toy for highlighting images or doing simple effects.

Most JavaScript is executed not when it is initially read into a browser window, but rather when a particular event takes place. The easiest, and most common, way to assign event handlers is inside the HTML itself. For example, we can create a submit button for an HTML form as follows:


<form method="POST" action="/action">
    <input type="submit" value="Submit the form" />
</form>

When a user clicks on this button, the browser submits the contents of the form to the URL specified in the form tag's action attribute, using the method specified in the method attribute. But, we can change this by adding an onclick attribute to the submit button:


<form method="POST" action="/action">
    <input type="submit" value="Submit the form"
           onclick="alert('hello!'); return false;" />
</form>

With the onclick handler in place, the button now opens a JavaScript alert box (saying “hello”). Moreover, because our event-handler definition returns false, the form will not be submitted.

Of course, we don't have to put our JavaScript literally in the event handler. We could define a function somewhere else—in the document's <head> or perhaps in an external JavaScript file altogether:


<form method="POST" action="/action">
    <input type="submit" value="Submit the form"
           onclick="do_something(); return false;" />
</form>

Now, none of this is new. But, there are problems associated with setting event handlers in “on___” attributes. For one, it becomes difficult to assign more than one handler to the same event on an object.

A second and more significant reason is that our HTML becomes full of JavaScript. Until a few years ago, it wasn't unusual for HTML to be mixed in with code and style information as well, but the growth of strictly separated MVC frameworks have removed most code from the HTML, and style information now is put in external CSS files.

A growing movement during the past few years has pushed for “unobtrusive JavaScript”. Proponents of unobtrusive JavaScript argue that by placing JavaScript in a separate file and by defining event handlers in a separate file, the code becomes easier to read and understand and is cached by the browser. By using JavaScript unobtrusively, we also have the opportunity to make our HTML pages degrade gracefully, continuing to work with browsers that don't support JavaScript.

This month, we look at unobtrusive JavaScript and the unobtrusive approach to defining functions and event handlers. We also examine the Lowpro library that works with the popular Prototype JavaScript library, allowing us to write JavaScript that is unobtrusive, clean and easy to read.

Observing Events

Above, I showed how to assign a piece of JavaScript (called “do_something”) to an event on a particular HTML element. A fuller version of this HTML form, as well as some more content and tags, is shown in Listing 1 (test.html). This file contains a simple hyperlink, as well as our form.

Listing 1. test.html, the Simplest Version


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
         "http://www.w3.org/TR/html4/strict.dtd">
<html>
 <head>
   <title>Unobtrusive JavaScript</title>
 </head>
 <body>
   <h1>Unobtrusive JavaScript</h1>

   <p>A paragraph of text.</p>

   <p>A <a href="http://www.nytimes.com" id="hyperlink">hyperlink</a>
   to The New York Times.</p>

   <form method="POST" action="/action">
       <input type="text" name="text_field" id="text_field" />
       <input type="submit" value="Submit the form" id="submit_button" />
   </form>

 </body>
</html>

I've already discussed how to handle an onclick event by setting the onclick attribute. However, there are at least two other methods for setting this event handler. One is to set the onclick attribute through JavaScript, treating onclick as a property of the DOM element associated with the hyperlink or button. Using Prototype's $() function, we can write:

$('hyperlink').onclick =
            function() { alert('clicked!'); return false; }

Notice how the event handler is an anonymous function, similar to “lambda” in Ruby and Python or an anonymous subroutine in Perl. The event-handling function can take an optional argument, whose value will be an event object. For example:

$('hyperlink').onclick =
            function(event) { alert(event); return false; }

With this alternate code in place, the alert (in Firefox, at least) indicates that the event was an “object MouseEvent”. This object, like all objects in JavaScript, then has a number of properties we can query. For example, the pageX and pageY properties indicate the X and Y coordinates of the mouse cursor when the event took place. We can see these by specifying the following:

$('hyperlink').onclick =
            function(event) { alert(event.pageX + ", " +
                              event.pageY); return false; }

Each click on the link will give a slightly different result, depending on the coordinates of the mouse cursor at the time of the click.

Of course, we also can define non-anonymous functions as our event handlers:

function show_x_and_y(event) {
    alert(event.pageX + ", " + event.pageY); return false;
}

$('hyperlink').onclick = show_x_and_y;

Notice that our assignment is to show_x_and_y (that is, the name of a function), rather than to show_x_and_y() (that is, the result of executing the function). This is a particularly useful technique if we want to assign the same function to handle multiple events.

We can handle a number of different events. For example, the onmouseover and onmouseout events let us execute a function based on when the mouse starts or stops pointing to a DOM element. Thus, we can do the following:

$('hyperlink').onmouseover =
            function() { $('the_form').hide(); }
$('hyperlink').onmouseout =
            function() { $('the_form').show(); }

When the mouse points to the hyperlink in test-2.html (Listing 2), the HTML form disappears. When the mouse moves away from the link, the form reappears. This might not be especially useful, but it does demonstrate the sorts of events (and event handlers) we can define.

Listing 2. test-2.html, with Event Handlers Defined in <script> Tags

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
         "http://www.w3.org/TR/html4/strict.dtd">
<html>
 <head>
   <title>Unobtrusive JavaScript</title>

 </head>
 <body>
   <h1>Unobtrusive JavaScript</h1>
   <script text="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/prototype/
↪1.6.0.2/prototype.js"></script>

   <p>A paragraph of text.</p>

   <p>A <a href="http://www.nytimes.com" id="hyperlink">hyperlink</a>
     to The New York Times.</p>

   <form method="POST" action="/action" id="the_form">
     <input type="text" name="text_field" id="text_field" />
     <input type="submit" value="Submit the form" id="submit_button" />
   </form>

 </body>

 <script>
   function show_x_and_y(event) {
       alert(event.pageX + ", " + event.pageY); return false;
   }

   $('hyperlink').onclick = show_x_and_y;

   $('hyperlink').onmouseover =
               function() { $('the_form').hide(); }
   $('hyperlink').onmouseout =
               function() { $('the_form').show(); }


 </script>
</html>
</programlisting>

Assigning events in this way has some advantages over using the onclick and related attribute-based event handlers. It lets us define all of our event handlers in a single place—typically at the end of the HTML file. Thus, we have some separation between our HTML and JavaScript.

But, what if we want to go one step further, putting all our JavaScript into a separate file? Listing 3 shows a new version of our HTML file, now called test-3.html. Instead of having the JavaScript at the bottom of the page, I put it in a separate file, called atf-events.js (Listing 4). However, if you try to load this file, you quickly will discover that it doesn't work. We get a JavaScript error upon loading the file (clearly evident and readable if you're using the wonderful Firebug debugger for Firefox), telling us that $('hyperlink') is null.

Listing 3. test-3.html, All JavaScript Removed and Placed in atf-events.js

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
         "http://www.w3.org/TR/html4/strict.dtd">
<html>
 <head>
   <title>Unobtrusive JavaScript</title>

 </head>
 <body>
   <h1>Unobtrusive JavaScript</h1>
   <script text="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/prototype/
↪1.6.0.2/prototype.js"></script>
   <script text="text/javascript" src="atf-events.js"></script>

   <p>A paragraph of text.</p>

   <p>A <a href="http://www.nytimes.com" id="hyperlink">hyperlink</a>
     to The New York Times.</p>

   <form method="POST" action="/action" id="the_form">
     <input type="text" name="text_field" id="text_field" />
     <input type="submit" value="Submit the form" id="submit_button" />
   </form>

 </body>
</html>

Listing 4. atf-events.js, (broken) JavaScript Code for test-3.html

function show_x_and_y(event) {
 alert(event.pageX + ", " + event.pageY); return false;
}

$('hyperlink').onclick = show_x_and_y;
$('hyperlink').onmouseover = function() { $('the_form').hide(); }
$('hyperlink').onmouseout = function() { $('the_form').show(); }

How can this be? If you look through Listing 3, you still will see an HTML element with an ID of hyperlink. And, we definitely have included the Prototype library, so $() should work. How can it be, then, that $('hyperlink') returns null?

The answer is subtle, but well known to JavaScript programmers: $('hyperlink') is available only after the HTML element with an ID of hyperlink has been loaded. Because our JavaScript file was loaded (in the <head> of the document) before the hyperlink element was defined, JavaScript threw us an error.

One solution to this problem is to load our JavaScript at the end of the file, right before the closing </body> tag. Another possibility is to define all of our event handlers in a function that itself is executed only after the entire document is loaded. In other words, we define a function (set_event_handlers) that defines all of our event handlers. Then, we attach this function to the window.onload event, which executes only after the entire document has been loaded. The code, shown in Listing 5, is exactly the same as Listing 4, except the functionality is wrapped in the set_event_handlers function, which is invoked based on an event.

Listing 5. atf-events-2.js, JavaScript Code for test-3.html

function set_event_handlers () {

 function show_x_and_y(event) {
   alert(event.pageX + ", " + event.pageY); return false;
 }

 $('hyperlink').observe('click', show_x_and_y);
 $('hyperlink').observe('click', function() { alert('yay!'); return false;
});

 $('hyperlink').onmouseover = function() { $('the_form').hide(); }
 $('hyperlink').onmouseout = function() { $('the_form').show(); }
}

window.onload = set_event_handlers;
Events in Prototype and Lowpro

Our event handlers are now unobtrusive. However, there still are some problems associated with them. For example, what happens if we want to assign multiple handlers to a single event? That is, what if we want to execute not one function, but two, for $('hyperlink').onclick? In our current paradigm, we don't have any options; to have two functions execute, we need to wrap them both into a single function and then make that single wrapper function the event handlers.

This isn't much of a solution, particularly if we are loading third-party libraries that might want to attach handlers to one or more events. Instead, we need to use a different paradigm—one that lets us attach a handler to an event, rather than set the handler.

Prototype lets us do this with the observe method, which is available to any extended element—including those returned by the $() and $$() functions. So, we can say:

$('hyperlink').observe('click', show_x_and_y);

Because of the way that Prototype's observe method works, we can attach multiple handlers to a single event:

$('hyperlink').observe('click', show_x_and_y);
$('hyperlink').observe('click',
            function() { alert('yay!'); return false;});

Of course, because this code still depends on the existence of $('hyperlink'), we still need to wrap it in a function that is then attached to window.onload. (We also can attach our function to the dom:loaded event, which fires before window.onload, but the idea is the same.)

An alternative solution is to use the Lowpro JavaScript library, which provides functions that facilitate easier writing of unobtrusive JavaScript.

By loading lowpro.js (after Prototype, but before any code that will use Lowpro), we gain access to the Event.addBehavior method, which lets us attach one or more events to any CSS selector. Listing 6 is a slight rewrite of our HTML file to include lowpro.js, and Listing 7 shows how we can set our event handlers using Event.addbehavior:

Event.addBehavior({
     '#hyperlink:click' : show_x_and_y,
     '#hyperlink:mouseover' : function() { $( 'the_form' ).hide() },
     '#hyperlink:mouseout' : function() { $( 'the_form' ).show() }
});

We see that Event.addBehavior is a function that takes a single parameter, a JavaScript object (which we can think of as a hash). Each of the object's keys combines a CSS selector (#hyperlink in this case) with the name of an event, with a colon separating the two. Note that the event name does not include a leading “on”. So what would be the onmouseover handler is called mouseover for Event.addBehavior.

Listing 6. test-4.html, Using Lowpro

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
         "http://www.w3.org/TR/html4/strict.dtd">
<html>
 <head>
   <title>Unobtrusive JavaScript</title>

 </head>
 <body>
   <h1>Unobtrusive JavaScript</h1>
   <script text="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.2/
↪prototype.js"></script>
   <script text="text/javascript" src="lowpro.js"></script>
   <script text="text/javascript" src="atf-events-3.js"></script>

   <p>A paragraph of text.</p>

   <p>A <a href="http://www.nytimes.com" id="hyperlink">hyperlink</a>
     to The New York Times.</p>

   <form method="POST" action="/action" id="the_form">
     <input type="text" name="text_field" id="text_field" />
     <input type="submit" value="Submit the form" id="submit_button" />
   </form>

 </body>
</html>

Listing 7. atf-events-3.js, Using Lowpro's Event-Adding Code

function show_x_and_y(event) {
 alert(event.pageX + ", " + event.pageY); return false;
}

Event.addBehavior({
         '#hyperlink:click' : show_x_and_y,
         '#hyperlink:mouseover' : function() { $( 'the_form' ).hide() },
         '#hyperlink:mouseout' : function() { $( 'the_form' ).show() }
   });

As you can see in Listing 7, Event.addBehavior automatically wraps our event-handler definitions in code that waits for the entire page to load. So, we no longer need to set document.onload, for example.

Finally, the CSS selector code means we can set events on multiple elements simultaneously. If we want all paragraphs, or all table headers or even all images, we can do that quickly and easily with Lowpro. Lowpro allows us to reduce the amount of event-handling code that we write dramatically, keeping it in a single location and removing it from the HTML file where we might have first considered putting it.

I should add that Lowpro used to include DOM-manipulation routines as well, allowing us to add and modify page elements using a variety of convenience functions. However, recent versions of Prototype include this functionality already, allowing Lowpro to stick to behavior not addressed by Prototype.

Conclusion

Unobtrusive JavaScript is an increasingly popular style for working with JavaScript, particularly when it comes to defining event handlers. Prototype makes it easier to work with events than with raw JavaScript, but the Lowpro library makes it even easier than that. With Lowpro, it becomes quite simple to assign event handlers to any combination of elements in our document, without having to clutter up our HTML page or worry about when the page has loaded.

Resources

David Flanagan's JavaScript: The Definitive Guide is an excellent resource for JavaScript programmers, including both a tutorial and a reference section. Douglas Crockford's recent book, JavaScript: The Good Parts, is much shorter, but it's also excellent and provides useful advice on which parts of JavaScript we should avoid. Both books are published by O'Reilly. My opinion (and use) of JavaScript has improved dramatically since reading Crockford's writing, letting me concentrate more on writing code and less on problems associated with the specification or implementation of JavaScript.

You can read more about Prototype at its home page, www.prototypejs.org. I also enjoyed the book Prototype and Scriptaculous, written by Christophe Porteneuve and published by the Pragmatic Programmers.

Finally, the Lowpro library is written and distributed by Dan Webb, and it's best described on his blog, www.danwebb.net/2006/9/3/low-pro-unobtrusive-scripting-for-prototype. And, a Google group for discussing Lowpro is at groups.google.co.uk/group/low-pro.

Reuven M. Lerner, a longtime Web/database developer and consultant, is a PhD candidate in learning sciences at Northwestern University, studying on-line learning communities. He recently returned (with his wife and three children) to their home in Modi'in, Israel, after four years in the Chicago area.

Load Disqus comments

Firstwave Cloud