Doubly-nested models

Hi all,

I have three models, like so:

User
belongs_to :person
accepts_nested_attributes_for :person, :allow_destroy => false
has_many :contacts

Person
has_many :contacts
accepts_nested_attributes_for :contacts, :allow_destroy => true

Contact
belongs_to :person

And actually, it’s a bit more complicated because Contact uses single-
table polymorphism and may be a PhoneNumber or an Address. Different
information would be filled in for each on the form, so right now I
have separate partials for each of those.

My goal is to create both the User and the Person, as well as a
Contact (if specified), when the first form is submitted. I’ve got it
so it creates the User and the Person, but it doesn’t set any of the
data in the Person row. It won’t create any Contacts at all.

new.html.erb

Sign up

<% form_for @user, :url => account_path do |f| %>
<%= f.error_messages %>
<%= render :partial => “form”, :object => f %>

<%= f.submit "Register" %>

<% end %>

_form.html.erb

<%= error_messages_for :user %>

<% form_for @user do |user_form| -%>
<%= user_form.label :email %><br />
<%= user_form.text_field :email %><br />
<br />
<%= user_form.label :password, user_form.object.new_record? ?

nil : “Change password” %>

<%= user_form.password_field :password %>



<%= user_form.label :password_confirmation, ‘Confirm’ %>

<%= user_form.password_field :password_confirmation %>



<%= render :partial => “person”, :object => @person %>
<% end -%>


_person.html.erb

<% form_for @person do |person_form| -%>
<%= person_form.label :title %><br />
<%= person_form.text_field :title %><br />
<br />
<%= person_form.label :first_name %><br />
<%= person_form.text_field :first_name %><br />
<br />
<%= person_form.label :last_name %><br />
<%= person_form.text_field :last_name %><br />
<br />
<%= person_form.label :job_title %><br />
<%= person_form.text_field :job_title %><br />
<br />
<div id="contacts">
  <%= render :partial => "address",      :collection =>

@person.addresses %>
<%= render :partial => “phone_number”, :collection =>
@person.phone_numbers %>



<%= add_contact_link :address %>
<%= add_contact_link :phone_number %>


<% end -%>

_phone_number.html.erb (_address is very similar)

<% new_or_existing = phone_number.new_record? ? 'new' : 'existing' %> <% prefix = "person[#{new_or_existing}_contact_attributes][]" %>

<% fields_for prefix, phone_number do |contact_form| -%>


<%= contact_form.hidden_field :type %>
Area + Phone <%= contact_form.text_field :value %>

<%= remove_contact_link :phone_number, ‘remove’ %>


<% end -%>

users_controller.rb

def new
@user = User.new
@person = @user.build_person
@addresses = @person.addresses.build
@phone_numbers = @person.phone_numbers.build
end

POST /users

POST /users.xml

def create
@user = User.new(params[:user])
if @user.save
flash[:notice] = ‘User was successfully registered.’
redirect_back_or_default account_url
else
render :action => :new
end
end

Any thoughts? Apologies if things are a little unorthodox. I’ve been
fiddling for hours.

Thanks,
John

If I recall correctly, you can’t nest forms, which is what your _person
partial is attempting to do.

I don’t have the references handy, but google for rails multipart forms
and you should get some hits.

Ar Chron wrote:

If I recall correctly, you can’t nest forms, which is what your _person
partial is attempting to do.

That is correct. HTML doesn’t allow nesting of elements. Since
each form_for creates a tag, you’re producing bad HTML. Replace
the inner form_for with fields_for.

I don’t have the references handy, but google for rails multipart forms
and you should get some hits.

Stephen Chu’s params[:fu] series is lovely, though perhaps slightly out
of date. Have a look at it.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Thanks all, think I fixed it, mostly.

UsersController:
def create
@user = User.new(params[:user])
@person = @user.build_person(params[:user][:person])
@person.contacts_attributes = params[:person][:contacts]
if @user.save
flash[:notice] = ‘User was successfully registered.’
redirect_back_or_default account_url
else
render :action => :new
end
end

def new
@user = User.new
@person = @user.build_person
@person.addresses.build
@person.phone_numbers.build
end

new.html.erb:

Sign up

<% form_for @user, :url => account_path do |f| %>
<%= f.error_messages %>
<%= render :partial => “form”, :object => f %>

<%= f.submit "Register" %>

<% end %>

_form.html.erb
<%= error_messages_for :user %>

<% form_for @user do |user_form| -%>
<%= user_form.label :email %>
<%= user_form.text_field :email %>
<br />
<%= user_form.label :password, user_form.object.new_record? ?

nil : “Change password” %>
<%= user_form.password_field :password %>


<%= user_form.label :password_confirmation, ‘Confirm’ %>
<%= user_form.password_field :password_confirmation %>


<%= render :partial => “person”, :object => @person %>
<% end -%>


_person.html.erb:

<% fields_for @person do |person_form| -%>
<%= person_form.label :title %><%= person_form.select :title,

Person::TITLES %>
<%= person_form.label :first_name %><%=
person_form.text_field :first_name %>
<%= person_form.label :last_name %><%=
person_form.text_field :last_name %>



<%= person_form.label :job_title %>

<%= person_form.text_field :job_title %>




<%= render :partial => “address”, :collection =>
@person.addresses %>
<%= render :partial => “phone_number”, :collection =>
@person.phone_numbers %>


<%= add_contact_link :address %>
<%= add_contact_link :phone_number %>


<% end -%>

_address.html.erb:

<% fields_for "person[contacts][]", address do |contact_form| -%>

Address
<%= contact_form.hidden_field :type %>
<%= contact_form.select :label, Address::LABELS, :selected => Address::LABELS.first %> Street <%= contact_form.text_field :value %>
Zip+4 <%= contact_form.text_field :zip_plus_four, :limit => 10 %> <%= remove_contact_link :address, 'remove' %>

<% end -%>

And phone number is about the same as that.

The only problem is that it won’t save the contents of the :type
hidden_field in the last partial. It fails to validate. That’s a topic
for another post, though, I suppose.