32x32_su_3d

Rails Complex Forms + jQuery == Headache

See update at the end for a solution that doesn't go back to the server.

A co-worker came to me today with a problem where he couldn't make his code DRY. He is using the concepts from Railscasts #75: Complex Forms Part 3 to create and update a model and its associations in a single form. Where Railscasts #75 breaks down for him is he is using jQuery for the project. Until Rails 3, jQuery and RJS just don't mix. Since RJS is the mechanism for inserting the partial code for new associated models client side in Railscasts #75 this was a problem.

His original solution was to basically re-create the partial in a javascript function that uses jQuery. This was the not DRY part. Everytime he updated the partial he needed remember to update the javascript version of that partial too.

As I was getting ready for bed tonight, I thought of a solution to the problem. This, of course, has delayed me getting to bed at a reasonable hour as I didn't want to forget it. It's a little bit of a hack (actually not too bad), and puts the partial code in one place. I haven't tested this with rendering existing tasks with edit, but since I didn't deviate from Railscasts #75 code that much, I expect it's real close to working if it doesn't work already.

This solution takes the concept from Railscasts #75 and adds a custom REST action (as shown in Railscasts #35: Custom REST Actions) to render the partial for inserting into the form with jQuery. It's actually very little code to serve up the partial for jQuery.

The additional code beyond what is prescribed in Railscast #75 is as follows:

// config/routes.rb
map.resources :projects, :collection => { :task_partial => :get }

// app/controllers/projects_controller.rb
def task_partial
  @task = Task.new

  respond_to do |format|
    format.html { render :action => 'task_partial', :layout => false }
  end
end

// app/views/projects/task_partial.html.erb
<%= render :partial => 'task', :object => @task %>

// app/views/projects/_fields.html.erb
<%=link_to_function 'Add Task', "append_partial('#tasks', '#{task_partial_projects_path}');" %>

// public/javascripts/application.js
function append_partial(target_id, partial_path) {
    $.get(partial_path, null, function(data, status, xhr) {
        $(target_id).append(data);
    });
}

Update

There is a much better solution to this problem. You can get rid of the call back to the server by replacing the Add Task link in Railscasts #75 with the following:

// app/views/projects/_fields.html.erb
<%=link_to_function 'Add Task', "$('#tasks').append('#{escape_javascript render(:partial => 'task', :object => Task.new)}')" %>

This eliminates the need for the changes in config/routes.rb, app/controllers/projects_controller.rb and public/javascripts/application.js, and the need for the existence of app/views/projects/task_partial.html.erb as well.