Recently, like everyone else, we’ve been putting some AJAX in our apps.
Now in Rails you have 2 ways to respond to AJAX requests:
- Send HTML back to the client and let client side JavaScript (in the form of Rails helper methods) update the page with the response text. This can be done by rendering partials without layouts in your controller actions.
- Send JavaScript back to the client that gets executed automatically and contains the logic to update the page with the response text. This can be done by rendering RJS views in your controller actions that will be converted to JavaScript before being sent back to the client.
I like either way but I found out that responding with JavaScript that the client automatically executes results in some cleaner and simpler views. Plus using RJS, I can write my JavaScript in Ruby. I like this since I’ve already gotten used to writing my SQL in Ruby via migrations.
Let’s take the simple example of an AJAXified form where the form to create a new record is on the same page that lists all records, in other words on #index there’s a form that POSTs to #create.
Say in this app we’re creating users that have nothing more than a name.
class User < ActiveRecord::Base
validates_presence_of :name
end
Schema:
users (id, name)
Here’s our controller:
class UsersController < ApplicationController
def create
@user = User.new params[:user]
respond_to do |wants|
if @user.save
wants.html { redirect_to users_path }
wants.js
else
wants.html { render :action => :index }
wants.js
end
end
end
def index
@users = User.find :all
end
end
In create.rjs
:
if @user.valid?
page.insert_html :top, 'users', render(:partial => 'user',
:object => @user)
page[:errors].replace_html ''
page[:users_form].reset
else
page[:errors].replace_html error_messages_for(:user)
end
I don’t like the fact that there’s an ‘if’ in the action and in the create.rjs
template. Now in the normal non-AJAX version of the #create action I just
render the #index action’s view if the save fails so there’s only 1 ‘if’. 2
‘if’s are necessary in the AJAX version of the #create action because there’s only 1 RJS view
used (technically there’s only 1 view, index.rhtml
used in the non-AJAX
version because its used for both success and failure).
I’ve also seen alternative implementations of #create like this:
class UsersController < ApplicationController
def create
@user = User.new(params[:user])
@saved = @user.save
respond_to do |wants|
if @saved
wants.html { redirect_to users_path }
wants.js
else
wants.html { render :action => :index }
wants.js
end
end
end
def index
@users = User.find :all
end
end
In create.rjs
:
if @saved
page.insert_html :top, 'users', render(:partial => 'user',
:object => @user)
page[:errors].replace_html ''
page[:users_form].reset
else
page[:errors].replace_html error_messages_for(:user)
end
This results in the same amount of 'if’s but now we have this nice ugly boolean
flag instance variable telling us the success or failure of the #save call. In
my endless quest to remove all conditional logic from software, what I want is
another RJS view to handle the case in which the save fails, so I can get rid of
the 2nd 'if’ in my create.rjs
view.
Let’s try:
class UsersController < ApplicationController
def create
@user = User.new params[:user]
respond_to do |wants|
if @user.save
wants.html { redirect_to users_path }
wants.js
else
wants.html { render :action => :index }
wants.js { render :action => :index }
end
end
end
def index
@users = User.find :all
end
end
In index.rjs
:
page[:errors].replace_html error_messages_for(:user)
In create.rjs
:
page.insert_html :top, 'users', render(:partial => 'user',
:object => @user)
page[:errors].replace_html ''
page[:users_form].reset
What I added to UsersController#create is in the else
branch:
wants.js { render :action => :index }
However this is just going to respond back with index.rhtml
and not respond
with index.rjs
which has the RJS to display the errors on the page.
I thought maybe I could pass a :format parameter like:
wants.js { render :action => :index, :format => :js }
But no, this didn’t work either.
After some more trial and error I found out the following works:
class UsersController < ApplicationController
def create
@user = User.new params[:user]
respond_to do |wants|
if @user.save
wants.html { redirect_to users_path }
wants.js
else
wants.html { render :action => :index }
wants.js { render :action => 'index.rjs' }
end
end
end
def index
@users = User.find :all
end
end
I changed that same 1 line in UsersController#create this time to:
wants.js { render :action => 'index.rjs' }
Now actions for handling both AJAX and non-AJAX requests contain the same amount of conditional logic and follow the same pattern.