Thursday, June 18, 2009

Unobtrusive Unobtrusive JavaScript

In the past few months, I have become quite the fan of unobtrusive JavaScript. Not that it took very much convincing - I just didn’t know any better before becoming a follower. Having exactly zero JavaScript calls in my HTML files is very pleasing indeed.

One problem with unobtrusive JavaScript however: very large JavaScript files. In fact, my original approach to using unobtrusive JavaScript was to have one big .js file for all of the pages in my application. As you can imagine, this single JavaScript file grew very large very quickly, and it became apparent that I should break this file up into smaller files.

Ideally, I wanted one static JavaScript file per Rails view. To accomplish this, a coworker of mine mustered up the following method in the ApplicationHelper that would automatically include a particular JavaScript file specific to the controller and action you are visiting:

module ApplicationHelper
def link_default_javascript
path = "#{@controller.controller_name}/#{@controller.action_name}"
if FileTest.exists?("#{RAILS_ROOT}/public/javascripts/#{path}.js")
return javascript_include_tag(path)

This link_default_javascript method is meant to be referenced from a layout file as follows:

Example: app/views/layouts/main.html.erb

<title>Rails App!</title>
<%= javascript_include_tag :defaults %>
<%= link_default_javascript %>
<%= yield :extra_javascripts %>
<body> <%=yield %> </body>


Note how the link_default_javascript call happens after the javascript_include_tag :defaults call - this is necessary if you reference any Prototype methods in your custom JavaScript files. I’ll get to the yield :extra_javascripts shortly.

Calling the link_default_javascript method in our layout file means if we visit the Rails application's main controller and index action, the layout will automatically load a JavaScript file located at public/javascripts/main/index.js, or do nothing if it cannot find the .js file.

To further this example, our app/views/main/index.html.erb may look like this:

<input type=”submit” value=”Click me!” id=”click_me_button” />

And the public/javascripts/main/index.js file may look something like this:

init = function() {
Event.observe($(‘click_me_button),’click’, function(event) {
alert(“You clicked the button!”);

Event.observe(window, ‘load’, init);

Thus, if our main controller’s index action uses the main.html.erb layout and renders its default view, app/views/main/index.html.erb, we can expect the public/javascripts/main/index.js JavaScript file to be loaded by default when the page loads. As a result, we should see a popup window appear when we click the "Click me!" button.

Clean, simple and straightforward.

Back to the yield :extra_javascripts call in the layout: this is a popular way to include additional JavaScript files in a specific page without any complex logic in your layout. Say we had a JavaScript file public/javascripts/shared/common.js that we wanted to load on a few views, but not all. In those views, we could add the following bit of code:

<% content_for :extra_javascripts do %>
<%= javascript_include_tag "shared/common" %>
<% end %>

Anything put inside the content_for block will be inserted in the <head> tag in place of the yield :extra_javascripts call. In this case, a javascript_include_tag that includes the common.js file will be inserted into the page's header.

Hopefully you all will find these two techniques useful!


Tony said...

Nice post! I yield the extra javascripts a bit differently.

In application_helper.rb:

def javascript(*files)
content_for(:head) { javascript_include_tag(*files) }

def stylesheet(*files)
content_for(:head) { stylesheet_link_tag(*files) }

In your views do stuff like:

<% javascript 'bsn.AutoSuggest_c_2.0' %>
<% stylesheet 'autosuggest_inquisitor' %>

In your layout head section:

<%= yield(:head) %>

colonhyphenp said...

Thanks for the code sample, I like it! Calling your "javascript" or "stylesheet" methods is more concise and probably easier to remember than the method I mentioned in the post.