Rails Faux Nested Forms

Sunday 27 April 2008 3 comments

For adding an item via Ajax to related models then updating an existing select...

Here's the situation:

For this example we have a New Product form. Products belong to Companies so we have a select menu of Companies.

You want to add a product and you have a select menu of companies, but in this case you have a product from a company you have not dealt with in the past. So directly below your select we are going to create a text field from which you can add a new company. You fill in your company name, press the faux-submit button, your company gets added via the companies controller, then the select menu will automatically be updated, with your new company selected...

*DISCLAIMER

There is probably a better, safer, simpler, more DRY, more best-practice, more concise, more Railsy, and more Ruby ways of doing this. Personally, I had a hard time finding an answer to this problem on any Rails forums or tutorials, but I had previously done it with ease in PHP. So I applied those same principles here and it seems to work fine. Hopefully it helps you, and if you know of a better way to handle this situation I would be thrilled to hear it.

First let's start with the main form for adding a product:

First you have your original select wrapped in a div. The contents of the div (the select) are we will be updating with your new select....

<div id='companySelect'>
   <%= collection_select("company", "company_id" , @companies, "id", "name", {:prompt => 'Choose company...'}) %>
</div>

Now you have a script that identifies the div to update, gets the input value, then does an AjaxUpdate passing the new company value in the url.

<script type='text/javascript'>
function addCompany() {
   var selector = $('companySelect');
   var newCompany = $('newCompany_name');
   var companyValue = newCompany.getValue();
   var url = '/companies/createForAjax?company=' + companyValue
   new Ajax.Updater(selector, url); // END AJAX UPDATER
   return false;
   } // END FUNCTION
</script>

Here's your 'internal form' for submission of the new company. First a text field: <%= text_field 'newCompany', 'name' %>

Then a faux submit button

<a href='#' onmouseover="this.style.cursor='pointer'" onclick='return addCompany();'> Add New Company </a>

Now our main form view is done so let's move on....

ADD YOUR ROUTE to routes.rb:

map.connect 'companies/afterAjax', :controller=>'companies', :action=>'afterAjax'

Beef up your Companies Controller:

class CompaniesController < ApplicationController
  protect_from_forgery :only => [:create, :destroy, :update]

the protect_from_forgery line seems to solve token error problems for Ajax - makes me nervous though - is this creating security risks? If you know how better to deal with this - let me know....

# Add The Company
def createForAjax
   @company = Company.new()
   @company.name = params['compay']
   respond_to do |format|
     if @company.save
       format.html { redirect_to :action=>'afterAjax', :params=>{:id=>@company.id.to_s} }
     end
   end
end


# Generate A New Select With The Company You Created Selected
def afterAjax
   @companies = Company.select (or find :all -- whatever your method to get all companies)
   @company = Company.find(params[:id]) (this is the one you just created)
   respond_to do |format|
     format.html
   end
end

Finally, this Is The View For The New Select:

afterAjax.html.erb view in companiesController:

<%= select_tag 'product[company_id]', options_from_collection_for_select(@companies, :id, :name, @company.id) %>

That's It

Like I said, I imagine there are better and more concise ways of doing this and if you know them I would be happy to hear about it.

Tags: Rails

Comments

3 Comments Subscribe via RSS

  • Avatar

    Sol Irvine Tuesday, February 24, 2009, 10:33 PM

    A few notes:

    1. Be sure you add <%= javascript_include_tag 'prototype' %>
    at the top of your view, if it's not already there.

    2. The afterAjax method should be:

    @companies = Company.find(:all)
    @company = Company.find(params[:id])

    Works like a charm, and I really needed this!

  • Avatar

    Gordon Tuesday, April 07, 2009, 12:47 AM

    Thanks for mentioning that Sol, I updated the post.

  • Avatar

    Herb Wednesday, March 10, 2010, 02:03 PM

    Excellent. Had been looking for a solution to this since before rails 2.0. Although not railsy, it works for me.

    had to add
    { render :layout => false } to format.html as to not get my layout rendering within the new select box.