Project Website, Part Two: Simple jQuery With Rails

project_website.jpg

This is something of a mix between what I've been doing on the http://www.pathf.com/blog/tag/project-website/ posts with my more typical "here's how you do something in Rails" post. I'm going to describe how to use jQuery to manage JavaScript and Ajax in a Rails application rather than Prototype and Scriptaculous.

Conclusions, Up Front

The benefits of using jQuery on a Rails project seem to be:

  • A lot of basic behavior can be specified with much less JavaScript then in Prototype/Scriptaculous, making it feasible for even somebody with my JavaScript skills to write complex behavior directly in JavaScript. I found myself doing things client-side in jQuery that I would have done with a separate Ajax call before.
  • In particular, certain kinds of behavior are almost ridiculously easy -- especially the case where you want every element of a certain kind to have similar behavior. So for instance, if you were, say, maintaining an iPhone plugin and wanted all links of a certain kind to trigger Ajax calls and slide left like a native iPhone app would, jQuery would be a pretty easy way to get there.
  • Creating a Web site that is functional without JavaScript is a much easier and cleaner process using jQuery.

There are a couple of costs, especially from a Rails application

  • You lose some or all of the Rails helper methods for writing JavaScript. You can keep some of the helpers by using the jRails plugin, but using that plugin minimizes some of the gains of using jQuery.
  • The biggest loss for me was assert_select_rjs and the lack of unit testing of jQuery behavior. As much as JavaScript development tools have improved, there are still gaps in testing browser-centric behavior.

jQuery Basics In Two Seconds

The jQuery framework does three essential things. It provides an easy syntax for identifying elements in the DOM tree, allows you to manipulate those elements, and provides a hook for you to perform these tasks after the DOM is ready on the page but before the page starts displaying.

The DOM-ready hook is often abbreviated $, code that you want jQuery to run when the DOM is ready is housed in an anonymous function like this:

$(function() {
  // do stuff here.
});

This enables you to write unobtrusive JavaScript, meaning that all your JavaScript event handlers and functionality are out of the body of the HTML document. In addition, the event handlers and other DOM manipulation can also be placed in this function and outside the HTML body.

Inside the ready function the $ symbol has a different meaning, it represents the global jQuery object. Most activity in jQuery is initiated by querying the jQuery object for a set of DOM elements using CSS selector-style syntax, for example:

$("div.header").hide();

The extremely cool thing about this is that the hide() command is automatically applied to anything that matches the div.header selector -- the iteration over the set is implied. This is very powerful, and when combined with jQuery's selectors and selector filtering methods, is also very flexible.

Building the Site

Which takes us to the actual web site itself. What I'm building here is the administrative page for Brands that have products sold by the client. As it happens, there are three JavaScript tasks that are in roughly increasing order of difficulty. First, I want a link to create a new brand to cause a form to be inserted into the page. That's quite easy. Second, brand has a row, and each row has a similar hidden inline form to add an alias to that brand. Finally, each brand has a "Remove Alias" link that causes an [x] to appear next to each alias, clicking on that [x] causes the alias to be deleted. We'll take them one at a time.

I should note here that I was able to muddle through this on my own, from zero jQuery knowledge pretty quickly, all things considered. Then I had the great benefit of Brian Dillard giving me some style and functionality pointers to make the code look a little bit better. (Any mistakes, of course, are my own.)

Getting ready for jQuery

To set up the jQuery code, I first installed the jRails plugin. I probably didn't need to do this, because I'm not planning on using the Rails helpers it modifies, but it does modify the default list of JavaScript files to include jQuery instead of Prototype.

The header of my layout file looks like this:

  <%= stylesheet_link_tag 'stylesheet' %>
  <%= javascript_include_tag :defaults %>
  <%= javascript_include_tag "jquery.form" %>
  <%= javascript_include_tag "jquery-debug" %>
  <%= yield :jquery %>

The default list includes the jQuery file itself as well as jQuery's UI plugin. I've added the jQuery form and debug plugins.

The yield :jquery bit is something shown in a training session by Jason Seifer of RailsEnvy. Later, all my jQuery code will be on its ERb page enclosed in a content_for :jquery block, and Rails will insert it into the page header.

Showing the hidden

I wrote the view code for this using the HTML code generating helper I wrote about a few weeks ago. I still like the resulting code, although it has been called, and I quote, "weird". The top-level body of the page looks like this:

  def index_body(brands)
    Html.div(:id => "brand_index") do |div|
      div << Html.h1("Brand Listing")
      div << link_to("New Brand", new_brand_url,
          :id => "new_brand_a")
      div << new_brand_form
      div << index_table(brands)
    end
  end

Notice that I include both the link to the new brand and the new brand form in this method -- I'm making no effort here to hide the form. (The details of the form aren't important except that it has a DOM ID of new_brand_form.) This means that a user without JavaScript, for whatever reason, would see both the link and the form. This allows a non-JavaScript user to have a functional site, even if it is a bit awkward.

For the JavaScript enabled, I use jQuery to hide the form. Here's the entire beginning of my jQuery code block, which I placed in my views/brands/index.html.erb file. All subsequent code will go inside the function after the hide() call.

<% content_for :jquery do %><% end %>

As mentioned earlier, the $(function() { places all the code inside a callback that is executed when the DOM is loaded, but critically, before the page starts to display. The relevant line that gets executed is $("#new_brand_form").hide();. The first part of that code is a call to the jQuery global object, querying it for all elements that match the CSS selector #new_brand_form -- that is, all DOM elements with the id new_brand_form. Turns out there's exactly one of them. The query returns the jQuery object (pretty much all jQuery methods return the jQuery object, which allows them to be chained), and the call to hide hides every element in the currently selected set. In this case, it's just the one form, but again, as the programmer I don't care whether there's one element or a jillion, the hide method will hide all of them.

The upshot is that the form is hidden via JavaScript before it displays. The next step is to make it so that the New Brand link exposes the form -- remember, with JavaScript off, the link goes to the regular CRUD new brand page. Using jQuery, it's easy to add a new click handler to the anchor link. As before, the first step is acquiring the DOM object via querying the jQuery object:

$("#new_brand_a").click(function() {
  $(this).hide();
  $("#new_brand_form").show();
  return false;
});

The jQuery function click takes an anonymous function as an argument, and causes that function to be called whenever the onclick event of any of the selected DOM elements is triggered (similar helper functions exist for other common DOM events).

Within the function, the element target is hidden first -- this is assigned by jQuery to the target of the click, and wrapping this in a selector query allows other jQuery calls, such as hide to be applied to it. Similar jQuery syntax is used to show the item with the DOM id new_brand_form, which is the form itself. Finally, returning false prevents the regular click behavior -- following the href attribute of the anchor link -- from happening. (If you are like me, your Ruby experience will cause you to initially make the last line just false instead of return false, which won't work and your JavaScript-savvy colleagues will chuckle at you. So don't do that.)

I think that's a painless way to get simple dynamic behavior on the screen without Ajax -- if was using just Rails helpers, I would have probably done this as an Ajax call, but having everything be client-side saves some wear-and-tear on the server.

Adding An Alias with an Ajax Call

The next functional bit is adding a new alias to an existing brand. In the UI, this looks like this:

brand_row.jpg

I think I can make this explanation clear without dumping a huge mess of HTML in the middle of this post. Bear with me...

The "Add New Alias..." link causes a form to be displayed allowing you to enter the new alias. Submitting the form triggers an Ajax call that redraws the cell with all the aliases. The basics of doing this in jQuery are similar to the previous example. One significant change is that each row will have it's own alias link. We still need each row's link to have a unique ID -- in the server-side Ruby code it's dom_id(brand, :alias_a), but in order to be able to apply the same behavior to each link in a single jQuery call, they need to have a common CSS class -- in this case subtle alias_a -- with subtle handling the visual display and alias_a used to reference behavior.

(As a sidebar, I think maybe I should be embarrassed to admit this, but I've been working with CSS since about 1996 and I had never realized that a DOM element could have multiple CSS classes... You all knew that already, right?)

Anyway, the basic functionality for the show and hide behavior is similar -- hide the form container on startup, when the anchor link is clicked, hide it and show the form.

$(".alias_form_div").hide();
$(".alias_a").click(function() {
  $(this).hide();
  var form_selector = "#" + $(this).attr('id').replace("_a", "_form_div");
  $(form_selector).show();
  return false;
});

There are a couple of differences to note. Since this code is hiding multiple elements, the selectors now start with . to indicate CSS classes. To make the point one more time, the first line of the above snippet still hides all the matching elements no matter how many there are.

The click callback function is different only in that the specific form to show must be identified -- it wouldn't do to show all the forms when any link is clicked. There are a couple of different ways to do this, my choice was to have a naming convention such that the name of the anchor link and it's associated form container are related, so I have a line that builds the ID selector from a string manipulation of the DOM id of the anchor link. Another option, which you'll see a variant of later on, is to walk the DOM tree back up to the table cell, and then find the enclosed form tag using jQuery selectors.

Finally, each form needs to trigger an Ajax call back to the server. For this, I used the jQuery form plugin, which makes it trivially easy to convert any HTML form to an Ajax query. Step one: write the form exactly as you would if it was not Ajax. Step two: associate the form with a callback function using the ajaxForm method defined by the form plugin. Step three: there is no step three. The ajaxForm method automatically binds an Ajax call to the submit action of the form using the form's own action attribute to determine the server call.

In this specific case, each row has its own form, so each form needs to be bound as an Ajax form separately. (When I went over this code with Brian Dillard, he felt it actually could be done without the each wrapper, but left that as an exercise for me...)

$(".alias_form").each(function(n) {
  $(this).ajaxForm(function(response, result, target) {
    var $td = $(target).parents().filter("td");
    $td.html(response);
    $td.find(".alias_form_div").hide();
    $td.find(".alias_remove_x").hide();
  })
});

The snippet starts by querying for all the form objects based on their class. The each method explicitly iterates over each one, calling the anonymous function for each element -- the n argument is the index of the element in the set, the actual element itself is bound to this.

Inside the each function, each form is bound using ajaxForm. The callback function takes three arguments -- the HTML response from the server to the submission (which in this case, will be the reconstituted list of aliases including the newly added one), a status result which should be success, and the target, which is the DOM element or jQuery selector that was bound to the ajaxForm call, in this case, the form represented by this.

Inside the form callback, the first thing to do is identify the table cell that needs to be changed. I use a jQuery set starting with the form itself $(target) -- then do some jQuery manipulation to get a list of all parents of the form back up to the document root, then filtering that list for a td element. This code assumes there will only be one td, if there are going to be nested tables, the selector will need to be more specific.
Having identified the table cell, the next line sets its html property, equivalent to it's DOM innerHtml. (Many jQuery functions act as getters when called with no arguments, and setters when called with one argument).

The HTML from the server method (not shown here) returns the exact same HTML as the original page load. This means the add form and the remove links that should be hidden are included, so this callback function needs to re-hide them, which I admit is not the DRYest way of handling this.

There's actually a bug in this code which is that the click handlers aren't reassigned either, so the newly created add and remove links don't work. There seem to be a couple of ways to address this in jQuery land, and I haven't picked a method yet.

Removing an Alias with an Ajax Call

The last feature here is a series of [x] remove links that only appear when the Remove link is clicked, and which cause the associated alias to be deleted. The twist here is that not only is there one remove link for each row, there are multiple x targets within each row. With jQuery, managing multiple links doesn't add much complexity. Here's the code to hide the individual remove links, then show them when the "Remove Link" is pressed.

$(".alias_remove_x").hide();
$(".alias_remove_a").click(function() {
  $(this).hide();
  var x_class_selector = "." + $(this).attr('id').replace("_a", "_x");
  $(x_class_selector).show();
  return false;
});

I hope the naming convention is clear -- alias_remove_x is the class for the actual [x] links that start out hidden, and alias_remove_a is the class for the Remove Alias... links that start out displayed. To make the point one final time, the jQuery code to show the [x] links is nearly identical to the one to show the form for adding an alias, even though there are multiple [x] links to be shown -- it's a complexity that jQuery manages for you completely.

Finally, the behavior when a [x] is clicked. This removes the alias from the server and clears the HTML section.

$(".alias_remove_x").click(function() {
  var id = $(this).attr('id').split("_").pop();
  if (confirm("Are you sure you want to delete this alias?")) {
    $.post("/brand_designations/destroy/" + id + ".js");
    $(this).parent().html("");
  }
  return false;
})

The callback function extracts the numerical ID of the alias to be removed from the DOM ID of the link (since the Rails code uses the Rails dom_id helper, I know exactly where the numerical ID is in the string. There's a JavaScript confirm, then a jQuery global method &.post is called, which triggers an Ajax POST call back to the server at the given URL. Note the use of the .js extension to explicitly force the JavaScript branch of the server-side respond_to block. In this case, though, the return value isn't used. (If I had wanted to use it, then I would have given the global method a callback function as a second argument similar to the one used for ajaxForm earlier.) Rather than use the response from the server, I just clear the HTML from the enclosing span -- $(this).parent().html(""), which removes the alias on the client side as well. (Ideally, that would only be executed if the POST returns success....)

This was not my first attempt at the functionality -- I had originally tried to use the jQuery load function, which takes a URL and automatically applies the result to a DOM target, but I had some difficulty with jQuery accepting the server result as successful. Rather than continue to bang my head against that wall, I realized I didn't need any actual text from the server, the alias could be removed more easily client-side.

And In Conclusion I'm Done

Wow, that post got long. Thanks for sticking with it. I'm liking jQuery enough that it's worth moving out of the Rails comfort zone, although I really miss the easier automated testing. (There is a jQuery unit test tool, but I'm nowhere near comfortable with it yet.) Inside jQuery's sweet spot, the code is very elegant, and I'm hoping less brittle than other Ajax code -- in this project so far, it's led me to do much more in the client then I otherwise would have.

Next time:

The next three parts of this are going to probably be drag and drop, the user-facing site coding, and staging deployment using Morph. Not sure what order, though.

Related Services: Ruby on Rails Development, Ajax Rich Internet Applications, Custom Software Development