I am building a flight booking app with Rails that lets you select airports, date and number of passengers. Once you select the airports and dates, it gives you radio buttons to select which flight you want and, once you click submit, you are taken to a booking confirmation page where you are asked to provide passenger info.
The confirmation page(bookings#new) has a nested form to include passengers in the booking object. To do this, I have first set the following models and associations:
class Passenger < ApplicationRecord
belongs_to :booking
end
class Booking < ApplicationRecord
belongs_to :flight
has_many :passengers
accepts_nested_attributes_for :passengers
end
And the relevant migrations that result in the following schema tables:
create_table "bookings", force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "flight_id"
t.integer "passenger_id"
end
create_table "passengers", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "booking_id"
t.index ["booking_id"], name: "index_passengers_on_booking_id"
end
From what I understand, the flow goes like this:
User selects flight -> User submits flight, goes to Booking#new through the #new method on my controller:
class BookingsController < ApplicationController
def new
#booking = Booking.new
#flight = Flight.find(params[:flight_id])
params[:passengers_number].to_i.times do #params passed from select flight page
#booking.passengers.build
end
end
Then, the form I built takes over on new.html.erb:
<%= form_with model: #booking, url: bookings_path do |f| %>
<%= f.hidden_field :flight_id, value: #flight.id %>
<% #booking.passengers.each_with_index do |passenger, index| %>
<%= f.fields_for passenger, index: index do |form| %>
<h4><%= "Passenger #{index+1}"%> <br> </h4>
<%= form.label :name, "Full name:" %>
<%= form.text_field :name %>
<%= form.label :email, "Email:" %>
<%= form.email_field :email %>
<% end %>
<% end %>
<%= f.submit "Confirm details"%>
<% end %>
I fill it in with names and emails, click 'Confirm details' and I get this error on my terminal:
Unpermitted parameter: :passenger
Why? I have set accepts_nested_attributes_for :passengers on my Booking model, my booking_params method is:
def booking_params
params.require(:booking).permit(:flight_id,
:passengers_attributes => [:name, :email, :passenger_id, :created_at, :updated_at])
end
and my #create method is:
def create
#booking = Booking.new(booking_params)
respond_to do |format|
if #booking.save
format.html { redirect_to #booking, notice: "booking was successfully created." }
format.json { render :show, status: :created, location: #booking }
else
format.html { redirect_to root_path, alert: "booking failed, #{#booking.errors.full_messages.first}" , status: :unprocessable_entity }
format.json { render json: #booking.errors, status: :unprocessable_entity }
end
end
end
Is there something I am not permitting properly? Note that if I set booking_params as params.require(:booking).permit! it gives me an unknown attribute 'passenger' for Booking error. But I have defined associations and database on passenger and booking, at least to my knowledge.
Thanks in advance
Edit: The server log that generates the error is:
Started POST "/bookings" for ::1 at 2021-08-27 17:52:17 +0300
Processing by BookingsController#create as HTML
Parameters: {"authenticity_token"=>"[FILTERED]", "booking"=>{"flight_id"=>"5", "passenger"=>{"0"=>{"name"=>"Jason Smason", "email"=>"jason#ymail.com"}, "1"=>{"name"=>"Joe Smith", "email"=>"Joe#smith.com"}}}, "commit"=>"Confirm details"}
Unpermitted parameter: :passenger
Edit2: I followed PCurell's advice and changed my fields_for to <%= f.fields_for :passengers, passenger, index: index do |form| %> and my booking params to :passenger => [:name, :email, :passenger_id, :created_at, :updated_at])
That generated a different error: Unpermitted parameter: passengers_attributes'. So I changed my controller's booking_params` to:
def booking_params
params.require(:booking).permit(:flight_id,
:passengers_attributes => [:name, :email, :passenger_id, :created_at, :updated_at],
:passenger => [:name, :email, :passenger_id, :created_at, :updated_at])
end
With that, I successfully managed to create a booking with 2 passengers. However, the passengers are blank; their name and email are nil. The log says:
Parameters: {"authenticity_token"=>"[FILTERED]", "booking"=>{"flight_id"=>"1", "passengers_attributes"=>{"0"=>{"0"=>{"name"=>"John Smith", "email"=>"John#smith.com"}}, "1"=>{"1"=>{"name"=>"Burger King", "email"=>"bk#bk.com"}}}}, "commit"=>"Confirm details"}
Unpermitted parameter: :0
Unpermitted parameter: :1
I might be able to hardcode :0 and :1 to pass, but surely that's not the Rails way. Is there a way to dynamically let them in? Or am I doing the whole thing wrong?
Your usage of fields_for is incorrect. When you look at the example in the guide you will notice that there is no need to wrap it with an .each if used for a collection.
10.2 Nested Forms
The following form allows a user to create a Person and its
associated addresses.
<%= form_with model: #person do |form| %>
Addresses:
<ul>
<%= form.fields_for :addresses do |addresses_form| %>
<li>
<%= addresses_form.label :kind %>
<%= addresses_form.text_field :kind %>
<%= addresses_form.label :street %>
<%= addresses_form.text_field :street %>
...
</li>
<% end %>
</ul>
<% end %>
When an association accepts nested attributes fields_for renders its block once for every element of the association. In particular,
if a person has no addresses it renders nothing. A common pattern is
for the controller to build one or more empty children so that at
least one set of fields is shown to the user. The example below would
result in 2 sets of address fields being rendered on the new person
form.
def new
#person = Person.new
2.times { #person.addresses.build }
end
When applying this to your code, removing the .each wrapper and changing passenger into :passengers should do the trick. You can access the index through the FormBuilder instance (form) passed to the fields_for block.
<%= form_with model: #booking, url: bookings_path do |f| %>
<%= f.hidden_field :flight_id, value: #flight.id %>
<%= f.fields_for :passengers do |form| %>
<h4>Passenger <%= form.index + 1 %></h4>
<%= form.label :name, "Full name:" %>
<%= form.text_field :name %>
<%= form.label :email, "Email:" %>
<%= form.email_field :email %>
<% end %>
<%= f.submit "Confirm details"%>
<% end %>
As #Rockwell Rice mentioned in his comment:
The problem here is that you are permitting the wrong param.
def booking_params
params.require(:booking).permit(:flight_id,
:passenger => [:name, :email, :passenger_id, :created_at, :updated_at])
end
Should work.
Although you might encounter another error.
I think that you are constructing your fields_for wrong.
This should be what you are looking for (and you will not have to change the booking_params)
<%= form_with model: #booking, url: bookings_path do |f| %>
<%= f.hidden_field :flight_id, value: #flight.id %>
<% #booking.passengers.each_with_index do |passenger, index| %>
<%= f.fields_for :passengers, passenger, index: index do |form| %>
<h4><%= "Passenger #{index+1}"%> <br> </h4>
<%= form.label :name, "Full name:" %>
<%= form.text_field :name %>
<%= form.label :email, "Email:" %>
<%= form.email_field :email %>
<% end %>
<% end %>
<%= f.submit "Confirm details"%>
<% end %>
The above should work.
If you don't care too much about the index this would work as well:
<%= form_with model: #booking, url: bookings_path do |f| %>
<%= f.hidden_field :flight_id, value: #flight.id %>
<%= f.fields_for #booking.passengers do |form| %>
<%= form.label :name, "Full name:" %>
<%= form.text_field :name %>
<%= form.label :email, "Email:" %>
<%= form.email_field :email %>
<% end %>
<%= f.submit "Confirm details"%>
<% end %>
Related
I am new in ror and when I submit my form:
<%= form_for :project, url: projects_path, html: {id:'form'} do |f| %>
<%= f.text_field :text, placeholder: 'Новая задача' %>
<%= link_to 'Отмена', '', id:'cancel_link' %>
<%= link_to 'Отправить', projects_path, id:'submit_link' %>
<% end %>
Have error:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"OR2HWCi3zVz9gB5VAmnzbEuzIwFGE58JlLrWQdNcws6FVTzqh5Cu0zvUJTUEv2O/sCvU9HuadJYr3mfA40ehGA==", "project"=>{"text"=>"NEW ITEM"}} Unpermitted parameter: :text
Have two models:
class Project < ApplicationRecord
has_many :todos
validates :title, presence: true
accepts_nested_attributes_for :todos
end
class Todo < ApplicationRecord
belongs_to :project, required: false
end
The Todo model has a text attribute in which our todo should be located
Controller
class ProjectsController < ApplicationController
def index
#projects = Project.all
end
def create
#project = Project.new(project_params)
if #project.save
redirect_to root_path
end
end
def update
end
private
def project_params
params.require(:project).permit(:title, todos_attributes: [:id, :text])
end
end
Project db
class CreateProjects < ActiveRecord::Migration[5.2]
def change
create_table :projects do |t|
t.string :title
t.string :todos
t.timestamps
end
Todo db
class CreateTodos < ActiveRecord::Migration[5.2]
def change
create_table :todos do |t|
t.text :text
t.boolean :isCompleted
t.integer :project_id
t.timestamps
end
I'm requesting the todo attributes using accepts_nested_attributes_for: todos, the controller is also registered on the guides, in project_params I request todos_attributes. But when sending a form to the database, the value is text. He does not save in db. Can u help please
In order to save text field in Todo model, you have to create nested form. Use nested_form gem for this purpose.
A vague example to show how it works:
<%= nested_form_for :project, url: projects_path, html: { id: 'form' } do |f| %>
<%= f.text_field :title, placeholder: 'Новая задача' %>
<%= f.fields_for :todos do |todo_form| %>
<%= todo_form.text_field :text %>
<%= todo_form.link_to_remove "Remove this todo" %>
<% end %>
<p><%= f.link_to_add "Add a todo", :todos %></p>
<%= link_to 'Отмена', '', id:'cancel_link' %>
<%= link_to 'Отправить', projects_path, id:'submit_link' %>
<% end %>
In controller, to have the functionality of removing a todo in case of editing a project:
def project_params
params.require(:project).permit(:title, todos_attributes: [:id, :text, _destroy])
end
In the migration CreateProjects < ActiveRecord::Migration[5.2], I do not think that you require todos as a string.
The form which you created is wrong, you need to create a nestead_form
It is giving you and Unpermitted parameter error because the text is not a field of project model you can check this on your migration file. You need to change it to title because the title is the field of project model.
And for to create a nested form you need to do some changes in your form
<%= form_for :project, url: projects_path, html: {id:'form'} do |f| %>
<%= f.text_field :title, placeholder: 'Новая задача' %>
<%= f.fields_for :todos do |todo| %>
<%= f.text_field :text %>
<% end %>
<%= link_to 'Отмена', '', id:'cancel_link' %>
<%= link_to 'Отправить', projects_path, id:'submit_link' %>
<% end %>
I am running a Rails 5.1 app with the following information:
Models
class Company < ApplicationRecord
has_many :complaints
accepts_nested_attributes_for :complaints
validates :name, presence: true
end
class Complaint < ApplicationRecord
belongs_to :company
validates :username, :priority, presence: true
end
Controller
class ComplaintController < ApplicationController
def new
#company = Company.new
#company.complaints.build
end
def create
#company = Company.new(company_params)
respond_to do |format|
if #company.save
format.html { redirect_to complaint_url }
else
format.html { render :new }
end
end
end
private
def company_params
params.require(:company).permit(:name, complaints_attributes: [:username, :priority])
end
Form in view
<%= form_for #company do |f| %>
<%= f.label :name, "Company" %>
<%= f.text_field :name, type: "text" %>
<%= f.fields_for :complaints do |complaint| %>
<%= complaint.label :username, "Username" %>
<%= complaint.text_field :username %>
<%= complaint.label :priority, "Priority" %>
<%= complaint.text_field :priority %>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
If I have just one input field for the complaint_attributes part of the form (in other words just one field for username and one field for priority as shown above), this works just fine.
However, if I want to have multiple fields for username/priority in the form, so that I can submit multiple username/priority combinations in a single submission, I find that submitting the form will only save the last username/priority values from the form. Example of this view would be:
<%= form_for #company do |f| %>
<%= f.label :name, "Company" %>
<%= f.text_field :name, type: "text" %>
<%= f.fields_for :complaints do |complaint| %>
<div>
<%= complaint.label :username, "Username" %>
<%= complaint.text_field :username %>
<%= complaint.label :priority, "Priority" %>
<%= complaint.text_field :priority %>
</div>
<div>
<%= complaint.label :username, "Username" %>
<%= complaint.text_field :username %>
<%= complaint.label :priority, "Priority" %>
<%= complaint.text_field :priority %>
</div>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
I noticed that when submitting the form, I get a hash like this (for submitting single complaint):
{"utf8"=>"✓", "authenticity_token"=>"...", "company"=>{"name"=>"Test", "complaints_attributes"=>{"0"=>{"username"=>"test_person", "priority"=>"1"}}}, "commit"=>"Submit"}
Is there any way to modify the params to make it similar to this and have it saved to the DB?:
{"utf8"=>"✓", "authenticity_token"=>"...", "company"=>{"name"=>"Test", "complaints_attributes"=>{"0"=>{"username"=>"test_person", "priority"=>"1"}"1"=>{"username"=>"test_person", "priority"=>"2"}}}, "commit"=>"Submit"}
Or if not the above, what would be the best way to have the username/priority values saved if using multiple fields for them in a single form?
EDIT: I should point out that I can dynamically add the username/priority field groups as needed, so I don't want to be restricted to a set number.
the second block will override the first fields... you should instead build many complaints in the controller:
def new
#company = Company.new
3.times { #company.complaints.build }
end
and then with the following form it should generate to inputs according to the number of complaints you have built:
<%= form_for #company do |f| %>
<%= f.label :name, "Company" %>
<%= f.text_field :name, type: "text" %>
<%= f.fields_for :complaints do |complaint| %>
<%= complaint.label :username, "Username" %>
<%= complaint.text_field :username %>
<%= complaint.label :priority, "Priority" %>
<%= complaint.text_field :priority %>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
The goal is simple, create a document with a name and description an attached CSV file.
I have the following migration:
class CreateKeywords < ActiveRecord::Migration
def change
create_table :keywords do |t|
t.string :name, null: false, unique: true
t.string :description, null: false
t.string :keys, null: false
t.timestamps null: false
end
end
end
Model:
class Keyword < ActiveRecord::Base
mount_uploader :keys, KeywordsUploader
validates :name, :description, :keys, presence: true
validates :name, uniqueness: true
end
Controller:
class KeywordsController < ApplicationController
def index
#keywords = Keyword.all
end
def new
#keyword = Keyword.new
end
def create
puts "Keyword params"
pp keyword_params
#keyword = Keyword.new(keyword_params)
if #keyword.save
flash[:success] = "New server created!"
redirect_to #keyword
else
render 'new'
end
end
private
def keyword_params
params.permit(:keyword).permit(:name, :description, :keys)
end
end
And finally view:
<% provide(:title, 'Create keywords set') %>
<h1>New keywords set</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for #keyword, :html => {:multipart => true } do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :description %>
<%= f.text_field :description %>
<%= f.label :keywords %>
<%= f.file_field :keys %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
When I'm trying to submit a form with all required fields I'm getting the following error in a view:
1) Name can't be blank
2) Description can't be blank
3) Keys can't be blank
In Rails console I can see the following during the form submission:
Started POST "/keywords" for ::1 at 2015-10-09 17:21:40 +0300
Processing by KeywordsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"QO69fZ9xx1+mXRTL1TqZQLs9kYriQj4sqxV0t8P8XG2nu1FvKmOw6fISpmvi70VlWrD9bCJg7bCqwLwUfvwGRQ==", "keyword"=>{"name"=>"asd", "description"=>"asd"}, "commit"=>"Create keywords"}
Keyword params
Unpermitted parameters: utf8, authenticity_token, keyword, commit
{}
Unpermitted parameters: utf8, authenticity_token, keyword, commit
As you can see, the strong params function returns an empty hash, and
that is why it's impossible to create an instance of a model "Keyword".
What can be a problem?
Resolved!
Instead of using:
def keyword_params
params.permit(:keyword).permit(:name, :description, :keys)
end
I should been using:
def keyword_params
params.require(:keyword).permit(:name, :description, :keys)
end
The issue was caused by incorrect keyword_params method.
I have a Word model and a Category model.
Words has_and_belongs_to_many Categories.
Categories has_and_belongs_to_many Words.
Now, I have everything setup and running in the console, for example you can do:
Word.create(title: "Testicles")
testicles = Word.first
Category.create(title: "Genitalia")
genitalia = Category.first
testicles.categories << genitalia
testicles.categories
=> "genitalia"
Now I can get this up and running using forms in the views too, but only if I have separately created the Category in its own form on a separate page, and same with the Word. Then in the Word show view I can create a form to assign the category to it.
HOWEVER... what I really want to do is to do all this at the same time when I create the Word i.e. on the 'new word' view.
I'm having big problems working out how to do this. I think I'm right in saying that I can only have one form and one submit in that view, so I think I somehow have to send everything from that form to, say, the WordsController, and work some magic in there, but exactly what to do here is giving me big headaches. Can anyone help?
I haven't created a User model or setup authentication yet, so there are no obstacles in that respect.
models/word.rb:
class Word < ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :categories
validates :title, presence: true
end
models/category.rb:
class Category < ActiveRecord::Base
has_and_belongs_to_many :words
validates :title, presence: true
end
schema.rb:
ActiveRecord::Schema.define(version: 20150529144121) do
create_table "categories", force: :cascade do |t|
t.string "title"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "categories_words", id: false, force: :cascade do |t|
t.integer "category_id"
t.integer "word_id"
end
add_index "categories_words", ["category_id"], name: "index_categories_words_on_category_id"
add_index "categories_words", ["word_id"], name: "index_categories_words_on_word_id"
create_table "words", force: :cascade do |t|
t.string "title"
t.text "description"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "words", ["user_id"], name: "index_words_on_user_id"
end
After experimenting with various form_tag wizardry (and failing badly), at the moment I'm using this form:
<%= form_for(#word) do |f| %>
<% if #word.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#word.errors.count, "error") %> prohibited this word from being saved:</h2>
<ul>
<% #word.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :title, 'Word' %><br>
<%= f.text_field :title %>
</div>
<div class="field">
<%= f.label :description, 'Definition' %><br>
<%= f.text_area :description %>
</div>
<div class="field">
<%= f.label :categories, 'Category' %><br>
<%= f.text_field :categories %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
In the view, the form has:
#<Category::ActiveRecord_Associations_CollectionProxy:0x007f16dd834820>
in the 'Category' box, but this can be cleared and your own category inputted. When submitting the form with all the fields filled in, I get a NoMethodError.
So basically you want to create both a word and a category at the same time, and link them. If the classical solutions (for example using the nested_form gem) don't do exactly what you want, you can try this approach.
You really want a custom view/controller for this and do the business in your controller as you suggest. I suggest that instead you used a simple form_tag where you can add some fieldsets.
View
<%= form_tag your_custom_route_path, :html => {:class => "form-horizontal"} do |form| %>
<%= fields_for :word, #word do |f| %>
<%= f.text_field :title %>
<% end %>
<%= fields_for :category, #category do |f| %>
<%= f.text_field :title %>
<% end %>
<% end %>
routes
post '/yourRoute', to: 'your_controller#your_action_create', as: 'your_custom_route'
Controller featuring some actions
class YourController < ApplicationController
# new
def your_action_new
#word = Word.new
#category = Category.new
end
# Post
def your_action_create
#word = Word.new(word_params)
#category = Category.new(category_params)
if #word.save and #category.save
#word.categories << #category
#word.save
end
end
private
def words_params
params.require(:word).permit(:title, ...)
end
def category_params
params.require(:category).permit(:title...)
end
Thanks to Cyril DD's help, I've expanded on that and created a form and custom controller that will create a new Word with a new Category assigned to it and/or the user can assign existing Categories to that new Word.
_form.html.erb:
<%= form_tag(controller: "new_word", action: "create_word_and_category", method: "post") do %>
<% if #word.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#word.errors.count, "error") %> prohibited this category from being saved:</h2>
<ul>
<% #word.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= fields_for :word, #word do |word_form| %>
<div class="field">
<%= word_form.label(:title, "Word:") %><br>
<%= word_form.text_field(:title, id: "new_word", required: true) %>
</div>
<div class="field">
<%= word_form.label(:description, "Definition:") %><br>
<%= word_form.text_area(:description) %>
</div>
<% end %>
<%= fields_for :category, #category do |category_form| %>
<% if Category.count > 0 %>
<div class="field">
<%= category_form.label(:title, "Choose from existing Categories:") %><br>
<%= category_form.collection_check_boxes(:category_ids, Category.all, :id, :title) %>
</div>
<% end %>
<h4>AND/OR...</h4>
<div class="field">
<%= category_form.label(:title, "Create and Use a New Category:") %><br>
<%= category_form.text_field(:title) %>
</div>
<% end %>
<div class="actions">
<%= submit_tag("Create") %>
</div>
<% end %>
new_word_controller.rb (it's not completely finished yet, but it 'does what it says on the tin'):
class NewWordController < ApplicationController
def create_word_and_category
#word = Word.new(word_params)
if #word.save
(params["category"])["category_ids"].each do |i|
next if i.to_i == 0
#word.categories << Category.find(i.to_i) unless #word.categories.include?(Category.find(i.to_i))
end
if category_params.include?(:title) && ((params["category"])["title"]) != ""
#word.categories << Category.new(title: (params["category"])["title"])
end
end
if #word.save
redirect_to words_path, notice: 'Word was successfully created.'
else
redirect_to new_word_path, notice: 'A valid word was not submitted.'
end
end
private
# Use callbacks to share common setup or constraints between actions.
# def set_word
# #word = Word.find(params[:id])
# end
# Never trust parameters from the scary internet, only allow the white list through.
def word_params
params.require(:word).permit(:title, :description, :user_id)
end
def category_params
params.require(:category).permit(:title, :category_ids, :category_id)
end
end
In my routes.rb:
post 'create_word_and_category' => 'new_word#create_word_and_category'
I'm having going to place that controller code into the old words_controller, because I don't see a need to have it in a separate controller like this. If anyone has any thoughts/critique, great. And if anyone can help me write an rspec controller test for this controller code, even better! I'm having trouble with that.
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?
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.
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][]