AJAX validation on Rails

Written by BigSmoke on March, the 28th, in 2006


This text discusses how I handled validation messages and other notices caused by AJAX requests in Rails.

Update – 20 april 2007: Wiebe fixed a long-standing bug in the code: non-AJAX flash messages were not being discarded at all, resulting in them remaining in the flash a request too long.

Update – 20 may 2006: Wiebe pointed out to me that the FIXME comment in the application controller snippet was very unclear. This is now fixed. He also wanted to remind the unsuspicious reader that the json gem doesn't necessarily need to go into the vendor directory; it could also be installed globally, which is, incidentally, exactly what we did before freezing it.

The problem

I wanted AJAX requests in Rails to result in error handling and message reporting that was at least as nice (and almost as easy) as doing the same thing without AJAX.

A solution

Let me start with a short summary of the steps I followed to get easy, transparent error handling for AJAX requests.

  1. Make validation errors cause a 4xx HTTP error.
  2. Store validation error messages within the flash.
  3. Pass the flash to the browser through the X-JSON header.
  4. Extract the validation error messages and other flash notices from the X-JSON header using a custom AJAX responder.

Making validation errors cause a 4xx HTTP error
and handling action failures in the views

First, I needed to distinguish between actions that fail and actions that succeed. Rails makes this as easy as can be:

 { :action => "update", :id => @comment.id },
                    :update => { :success => "comment", :failure => "comment_edit" } %>
                    
  <%= text_field :comment, :author %>
  <%= text_area :comment, :body, :cols => 20, :rows => 40 %>
<%= end_form_tag %>]]>

The above form will update the comment element on success and the comment_edit element on failure.

Now, I needed to organize the update action so that it does the right thing on success and on failure.

 'show'
  else
    flash[:invalid] = @comment.errors.full_messages
    render :partial =>  'edit', :status => 444
  end
end]]>

(I use success and failure in the flash to better be able to distinguish between different types of messages when adding pretty icons and colors in the CSS stylesheets.)

The most important part here is rendering the edit partial with a 444 status code. This 444 code will make the request fail, which is just what I needed to rerender the form with invalid fields marked as such. This marking is done automatically by Rails.

Next, I wanted to override Rails' default behaviour because, by default, ActionController surrounds each invalid field with a <div class="fieldWithErrors"> element. This wouldn't be too bad if it were a span (<div>s in the wrong places will wreak havoc in many otherwise valid pages), but I rather have the classes added to the actual fields. This was done by adding the following code to environment.rb. (See ticket #2210 for more information on Rails' faulty default.)

Passing the flash to the browser through the X-JSON header

In my application.rb controller file, I added the following.

The flash_to_json_header after filter converts the flash to a JSON string which is added to the X-JSON header. The X-JSON header has a special meaning to Prototype.

To get the to_json method which I needed, I had to include the following lines in my environment.rb (after installing the appropriate gem in the vendor directory).

Handling the X-JSON header

Now, I had to tell Prototype to actually do something with the JSON header. The following code does that (and a little useless bit more).

Note that as of this writing (2006-03-28), Prototype has a nasty JSON parsing bug where it doesn't surround the JSON to parse with parentheses. See the trouble ticket to check if my patch has already been accepted. If it hasn't, apply it yourself.

The extractMessages(json) function is the function which does the actual work. The rest of the code in Messages is very custom and most probably of little interest to most readers.

In standard.rhtml layout file, I included the following code.



<%= js_extract_messages_from_flash %>]]>

Handling messages for non-AJAX requests

Note that to avoid complete dependency on JavaScript, I should have put a helper in <ul id="messages"> which simply outputs the messages through Ruby. Instead, because JavaScript independence was no requirement for this Intranet application, I used a helper—js_extract_messages_from_flash—which is defined as part of the ApplicationHelper module as follows:

 0)
    result = ""
    
    flash.discard

    result
  end
end]]>

On 5 May 2006, John Collins has spoken a few nice words about this article. Thanks for that, John!

Discuss this story on digg.com.