I was writing an article about the Unobtrusive JavaScript in Rails 3 when I realized the UJS section was becoming large enough to deserve a separate post. So here's my Unobtrusive JavaScript for dummies explanation.
If you don't know what Unobtrusive Javascript term means, this technique reflects what happened a few years ago with HTML pages when Cascading Style Sheets (CSS) were introduced. Before CSS, the presentation of HTML elements was defined inline. Did you remember the ugly font
tag or the widely-adopted align
attribute?
<p align="center">This is a centered paragraph with a <font color="#FF0000">red</font> word</p>
Then CSS come, and the document presentation attributes were slowly moved outside the main HTML document. Something similar is happening now with HTML and JavaScript. Before JavaScript frameworks made so easy to bind custom events to HTML elements, the most common way to append a JavaScript function was to use the HTML-Javascript bridges such as the on_event_
attributes.
<a href="javascript:void(0);" onclick="alert('Thanks for clicking me');">Click me</a>
With the huge adoption of JavaScript programming, HTML documents fall again into the trap of mixing presentation/interaction elements within the page content and structure.
Let's look the result of a link_to
Rails helper call with a custom :method
parameter.
<%= link_to "delete", domain_path(@domain), :method => :delete, :confirm => "Are you sure?" %>
generates
<a href="/domains/1" onclick="if (confirm('Are you sure?')) { var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value', 'delete'); f.appendChild(m);var s = document.createElement('input'); s.setAttribute('type', 'hidden'); s.setAttribute('name', 'authenticity_token'); s.setAttribute('value', 'pKvg9hsnQ33uk='); f.appendChild(s);f.submit(); };return false;">delete</a>
Pretty messy. Isn't it? What this helper does on click is to generate a form element, set a few attributes and submit the form. This is necessary because the browsers doesn't support the DELETE
HTTP method by default so Rails needs to emulate it.
The essence of the Unobtrusive JavaScript technique is to define the JavaScript interaction in a separate behavior layer.
With Rails 3, the same execution is composed by the HTML output of the link_to
helper
<a href="/domains/1" data-confirm="Are you sure?" data-method="delete" rel="nofollow">delete</a>
and the separate definition of the JavaScript script to execute.
[...]
$('a[data-remote],input[data-remote]').live('click', function (e) {
$(this).callRemote();
e.preventDefault();
});
$('a[data-method]:not([data-remote])').live('click', function (e){
var link = $(this),
href = link.attr('href'),
method = link.attr('data-method'),
form = $('<form method="post" action="'+href+'">'),
metadata_input = '<input name="_method" value="'+method+'" type="hidden" />';
if (csrf_param != null && csrf_token != null) {
metadata_input += '<input name="'+csrf_param+'" value="'+csrf_token+'" type="hidden" />';
}
form.hide()
.append(metadata_input)
.appendTo('body');
e.preventDefault();
form.submit();
});
Benefits
The major benefits of Unobtrusive JavaScripts are:
- Separation of the behavior from the markup (as happened for CSS with the separation of the presentation from the markup)
- Reusable code (both HTML and JavaScript code)
- Progressive enhancement
- Capability detection and gracefully degradation, as the opposite of browser detection. You can easily skip an entire JavaScript fragment if the current browser doesn't support it or likewise use the JavaScript to replace features not natively supported (such as HTML 5 features).
- Better cache support
Unobtrusive JavaScript allows developers to write code much more easier to maintain and debug, with the appropriate use of design patterns and high-quality development techniques already adopted in other mature programming environments.
Unobtrusive JavaScript and JavaScript frameworks
Despite the use of Unobtrusive JavaScript is not a language feature but rather a coding habit, the adoption of this technique has been largely influenced by the availability of high-level JavaScript frameworks. The main reason is that JavaScript frameworks offer an easy approach to define and attach custom scripts to HTML elements, taking advantage of JavaScript features such as closures and relieving the user from dealing with cross-browser compatibility.
Let me show you an example using the jQuery JavaScript framework. This is a traditional, inline, JavaScript code.
<a id="alert" href="javascript:void(0);" onclick="alert('Thanks for clicking me');">Click me</a>
This is the equivalent with jQuery
<script>
jQuery(function($) {
$("#alert").click(function() {
alert("Thanks for clicking me");
return false;
});
});
</script>
<a id="alert" href="#">Click me</a>
The advantages become more evident when the code complexity grown. With the following jQuery code you can show/hide all tables with the class .special
by clicking a generic element with class .button
.
<script>
jQuery(function($) {
$("#button").click(function() {
$("table.special").toggle();
});
});
</script>
Now, sit back for a moment an think about the example above. Using the traditional inline JavaScript programming you would have to place a call to your own toggle function in every single table element containing the .special
class, with a lot of code duplication. Also, what happen if you don't know in advance the number of these elements? What if the number of the elements can change, perhaps as the result of an other JavaScript execution?
With the separation of the behavior from the markup, you can easily** change the behavior of an element leaving the DOM element as it is**, exactly how you can change the color or the size of a link using CSS without actually modifying a single byte of the HTML document.
Equally important, you switch from a JavaScript framework to an other, simply replacing the behavior layer implementation. The switch won't require any change on the HTML document side.
Unobtrusive JavaScript and HTML 5
Technically speaking, there are no dependencies between Unobtrusive JavaScript and HTML 5. However, you can take advantage of some HTML 5 features to write even more flexible and reusable Unobtrusive JavaScript.
Again, a good example is represented by the Rails 3 Unobtrusive Javascript feature. Did you remember the click-and-confirm method?
<a class="confirm" href="/destroy" onclick="return confirm('Are you sure?');">Click to Destroy</a>
We already know we can write an unobtrusive version as follows
<script>
jQuery(function($) {
$(".confirm").click(function() {
if (confirm("Are you sure?")) {
// go ahead
} else {
return false;
}
});
});
</script>
<a class="confirm" href="/destroy">Click to Destroy</a>
Let's move one step forward. We can write a super-reusable script by moving the confirmation message back to the HTML document, having the script to automatically extract it. A new feature being introduced in HTML 5 is the addition of custom data attributes.
<script>
jQuery(function($) {
$('a[data-confirm], input[data-confirm]').live('click', function() {
if (!confirm($(this).attr('data-confirm'))) {
return false;
}
});
});
</script>
<a data-confirm="Are you sure?" href="/destroy">Click to Destroy</a>
Also note that, with the new data-confirm
attribute we don't need anymore to assign custom classes to identify the element. An other important relationship between these two topics is that you can use Unobtrusive JavaScript to emulate HTML 5 features whenever a browser doesn't support them natively. I already mentioned this before when I wrote about capability detection.
A real example is the jQuery autofocus plugin I created a few months ago to make the HTML 5 input autofocus attribute work with non-HTML-5-compliant browsers.
Likewise, there are several plugins to emulate the HTML 5 placeholder feature.
More…
Further readings about Unobtrusive JavaScript: Wikipedia, Unobtrusive JavaScript Coursebook.