From Rails Ajax helpers to Low Pro, Part 2

Posted by # August 6th 03:36 PM

See also: Part 3

In the first part of this series, we had a look at how we have evolved from using the standard Rails Javascript helpers to first use the UJS for Rails plugin and then to use Low Pro on its own.

However, there’s not much documentation about Low Pro yet. In this article, we’ll introduce Low Pro to you by taking a heavily Ajax-driven, fairly inaccessible Rails page and transforming it to an accessible, unobtrusive one.

We start with a simple todo list application that uses the traditional Rails Javascript helpers. You can download the original application from here.

Todo list app

The index page of the items controller is very simple:

<% form_for :item, :url => items_path 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>

<% end %>

<p>
  <%= link_to_function "Add new item", "$('add_form').toggle()" %>
</p>

<% remote_form_for :item, @new_item, :url => items_path,
   :html => {:id => "add_form", :style => "display: none;"} do |f| %>
  New item:
  <%= f.text_field :description %>
  <input type="submit" value="Add item">
<% end %>

We’ll take a closer look at the partials later, but let’s begin with the lower part of the page. There are two kinds of Rails JS helpers used. First, the link_to_function to implement toggling the visibility of the form for adding new items, and second, the remote_form_for for the actual form.

This is how the source looks to a browser:

<p>
  <a href="#" onclick="$('add_form').toggle(); return false;">Add new item</a>
</p>

<form action="/items" id="add_form" method="post" onsubmit="new
  Ajax.Request('/items', {asynchronous:true, evalScripts:true,
  parameters:Form.serialize(this)}); return false;" style="display: none;">
  New item:
  <input id="item_description" name="item[description]" size="30" type="text" />

  <input type="submit" value="Add item">
</form>

Above, the anchor tag is both inaccessible and obtrusive. Without Javascript support, nothing happens when you click the resulting link. The Javascript behaviour is also placed right into the tag.

The form tag has a normal action attribute, so it’s perfectly accessible, as long as the backend supports receiving the form submit without XmlHttpRequest. However, the tag is at least as obtrusive as the link, having the whole Ajax.Request call in the onsubmit event handler.

Let’s now make the parts accessible, starting with the link. First of all, we’ll want to make sure the link works even without Javascript. For that, we’ll modify the helper call to just use the normal link_to.

<%= link_to "Add new item", new_item_path %>

If we now click the link, it will bring us… nowhere. We don’t have a new action in our controller. Let’s create a template (new.rhtml) for it real quick. We don’t even need to add the action to the controller:

<%= render :partial => "form" %>

We already have the form in the index template, so let’s move it to the partial (_form.rhtml) from there…

<% remote_form_for :item, @new_item, :url => items_path, :html => {:id => "add_form", :style => "display: none;"} do |f| %>
  New item:
  <%= f.text_field :description %>
  <input type="submit" value="Add item">
<% end %>

...and replace it in index.rhtml with a similar render call we just added to new.rhtml.

If we now click the link again, we get to the new page and see…still nothing. It’s because the form is invisible. We don’t want that, so let’s remove the style attribute from the form partial. We also don’t want the form to be a remote form by default anymore, it wouldn’t work well within the separate new page:

<% form_for :item, @new_item, :url => items_path, :html => {:id => "add_form"} do |f| %>
  New item:
  <%= f.text_field :description %>
  <input type="submit" value="Add item">
<% end %>

We can try to create a new item now but we’ll get some weird stuff back. Our create action only has an RJS template so far. Let’s change the create action in items_controller.rb a bit so that it redirects in case of normal http request:

respond_to do |wants|
  wants.html do
    redirect_to items_path
  end
  wants.js
end

Now creating new items from the new action should work fine.

Progressive Enhancement

We have now ensured that adding items works without Javascript and can thus start the progressive enhancement phase. For it, we need the Low Pro javascript library.

Check the code out somewhere on your hard drive:

svn co http://svn.danwebb.net/external/lowpro/trunk lowpro

And copy the dist/lowpro.js and the behaviours subfolder to your app folder

cp dist/lowpro.js behaviours/*.js [path to your app]/public/javascripts/

We also need to update prototype to it’s latest version. Download http://prototypejs.org/assets/2007/6/20/prototype.js and replace the prototype.js in your app with it.

Now you need to load all the needed Javascripts in the layout file (app/views/layouts/items.rhtml):

<%= javascript_include_tag :defaults, 'lowpro', 'remote' %>

We also need a way to pass certain javascript includes for specific pages. We can do this by using the content_for mechanism in Rails. Put the following into the head of your layout template:

<%= yield :javascript %>

Then add the following to your index.rhtml template

<% content_for :javascript do %>
  <%= javascript_include_tag "items_index" %>
<% end %>

This makes the index action to load the Javascript file that is particular to it:

<script src="/javascripts/items_index.js" type="text/javascript"></script>

Now create the items_index.js file in your app’s public/javascripts folder and we’re ready to roll!

We’re using the excellent Event.addBehaviour method in Low Pro to attach behaviours to elements on our page. First of all, we want the form to be hidden when the page loads (remember we removed the css attribute from the element a few lines ago). This makes sure that users who have CSS working but Javascript not can still see the form.

Event.addBehavior({
  '#add_form' : function() {
    this.hide();
  }
});

Here, we target the form element by its id and then attach a function to it hiding the form. Note that addBehaviour always passes the actual element to the function as this, so it’s easy to call methods for that element directly.

Next, we want to make clicking the “Add new item” link to show the form. We need to first add an id to the link and then attach a behaviour to its click event.

<%= link_to "Add new item", new_item_path, :id => "add_new_link" %>
Event.addBehavior({
  '#add_form' : function() {
    this.hide();
  },
  '#add_new_link:click' : function() {
    $('add_form').toggle();
    return false;
  }
});

Note how the actual event is separated by a colon from the element id reference. The same way works for all Javascript events, such as submit, focus and blur.

We must remember to make the attached function return false in the end, otherwise browsers will follow through the link (just like would happen if the code was inside an onclick inline event handler).

Ok, our link is now both accessible and unobtrusive. For Javascipt-handicapped it works as a normal link, and for the majority of the users it shows the form inline on the current page.

Next thing to do is to make the form Ajax’ed again by Hijacking it, in Jeremy Keith’s terms.

Event.addBehavior({
  '#add_form' : function() {
    this.hide();
  },
  '#add_new_link:click' : function() {
    $('add_form').toggle();
    return false;
  },
  '#add_form' : Remote.Form
});

Hold it! What’s that? We’re not attaching a function to the element anymore. Remote.Form is a Low Pro behaviour class, a fairly recent addition in the library. Behaviour classes can be used to encapsulate common behaviour that you would put into an attached function inside addBehaviour. Remote.Form and Remote.Link are good examples of behaviour that is pretty much the same all the time. They will automatically hijack a form or link respectively, and make them use Ajax. We could specify a bunch of attributes to the calls, but most of the time they just work, getting all the needed info from the actual form and a elements.

However, we now have one problem. Since we’re attaching another behaviour to #add_form already, the latter will override the first one and the form is not hidden on the page. We could overcome this by writing our own behaviour class. However, we will take a short cut here and hide the form when the body is loaded, instead:

Event.addBehavior({
  'body' : function() {
    $('add_form').hide();
  },
  '#add_new_link:click' : function() {
    $('add_form').toggle();
    return false;
  },
  '#add_form' : Remote.Form
});

Ta-da! Our link and form now work just like they did in the beginning. However, now the code works even when JS is turned off. The produced HTML now looks like this:

<p>
  <a href="/items/new" id="add_new_link">Add new item</a>
</p>

<form action="/items" id="add_form" method="post">
New item:
<input id="item_description" name="item[description]" size="30" type="text" />
<input type="submit" value="Add item">
</form>

Isn’t that just beautiful?

This is a good time to have a short break and digest what you’ve learned so far. In the next installment, we’ll tackle the select box lists of todo items and see how you can unobtrusively attach behaviours to multiple elements with very small amount of code.

Now, continue to the next part of the series.

rails Jump to comment form

Comments

  1. Jacob Radford 08.08.07 / 12PM
    I think this tutorial is a great way to get things started for those unfamiliar with progressive enhancement. But, it would seem that the non-js version now has both a link to the ‘new’ form and the ‘new’ form is also on the index page. It seems better to not have the link to ‘new’ on the index page (what you probably have in mind for those without js). Then, to progressively enhance the page, in the body behavior:
    • hide the form
    • create a link
    • insert the link before the form
    • add js to the link to toggle the form
  2. Dan Webb 08.09.07 / 03AM

    Jacob: That’s how I’d approach the problem as well.

    Cheers for writing all this up Jarkko. It’s excellent.

  3. Brett 08.22.07 / 05AM

    Excellent article! Thanks. I do have one issue that I can not seem to fix. I have updated all the js files and copied the updates directly from the article, but after the “New Item” form shows on the index page, the browser still follows the link to the new item page. Any hints or points in the right direction?

  4. Jarkko 08.22.07 / 15PM

    Jacob: Absolutely. The purpose of the example is just to show how to attach behaviour to a simple link, not to be a good example of interaction design. When I noticed what you point out I was too lazy to change the app just for that. I hope it still serves it purpose, though :-)

  5. Jarkko 08.22.07 / 15PM

    Brett: Either the Javascript isn’t returning false or you have Javascript disabled. At least those are the two most probable reasons.

  6. TIm Chater 08.22.07 / 19PM

    Yes, this is an excellent article – I’m looking forward to the next installment.

    Interestingly I’m having the same problem as Brett – the ‘return false’ line doesn’t seem to prevent the browser (whether it’s Safari/Firefox/IE) following the link. I’m using LowPro 0.5 and Prototype 1.6.0_rc0.

    Javascript is definitely turned on – i.e. the following code hides ‘my_link’ but continues on to the link: });

    Event.addBehavior({
     '#my_link:click' : function() {
       this.hide()
       return false;
     }

    Thanks for any help! :-)

  7. JasonK 08.23.07 / 19PM

    TIm: Your code is not correct javascript. You are missing a semi-colon after the this.hide() command. That’s why it’s failing and continuing to the link.

  8. Tim Chater 08.26.07 / 19PM

    Oops. Good point, Jason. Unfortunately, the addition of the semi-colon doesn’t seem to make a difference… :-(

  9. Carl Youngblood 08.30.07 / 04AM

    Semicolons are optional in javascript and are only needed when you want to put more than one command on the same line.

  10. Juanma Cervera 08.30.07 / 20PM

    Excellent article Jarkko. I had the same problem oe Brett, and found ths solution, I have to use revision 247 in svn (LowPro.Version = ‘0.4.1’) and Prototype 1.5.1.1, instead of Prototype 1.6.0 release candidate. Everything works now.

  11. Tim Chater 09.03.07 / 18PM

    Thanks Juanma. Everything’s fine now.

  12. Jarkko Laine 09.06.07 / 11AM

    It seems that the way to correctly stop the browser from following the link is to use the event parameter in the function and then call stop for the event:

      '#add_new_link:click' : function(evt) {
        $('add_form').toggle();
        evt.stop();
      }
    

    Thanks to Robert Rasmussen for pointing this out!

  13. Brett 09.14.07 / 05AM

    Thanks for the follow-up. Works like a champ!

  14. Ryan Riley 10.06.07 / 18PM

    This is great! However, unlike Jacob’s thoughts above, I’d like do something similar where the default action is to link to the /new path. The behaviors should add the form, hide it, and then allow the link to toggle the form on. However, I can’t figure out how to load the form. Any tips?

  15. Sydney 10.07.07 / 03AM

    I think I’ve read this and Dan’s articles as thoroughly as possible, but nothing is working for me, so clearly I’ve missed an important step.

    I have added the following line in my application.js file and get nothing. Would really appreciate a thought as to what I’m doing wrong.

    Event.addBehavior({‘body’ : function() {alert(‘hello’);}});

    I’ve arranged the includes in my layout to ensure that prototype and lowpro are loaded before application.js, but still no luck. The js scripts are in the javascripts folder.

    <%= javascript_include_tag ‘prototype.js’, ‘effects.js’, ‘dragdrop.js’, ‘controls.js’, ‘lowpro.js’, ‘application.js’ %>

    I’m stumped – is there a lowpro group out there?

  16. Awef 10.10.07 / 11AM

    So I was looking at this, and with all the new REST things coming out, did you see a good way to intelligently hijack a ‘href’ and somehow tag it to properly do post/get/put/delete?

    I’m trying to stay away from making a billion different classes, so would you do something more along the lines of say :

  17. Gareth Townsend 10.11.07 / 05AM

    This is all well and good in theory, but like Sydney I cannot get anything to work.

    The javascript is being included but it doesn’t seem to have any effect.

    Any ideas?

    I’ve tried this in Safari and Firefox.

  18. Jarkko 10.18.07 / 22PM

    Sydney and Gareth,

    Have you tried using FireBug to see what’s happening? It’s saved my ass many times with JavaScript.

    About the javascript include tag, you don’t need to use the js appendices in the call but IIRC it shouldn’t hurt either.

    One thing is that you need fairly compatible versions of lowpro and prototype. The latest trunk version of lowpro didn’t work with proto 1.5 for me but worked beautifully with the latest trunk version.

    I think you can use the UJS4Rails mailing list to ask lowpro-related questions. I’m certainly lurking there.

    Awef: I think it’s safer to just let Rails use the hidden field hack for that. You could certainly use the real HTTP methods for Ajax calls with PUT and DELETE, but I don’t think even remote_form_for etc do that.

  19. Matt Buck 10.30.07 / 23PM

    I as well was experiencing difficulty with my behaviors. None of my events were firing, which gave the appearance that javascript was disabled. This was while I was using the latest version of lowpro from trunk and Prototype 1.6.0_rc1.

    I followed Juanma’s suggestion above (lowpro 0.4.1 and prototype 1.5), and all is well now. I think that lowpro 0.5 has yet to be officially released.