From Rails Ajax helpers to Low Pro, Part 3 (+ some announcements)

September 9th 08:09 PM

This is the third part of my introduction to Low Pro series, something that has been taken… hmmm… a while. The two first parts can be found here:

  • Part 1
  • Part 2

Coincidentally, all these pieces are also part of my new ebook, Unobtrusive Prototype, straight from the Peepcode oven. If you liked the articles, you might really enjoy the book. It will have all the code rewritten for Rails 2.1 (as has this article btw), it is professionally edited unlike my rumblings here, and has a much wider coverage of Low Pro, including writing your own Behaviors and event delegation.

I also managed to give a tutorial on the topics covered in the book in RailsConf Europe in Berlin last week. The tutorial slides can be found at Slideshare, where they also got a short featured treatment.

I moved the sample code for the articles and the book to my GitHub account. The Chapter 5 branch should be pretty much where this article starts at. If you’re not into git (yet), just click the “Download” button on the page to suck a zip file of the whole source code to your box.

Going DRY with multiple elements

Now that we have taken care of adding items to our to-do lists, let’s have a look at the the action of ticking items done and undone. The index.html.erb view uses the Rails’ partial mechanism to display the lists of items. Here’s how the _item.html.erb partial looks like:

<% @item = item %>
<li id="<%= item.state %>_<%= item.id %>">
  <%= check_box("item[]", :done, :id => "#{item.state}_box_#{item.id}") %>
  <label for="<%= "#{item.state}_box_#{item.id}" %>">
    <%= item.description %>
  </label>
  <%= observe_field("#{item.state}_box_#{item.id}", 
                    :url => item_path(item), 
                    :method => :put) %>
</li>

observe_field is a Rails helper that attaches an Javascript observer to the field in question. Whenever the field is changed (or every n seconds, if the time n is given as a parameter to the observe_field call), an Ajax call (in this case to item_path(item)) is made. In our app, the responding update action will then update the state of the item in the database and return Javascript that will move the item to the correct list on the page.

Here’s an example of what the list of items looks like to the browser:

<li id="undone_1">
<input id="undone_box_1" name="item[1][done]" type="checkbox" value="1" />
<input name="item[1][done]" type="hidden" value="0" />
<label for="undone_box_1">
  Buy carrots
</label>
<script type="text/javascript">
//<![CDATA[
new Form.Element.EventObserver('undone_box_1',
                  function(element, value) {
                    new Ajax.Request('/items/1', 
                                     {asynchronous:true, 
                                      evalScripts:true, 
                                      method:'put', 
                                      parameters:value + '&authenticity_token=' +
                encodeURIComponent('8d829cfcccdf4d2b494891ef47cc95893faa361e')})})
//]]>
</script>
</li>
<li id="undone_2">
<input id="undone_box_2" name="item[2][done]" type="checkbox" value="1" />
<input name="item[2][done]" type="hidden" value="0" />
<label for="undone_box_2">
  Return bottles to recycling
</label>
<script type="text/javascript">
//<![CDATA[
new Form.Element.EventObserver('undone_box_2', 
                  function(element, value) {
                    new Ajax.Request('/items/2', 
                                     {asynchronous:true, 
                                       evalScripts:true, method:'put', 
                                       parameters:value + '&authenticity_token=' +
                encodeURIComponent('8d829cfcccdf4d2b494891ef47cc95893faa361e')})})
//]]>
</script>
</li>

Imagine a list that has a couple dozen items. Not exactly DRY, is it, especially compared to how clean the source code of a Rails app tends to be?

Even if we set aside all the objections for the ugly code above, there is still the issue of the form not working at all without Javascript. Not good.

Let’s again start our round of refactoring by making the checkboxes work without javascript. For that, we’ll remove the observe_field call from the partial:

<% @item = item %>
<li id="<%= item.state %>_<%= item.id %>">
  <%= check_box("item[]", :done, :id => "#{item.state}_box_#{item.id}") %>
  <label for="<%= "#{item.state}_box_#{item.id}" %>">
    <%= item.description %>
  </label>
</li>

Now, to make our form work again, we need to figure out what it’s supposed to do and where we should send it to. Thinking restfully, by submitting a form with multiple items checked or unchecked, we are modifying a to-do list. Let’s thus create a simple controller for the imaginary List resource (remember, resources don’t need to map directly to ActiveRecord models).

script/generate controller Lists

Then add the necessary route to config/routes.rb

  map.resources :items
  map.resource :list

For simplicity’s sake, let’s pretend our app can only handle a single to-do list (it’s our personal to-do list app, after all) and use the single form of resource routes with it.

In our to-do list page, we currently have a form that’s unable to submit anything. Let’s add a submit button to it and also change the form in app/views/items/index.html.erb to point to the lists controller:

<% form_for :list, :url => list_path, :html => {:method => :put} do |f| %>
  <h3>
    Not done:
  </h3>

  <ul id="undone">
    <%= render :partial => "item", :collection => @not_done %>
  </ul>

  <h3>
    Done:
  </h3>

  <ul id="done">
    <%= render :partial => "item", :collection => @done %>
  </ul>

  <p>
    <%= submit_tag "Save changes", :id => "save_changes" %>
  </p>
<% end %>

Now let’s create a really simple update action to our new controller for the mass assignment of items:

class ListsController < ApplicationController
  def update
    params[:item].each do |key, values|
      item = Item.find(key)
      item.update_attributes(values)
    end

    redirect_to items_path
  end
end

If you now test the application with Javascript turned off, updating the item state should work fine. We can thus continue to the hijacking phase.

First of all, let’s hide the submit button because we don’t need it in the Ajax’ed form.

Event.addBehavior({
  'body' : function() {
    $('add_form').hide();
    $('add_new_link').show();
    $('save_changes').hide();
  },
  '#add_new_link:click' : function(e) {
    $('add_form').toggle();
    e.stop();
  },
  '#add_form' : Remote.Form
});

Now, what we need to do is to make clicking a checkbox to call the update action for the current item. This is easily done in the js file:

Event.addBehavior({
  'body' : function() {
    $('add_form').hide();
    $('add_new_link').show();
    $('save_changes').hide();
  },
  '#add_new_link:click' : function(e) {
    $('add_form').toggle();
    e.stop();
  },
  '#add_form' : Remote.Form,
  'input[type=checkbox]:click' : function() {
    var id = this.id.match(/\d{1,}$/);
    var auth_token = this.up('form').
                          select('[name=authenticity_token]').
                          first().value;
    new Ajax.Request('/items/' + id, 
                     {asynchronous:true, 
                      evalScripts:true, 
                      method: 'put',
                      parameters: {
                        authenticity_token: auth_token
                      }});
  }
});

We use the CSS3 selector syntax to get every input of type checkbox, then fetch the item id from the element id using a regular expression and finally call the update method of the items controller to update the item. The cool thing about addBehaviour is that the behaviour is attached to all elements returned by the selector. Thus adding a single call function to our javascript file automatically attaches the function to as many list items as needed.

Note that because of the spam-defense mechanism in Rails, we also need to send the authenticity token with our call. We use the cool Prototype selector functions to easily get to the current token inside the form.

An astute reader might have noticed that if you click an item in the list, and then try to re-click the same item after it’s moved to the opposing list, it’s not moved back automatically. This is because our Ajax update action created a new list item and added it to the list, and by default Event.addBehaviour does not reattach behaviours after each Ajax call.

We have a couple of ways to fix the situation. The simplest would be to add this line to our Javascript file:

Event.addBehavior.reassignAfterAjax = true;

However, the simplest way is not always the best. By reassigning behaviours automatically after each Ajax call we can deteriorate the Javascript performance considerably, and the effect gets larger when there are more items watched.

Second, a bit more surgical option would be to reload the addBehaviour rules in a callback of the Ajax call:

onComplete : function() {
  Event.addBehavior.reload();
}

This way the behaviours would only be reassigned after the particular Ajax call, not all of them. However, it’s still a bit heavy-handed.

Let’s take a step back and think how we could avoid reassigning all the behaviours for the new element. An obvious answer would of course be not to create a new element at all, but instead just move the existing one (with all the behaviours already attached).

In app/views/items/update.js.rjs we can see that the code first removes a list item and then adds a new one into the list of items in the opposite state:

page["#{@item.opposite_state}_#{@item.id}"].remove
page.insert_html :top, @item.state, :partial => "item" 

We can fairly easily change that code to not delete/create a new node to the page, but instead move the list item to the correct place in the DOM:

page << " 
  var el = $('#{@item.opposite_state}_#{@item.id}');
  $('#{@item.state}').insert({ top: el.remove() });
  el.id = '#{@item.state}_#{@item.id}';
" 

This time we don’t use the RJS syntax but instead just output plain old Javascript back to the browser. We first fetch the list element we’re about to move. We then remove it from the DOM tree, just to again insert it to the bottom of the list it now belongs to. In the end we change the id of the item to reflect its new state as well.

If you now try the app again, you should be able to tick and untick the items at will, and everything should work just fine. The behaviour assigned to the list element on the page load sticks to it through all the moving and renaming of the element.

spacer

If you enjoyed the article, consider grabbing a copy of my new ebook, Unobtrusive Prototype, straight from the Peepcode oven. It will have all the code rewritten for Rails 2.1, it is professionally edited unlike my rumblings here, and has a much wider coverage of Low Pro, including writing your own Behaviors and event delegation. If you’re quick and have a close look at my presentation slides in the beginning of this article, you might even find a way to get your copy for half the normal price!

Jump to comment form

Comments

  1. Tom 09.10.08 / 03AM

    Maybe it’s just me but unobtrusive javascript seems like a whole lot of work just to get things done. Who cares how the browser sees the code?

  2. Jarkko 09.10.08 / 08AM

    @Tom Who cares what your code looks like in general? Not all do, and you can certainly get things done with spaghetti PHP code. The question is whether there’s a better way. Just like I don’t want to put my business logic in Rails views, I want to keep the behavioral logic of my pages separated from the structure.

    That said, you’re completely omitting the progressive enhancement and accessibility part of the story. You might not care about all your users but pretty darned many do and some are even bound by law to provide a certain level of accessibility.

    Third, even though inline event handlers are generally a tad faster than ones attached in a separate file, the advantage turns the other way pretty fast when there are a bunch of items on the page. Repeating the exact same JavaScript code over and over in an HTML page doesn’t just go against esthetics, DRY and humanity, but it also makes your page less snappy. Jeremy Kemper talked about this in his RailsConf keynote in Berlin last week.

  3. Geoffrey Grosenbach 09.10.08 / 18PM

    It also makes it easy to add other behaviours, almost like plugins. I used Dan Webb’s DateSelector and it required only a single line of code (plus some CSS).

  4. Kris 10.08.08 / 08AM

    I certainly care. I am happy to see some one addressing the concerns i have had regarding inline js w/ in the views. Great article, I am happy to find this.

Recently

  • 09.08 From Rails Ajax helpers to Low Pro, Part 3 (+ some announcements)

spacer

spacer spacer

Powered by Mephisto


gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.