the more things change...

Jared Carroll

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:

  1. 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.
  2. 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.