4

I'm writing a simple Rails model called Person that has_many :phone_numbers and I'm trying to save the phone numbers in a complex form without manually writing setter methods. accepts_nested_attributes_for should do what I want but I'm having trouble getting it to work. Here's the code I have so far:

Migration

class CreatePeople < ActiveRecord::Migration
  def self.up
    create_table :people do |t|
      t.string :first_name
      t.string :last_name
      t.integer :address_id      
      t.string :email

      t.timestamps
    end
  end

  def self.down
    drop_table :people
  end
end

class CreatePhoneNumbers < ActiveRecord::Migration
  def self.up
    create_table :phone_numbers do |t|
      t.string :number, :limit => 10
      t.string :extension, :limit => 5
      t.string :description, :null => false
      t.integer :telephone_id
      t.string :telephone_type

      t.timestamps
    end
  end

  def self.down
    drop_table :phone_numbers
  end
end

Models

class Person < ActiveRecord::Base
  has_one :address, :as => :addressable, :dependent => :destroy
  has_many :phone_numbers,
    :as => :telephone,
    :dependent => :destroy

  accepts_nested_attributes_for :phone_numbers

  attr_protected :id

  validates_presence_of :first_name, :last_name, :email
end

class PhoneNumber < ActiveRecord::Base
  attr_protected :id

  belongs_to :telephone, :polymorphic => true
end

View

<% form_for @person, :builder => CustomFormBuilder do |f| %>
  <%= f.error_messages %>

  <%= f.text_field :first_name %>
  <%= f.text_field :last_name %>

  <% fields_for "person[address]", @person.address, :builder => CustomFormBuilder do |ff| %>  
    <%= ff.text_field :address_1 %>
    <%= ff.text_field :address_2 %>
    <%= ff.text_field :city %>
    <%= ff.text_field :state %>
    <%= ff.text_field :zip %>  
  <% end %>

  <h2>Phone Numbers</h2>
  <% @person.phone_numbers.each do |phone_number| %>
    <% fields_for "person[phone_numbers][]", phone_number, :builder => CustomFormBuilder do |ff| %>
      <%= ff.text_field :description %>
      <%= ff.text_field :number %>
      <%= ff.text_field :extension %>
    <% end %>
  <% end %>

  <%= f.text_field :email %>

  <%= f.submit 'Create' %>

<% end %>

Controller

def new
  @person = Person.new 
  @person.build_address
  @person.phone_numbers.build

  respond_to { |format| format.html }
end

def create
  @person = Person.new(params[:person])

  respond_to do |format|
    if @person.save
      flash[:notice] = "#{@person.name} was successfully created."
      format.html { redirect_to(@person) }
    else
      format.html { render :action => 'new' }
    end
  end
end

I have verified that a phone_numbers= method is being created, but the post still causes:

PhoneNumber(#69088460) expected, got HashWithIndifferentAccess(#32603050)

RAILS_ROOT: H:/projects/test_project

C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations/association_proxy.rb:263:in `raise_on_type_mismatch'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations/association_collection.rb:319:in `replace'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations/association_collection.rb:319:in `each'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations/association_collection.rb:319:in `replace'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/associations.rb:1290:in `phone_numbers='
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2740:in `send'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2740:in `attributes='
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2736:in `each'
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2736:in `attributes='
C:/Ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.3/lib/active_record/base.rb:2434:in `initialize'
H:/projects/salesguide/app/controllers/accounts_controller.rb:46:in `new'
H:/projects/test_project/app/controllers/accounts_controller.rb:46:in `create'

I can get this to work by manually writing the phone_numbers= method, but this would cause a tremendous duplication of effort, I would much rather learn how to do this right. Can anybody see what I'm doing wrong?

2 Answers 2

6

You're forgetting the to call fields_for as a method on the person form. Otherwise you're not actually using fields_for in a accept_nested_attributes_for context. Michael's solution tries to trick Rails into treating the submission as a properly defined accepts_nested_attributes_for form.

The correct syntax for what you are trying to do is:

parent_form_object.fields_for id, object_containing_values, {form_for options}, &block

You'll find the code looks cleaner and simpler to debug if you provide a symbol as id, containing the association name of the child model as defined in your Person model.

Also, the each block you're using might cause problems if @person.phone_numbers is empty. You can ensure that there is at least one set of Phone Number fields with a line similar to the one I used with

<% @phs = @person.phone_numbers.empty? ? @person.phone_numbers.build : @person.phone_numbers %> 

With all corrections, this code will do what you want it to.

View

<% form_for @person, :builder => CustomFormBuilder do |f| %>
  <%= f.error_messages %>

  <%= f.text_field :first_name %>
  <%= f.text_field :last_name %>

  <% f.fields_for :address, @person.address, :builder => CustomFormBuilder do |address_form| %>  
    <%= address_form.text_field :address_1 %>
    <%= address_form.text_field :address_2 %>
    <%= address_form.text_field :city %>
    <%= address_form.text_field :state %>
    <%= address_form.text_field :zip %>  
  <% end %>

  <h2>Phone Numbers</h2>
    <% @phs = @person.phone_numbers.empty? ? @person.phone_numbers.build : @person.phone_numbers %>
    <% f.fields_for :phone_numbers, @phs, :builder => CustomFormBuilder do |phone_number_form| %>
      <%= phone_number_form.text_field :description %>
      <%= phone_number_form.text_field :number %>
      <%= phone_number_form.text_field :extension %>
    <% end %>

  <%= f.text_field :email %>

  <%= f.submit 'Create' %>

<% end %>

You might find it useful to check out the complex-form-examples repository on github for a working example. It also comes with code to dynamically add new entries for :has_many relationships from the view/form.

Sign up to request clarification or add additional context in comments.

Comments

4

I was playing around with accepts_nested_attributes_for yesterday when trying to figure out Rails form with three models and namespace. I needed to setup the form slightly differently, try using: person[phone_numbers_attributes][]

1 Comment

I can't believe that with all the documentation I've looked through, that wasn't pointed out more clearly. Thanks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.