Scientists agree: Using JSON to bring the richness of the Rails controller and view layers to Ajax requests makes life more enjoyable!
How do you handle an Ajax request whereby the request itself is successful (i.e. it returns a status 200 OK), but the requested operation fails. For example, a user attempts to create a new ActiveRecord object, but the object fails validation and thus doesn’t get saved to the database. The HTTP request was successful, but the user’s wishes weren’t fulfilled.
There are many, many ways to handle such a request, and most of them are fucking painful. Especially if you don’t care to muck around with endless JavaScript.
What I’m about to propose is most useful for those using jQuery (or any framework outside of Prototype, actually). I say this because a lot of what this technique does is also accomplished with RJS templates. Why not just use RJS templates? Because Prototype sucks balls in the documentation department. jQuery doesn’t. Therefore, I use jQuery.
Anywho, let’s start with a controller action:
def create @object = Object.new(params[:object]) if @stock_color.save flash[:positive] = %{Object saved.} else flash[:negative] = %{Object not saved.} end if request.xhr? render(:partial => 'xhr_create', :layout => false) else redirect_to(:action => 'index') end end
And the Ajax-specific partial for that action:
<%= { :flash => { :positive => flash[:positive], :negative => flash[:negative] }, :objects => { :object => @object }, :views => { :some_partial => render(:partial => 'some_partial') } }.to_json %>
Note the to_json method we’re appending to our Hash. This converts and injects everything we care about from Rails directly into a format easily read and manipulated by JavaScript. We could easily add all our session and params data, too.
Finally, and most importantly, we’ll see how much easier it is to manipulate our page now that we have all the information we could possibly want from the Rails environment:
$('#quote_form').submit(function() { $.getJSON('/objects/create', $(this).serialize(), function(response) { Flash.set(response.flash); // An object I created to handle setting the flash. if(response.objects.quote.errors.length == 0) { $('#objects').append('<li>' + response.objects.object.attributes.name + '</li>'); $('#object_form').resetForm(); } else { // Handle the error here. Remember, you have access to the standard ActiveRecord errors! } }); // Don't actually submit the form, let the above JavaScript do its thing. return false; });
The pattern is a simple one.
- Do what you would normally do in a controller action.
- In addition to what you just did in that action, conditionally render a standard .rhtml partial if the request came from JavaScript.
- In the view that gets rendered for Ajax requests, create a standard Ruby hash with all the information you’re interested in. This includes any instance variables, session, params and even other views (this is good for generating lots of HTML that would otherwise be a hassle to generate in JavaScript).
- Call to_json on that hash.
- Smile with glee at all the kick ass information you have available to you in JavaScript!
The only thing I don’t like about this approach is that the controller code gets a bit longer and slightly more complex — it also introduces new views where there were previously none. On the flip side, it makes handling Ajax requests a freaking breeeze, and that’s worth it!