Jul 29 2007

Unobtrusive AJAX with jQuery and Rails

Whilst having become one of the de facto practices for rich web based user experience, AJAX presents a valuable method for web application performance optimization. In this article, I will be discussing using jQuery alongside Rails in an effort to create fast, responsive AJAX operations, while keeping the javascript as unobtrusive to the application's mark up as possible.

Let's start by creating a new Rails project with one Model, Bookmark that has one property, link.

rails -d sqlite3 bookmarks
cd bookmarks
script/generate model Bookmark link:string
script/generate controller Bookmarks
rake db:migrate

We create app/views/layouts/bookmarks.rhtml, the layout for the Bookmarks Controller where we can include the javascript libraries needed by the application. These are jQuery, the jQuery Form Plugin and application.js which will contain any custom javascript we will be writing.

<html>
<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
  <title>index</title>
  <script type="text/javascript" src="/javascripts/jquery-1.1.3.1.pack.js"></script>
  <script type="text/javascript" src="/javascripts/jquery.form.js"></script>
  <script type="text/javascript" src="/javascripts/application.js"></script>
</head>
<body>
<%=yield%>
</body>
</html>

Next, we add app/views/bookmarks/index.rhtml and one partial, app/views/bookmarks/_bookmarks_list.rhtml which will contain the list of bookmarks that will be updated with AJAX calls to the controller's methods.

<form method="post" action="/bookmarks/add" id="add-bookmark">
  <label for="bookmark-link">Bookmark:</label>
  <input type="text" name="bookmark[link]" id="bookmark-link"/>
  <input type="submit" value="Add">
</form>
<div id="bookmarks-list">
  <%= render :partial => 'bookmarks_list'%>
</div>
<% unless @bookmarks.empty? %>
<ul>
  <% for b in @bookmarks %>
  <li>
    <a href="<%= b.link %>"><%= b.link %></a>
    <a href="/bookmarks/delete/<%= b.id %>" class="delete">delete</a>
  </li>
  <%end%>
</ul>
<% end %>

Below is a simplified version of the Controller that handles server side support for add and delete operations.

class BookmarksController < ApplicationController
  def index
    @bookmarks = Bookmark.find(:all)
  end

  def add
    if Bookmark.create(params[:bookmark]).valid?
      @bookmarks = Bookmark.find(:all)
      render :partial => "bookmarks_list"
    else
      render :text => "Oops...", :status => "500"
    end
  end

  def delete
    Bookmark.destroy(params[:id])
    @bookmarks = Bookmark.find(:all)
    render :text => ""
  end
end

By rendering partials we are only updating a desired target sub-section of the mark up, cutting down the response content to a bare minimum and by doing so we should achieve a performance boost. Specifying a 500 HTTP error status code when things go wrong will allow our javascript to interpret a response as problematic.

Finally, here's the javascript for adding and deleting bookmarks and displaying error messages.

function hijackDeleteBookmarkLinks() {
  $('#bookmarks-list a.delete').bind('click', function() {
    var deleteLink = $(this)
    $.ajax({
      type: 'POST',
      url: deleteLink.attr('href'),
      success: function(){deleteLink.parent().remove()}
    })
    return false
  })
}

function hijackAddBookmarkForm() {
  $('#add-bookmark').submit(function() {
    $(this).ajaxSubmit({
      target: '#bookmarks-list',
      clearForm: true,
      success: hijackDeleteBookmarkLinks,
      error: displayError
    })
    return false
  })
}

function displayError(request, errorType) {
  var msg = '<div class="error">'+request.responseText+'(click to close)</div>'
  $('#bookmarks-list').append(msg)
  $('.error').click(function(){$(this).hide()})
}

$(function() {
  hijackAddBookmarkForm()
  hijackDeleteBookmarkLinks()
})

The hijackDeleteBookmarkLinks function intercepts click events on any link with class delete inside the bookmarks-list div and makes an asynchronous call to the link's original URL. Subsequent to a successful response, we dynamically remove the link list entry from the mark up.

It is worth noting the value of the url option to any of our AJAX calls. This should allow us to modify the request URL to anything we like, meaning that we can have different actions corresponding to AJAX or non AJAX calls, making the application work as expected even if javascript is not available on the client. I have omitted this step for the sake of simplicity.

The target option in hijackAddBookmarkForm specifies the element to be updated with the contents of the response to the AJAX call. Also, we need to call hijackDeleteBookmarkLinks on the success of the AJAX call to ensure that any newly created links are bound by the function.

Issues to consider

The example has been simplified for demonstration purposes.

The proposed architecture tightly couples the client-side with the server-side implementation of the application. We are writing actions intended to be used solely by javascript and the javascript itself expects partial HTML to be returned by the responses. The API is nowhere near being RESTful.

We could have separate actions responsible for deleting, creating and listing bookmarks. Those actions could also return something more flexible, like JSON. The reason I chose to have create returning the updated list of bookmarks as part of the create response is to avoid the second request-response roundtrip that would incur if creation and listing were separated. I favored partials over JSON to avoid having to operate on the response. This allows for simpler javascript.

It pays to consider the purpose of the API and based on that decide to compromise some core values in favor of others. When writing the piece of code that inspired this article my goal was not to create a public RESTful API. This code was UI driven and the intention was to create a rich, fast user interface that would work as expected both with javascript turned on or off.

As a side-note, I chose not to use Rails' respond_to method because I prefer actions (methods in general) that are responsible for doing one thing. This might introduce some duplication and any maintenance issues that come along with it, but in my case the actions had enough differences to justify breaking them up. This is a personal preference and not meant to discourage anyone from using respond_to.