6 Steps to Unobtrusive Rails (with jQuery)

Revised 11/20/2008 Simplified through use of save.js.erb and removal of jQuery Form plugin dependency.

Here's a set of 6 steps which breakdown the approach I have been using to unobtrusively add ajax functionality to a Rails application. The value of this approach (as opposed to traditional .rjs) is maintaining full separation of Javascript and display code, as well as building ajax on top of a version of your app that works without Javascript enabled in the user's browser.

Also here, we're going to be able to leverage our existing Rails templates, in both JS and non-JS scenarios. If a visitor has JS, the edit template will be displayed in a floating modal window without leaving the main page. So, we're not going to have to write separate display code for our ajax edit form, and importantly we're gonna get all of Rails built-in error handling magic as well. The ultimate goal is to have a simple tack-on system we can use to ajaxify our Rails app. First we have it working in standard mode, then we ajaxify it. While this may look like a lot of work, it really is not too bad considering that most of this is reusable for your app in general. Adding this same functionality to your Rails new, show, delete template-behaviors is done with minimal of additional effort.

Scenario
So let's assume that we're looking at a page with blog-like listing of multiple posts. Each post has an edit link and we have an edit template that handles editing of a post in standard Rails fashion. Our goal is to use this same template in an ajax manner, by loadiing the template into a modal dialog and processing its form submission all without moving away from our main page. Again, if there is no Javascript available in the user's client, our app will work in standard fashion.

So let's get started.

STEP 1: Load various JS libraries.

In this tutorial, we're using jQuery, Livequery and BlockUI. We will use: jQuery instead of Rails' default Prototype; Livequery for attaching events to dom elements that are loaded after the initial page (via ajax); and BlockUI for modal windows. You will need to include these libraries in your railsapp/public/javascripts folder, and remove all Prototype .js files.

HTML:
  1. <%= javascript_include_tag "jquery-1.2.6" %>
  2. <%= javascript_include_tag "jquery.livequery" %>
  3. <%= javascript_include_tag "jquery.blockui" %>
  4. </head>

STEP 2: Setup Rails for using jQuery.

In application.js ...

JavaScript:
  1. // rails auth token enabled in jquery
  2. $(document).ajaxSend(function(event, request, settings) {
  3. if (typeof(AUTH_TOKEN) == "undefined") return;
  4. settings.data = settings.data || "";
  5. settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(AUTH_TOKEN);
  6. });
  7. // add javascript request type
  8. jQuery.ajaxSetup({
  9.   'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript")},
  10. });

Add AUTH_TOKEN in JS variable in page header in application.html.erb...

HTML:
  1. <%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? %>

STEP 3: AJAX configure Rails controller code using respond_to
Here are 2 methods, edit and save. The edit method is vanilla and will simply default load edit.html.erb. However, as you'll see below, our js code will jump in and load this in a modal dialog if js is enabled in the browser. So we won't be leaving the page we're on to open our edit dialog. Save is more special.

According to standard Rails fashion, we're using respond_to to handle either html or js gracefully in our controller.

In posts_controller.rb...

RUBY:
  1. def edit
  2.     @post = Post.find(params[:id])
  3.   end
  4.  
  5.   def save
  6.     @post = Post.find(params[:id])
  7.     # ... update your post object with whatever params your form is posting
  8.     respond_to do |wants|
  9.        wants.html {
  10.           if @post.save
  11.             flash[:notice] = 'Post saved'
  12.             redirect_to posts_path
  13.           else
  14.             render :action => 'edit'
  15.           end
  16.         }
  17.        wants.js {
  18.           if @post.save
  19.             flash[:notice] = 'Post saved'
  20.           end
  21.         }
  22.     end

The wants.html{} is standard here -- handling things if js is not present.
However, if js is present, we get wants.js{}, where we handle either a successful save, or not. In our scenario, if we have success with our @post.save, we will render the updated @post object itself so we can insert that back into our dom, replacing the original post with our edited post (after we close the modal editor dialog). If we do not succeed with our save, we can access the errors themselves in json format, which I like because it's more visually elegant to simply load our errors rather than refresh the whole dialog. We will handle the errors display in a js function -- more on this below.

STEP 4: Edit Link in index.html.erb and edit.html.erb

The link tag below is used in each blog posting in our list of posts (posts/index.html.erb). We're using a partial as well that this code would be inside, _post.html.erb...

HTML:
  1. <!-- Post Partial -->
  2. <div class="post_div">
  3. ... template code displaying elements of the blog posting
  4. <%= link_to "Edit", {:controller => 'posts', :action => "edit",:id => post.id},  :class=> "edit" %>
  5. </div>

The css class 'edit' is key here, and will enable us to use jQuery to attach an event handler to it.

Here's our Rails edit template, edit.html.erb...

HTML:
  1. <div id="post_edit">
  2.     <span title="close editor" class="close">x</span>
  3. <div class="error_messages">
  4.     <%= error_messages_for 'post' %>
  5. </div>
  6. <div id="post_edit">
  7.    <% form_tag('/posts/save', :id=> "post_edit_form") do %>
  8.          <!-- Here goes all of the form elements for editing the post -->
  9.      <%= hidden_field_tag :id, @post.id %>
  10.      <%= submit_tag "Save Changes", :class => 'save' %>
  11.   <% end %>
  12. </div>

A standard Rails Form tag. Note id of 'post_edit_form' which we will use in our js to handlethe ajax form submission.

In application.html.erb, this is the modal window div. It's by default empty, and is filled with our templates and displayed when needed using BlockUI.

HTML:
  1. <div id="modal_window">
  2.     <div id="inner_content">
  3.     <!-- modal window used for ajax dialogs -->
  4.     </div>
  5. </div>

In styles.css ...

CSS:
  1. #modal_window{
  2.     position:relative;
  3.     display:none;
  4.     z-index:1000;
  5.     padding:30px;
  6. }

STEP 5: Setup AJAX event handlers in application.js

Now, in application.js, we use jQuery and the Livequery plugin to attach an event handler to every anchor tag with class of 'edit', inside of a div with class of 'post_div'. We need Livequery here, only because we will be allowing editing of posts without refreshing the page. Therefore edited posts have edit anchor tags that have been loaded after initial page load -- ie., reinserted into the dom. This is Livequery's special ability.

JavaScript:
  1. var parent_div_of_item; // init var to reference blog post container, used for ajax
  2. // re-inserting edited posts
  3. $(".post_div .edit").livequery('click',function(){
  4.         parent_div_of_item = $(this).parent();
  5.         $.get($(this).attr('href'),function(data){
  6. // displays modal window - note css centering
  7.             $.blockUI({message: $("#modal_window").html(data), css: {width:"400px",
  8.                 margin:"-200px 0 0 -200px", left:"50%", padding: "4px 4px 4px 15px", textAlign: "left"}});
  9.         });
  10.         return false;
  11.     });
  12. // cancel/closing modal is very simple
  13.     $("#modal_window .close").livequery('click',function(){
  14.         $.unblockUI();
  15.     });

The $.get uses the href url attribute from our edit link above, makes the ajax call, and returns the response (the edit.html.erb template) to our modal (div#modal_window) dialog displayed using the BlockUI plugin. Whew, that's a mouthful.

STEP 6: Handling ajax form submission

In application.js ...

JavaScript:
  1. var parent_div_of_form;
  2.     $('#post_edit_form').livequery('submit',function(){
  3.         parent_div_of_form = $(this).parent();
  4.         $.post($(this).attr("action"), $(this).serialize(), null, "script");
  5.         return false;
  6.     });

Here we use Livequery again to handle the submit event of the edit form which has been loaded after the initial page. We store a reference to the parent div of our form for later insertion of validation errors, if they exist. The jQuery $.post submits the form's serialized values and requests (via "script") Javascript as a result. The null parameter is because there is no callback function - none is needed here.

In save.js.erb...

RUBY:
  1. <% if @post.errors.empty? %>
  2.     <% p = render :partial => "post", :object => @post %>
  3.     parent_div_of_item.before('<%= p.gsub!(/n/, "")%>').remove(); // replace the div with new edited version
  4.     $.unblockUI();
  5.     notify('<%= escape_javascript(flash[:notice]) %>');
  6. <% else %>
  7.     parent_div_of_form.find(".error_messages").hide().html(error_messages(<%= @post.errors.to_json %>)).fadeIn(300);
  8. <% end %>

The save JS template is loaded because our ajax form submission requests a Javascript response and we have respond_to wants.js{} in our controller method. This is a very cool scenario where we have both Javascript and Ruby available in our template and access to our Rails app context as well as our Javascript functions and variables (in application.js etc). Coding heaven.

If there are no errors, we set a variable (p) to the partial we will load back into the dom. Then we use p.gsub remove line-breaks in the partial before inserting it in the dom, and removing it's previous incarnation all in one nice jQuery chain. .unblockUI hides our modal dialog, and finally the notify function simply animates our flash notice. In our error scenario, we locate the div in our modal dialog with class 'error_messages' in order to insert the resulting messages (see function below) which have been converted to json. The .hide and .fadeIn create the nice fade-in error display.

And finally we have a Javascript function to format our Rails json error messages.

In application.js ...

JavaScript:
  1. function error_messages(response_text){
  2.     var json = eval(response_text);
  3.     var error_text = "";
  4.     var len = json.length;
  5.     for (var x = 0; x <len; x++)
  6.     {
  7.         error_text += "<li>" + json[x][0] + ": " + json[x][1] +"</li>";
  8.     }
  9.     if (len> 0){
  10.         error_text = "<ul>" + error_text + "</ul>";
  11.     }
  12.     return error_text;
  13. }

So that's the tutorial ... for now at least. I am sure there are some different and perhaps better ways to do some of this. It represents the current state of my approach, which is always evolving (and may end up here in revisions).

Revised Nov 20th. -- simplified after influence of Ryan Bates' excellent screencast:
http://railscasts.com/episodes/136-jquery

My hope is that it is useful for people working in Rails who want to approach ajax unobtrusively via solid best-practice: writing a working, non-ajax app that is "progressively enhanced" with ajax code. We recognize that most visitors will have js, but for those who don't, we still present a functioning app to the web browser, and other non-js capable devices in addition.

Additional links of note:

The Pug Automatic
mad.ly

This entry was posted in Rails, Ruby, Tutorial, jQuery, javascript, unobtrusive and tagged , , , , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

3 Comments

  1. Babar
    Posted December 8, 2008 at 1:07 pm | Permalink

    Thanks ! Great tutorial !

  2. BobF
    Posted February 13, 2009 at 3:55 pm | Permalink

    Thanks. I learned quite a few things from this post. You might want to consider wrapping the response in line 3 of save.js.erb with a call to escape_javascript in case there’s some conflicting quotes, etc. in the content.

  3. BobF
    Posted February 13, 2009 at 5:17 pm | Permalink

    I retract the part about escape_javascript, although it still seems that you could get into an escaping mess somewhere along the way.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*