I'm running Rails 6.0.0.rc1 and having trouble getting a triple nested form working. I have products that have options that have option_values. So a Product might have an option called "Color" and an Option Value named "Red." I'd like to create all of these in a classic nested form in the Product form.
The form works and I can save Product with an Option, but not the Option Value on submit. I'm not sure why it's not working when I try to embed fields_for Option Values inside the fields_for Options.
What am I doing wrong here? I feel like I'm missing something obvious, but can't figure it out. (Probably not relevant, but note that I need to scope each object to account_id and my User has_one :account that's the reason for the hidden field.)
Here is my product model:
class Product < ApplicationRecord
belongs_to :account
has_many :options, dependent: :destroy
accepts_nested_attributes_for :options, allow_destroy: true
validates :account_id, presence: true
validates :name, presence: true
end
Option model:
class Option < ApplicationRecord
belongs_to :account
belongs_to :product
has_many :option_values, dependent: :destroy
accepts_nested_attributes_for :option_values, allow_destroy: true
validates :account_id, presence: true
validates :name, presence: true
end
OptionValue model:
class OptionValue < ApplicationRecord
belongs_to :account
belongs_to :option
validates :account_id, presence: true
validates :name, presence: true
end
Here's the Product form:
<%= form_with(model: product, local: true) do |f| %>
<%= f.fields_for :options do |options_form| %>
<fieldset class='form-group'>
<%= options_form.hidden_field :account_id, value: current_user.account.id %>
<%= options_form.label :name, 'Option' %>
<%= options_form.text_field :name, class: 'form-control' %>
</fieldset>
<%= f.fields_for :option_values do |values_form| %>
<fieldset class='form-group'>
<%= values_form.label :name, 'Value' %>
<%= values_form.text_field :name, class: 'form-control' %>
</fieldset>
<% end %>
<% end %>
<% end %>
ProductsController:
class ProductsController < ApplicationController
def new
#product = Product.new
#product.options.build
end
def create
#account = current_user.account
#product = #account.products.build(product_params)
respond_to do |format|
if #product.save
format.html { redirect_to #product, notice: 'Product was successfully created.' }
else
format.html { render :new }
end
end
end
private
def product_params
params.require(:product).permit(
:account_id, :name,
options_attributes: [
:id, :account_id, :name, :_destroy,
option_values_attributes:[:id, :account_id, :name, :_destroy ]
]
)
end
end
At first, you need to change the form to nest option_values inside option, and add account_id field to option values:
<%= form_with(model: product, local: true) do |f| %>
<%= f.fields_for :options do |options_form| %>
<fieldset class='form-group'>
<%= options_form.hidden_field :account_id, value: current_user.account.id %>
<%= options_form.label :name, 'Option' %>
<%= options_form.text_field :name, class: 'form-control' %>
</fieldset>
<%= options_form.fields_for :option_values do |values_form| %>
<fieldset class='form-group'>
<%= values_form.hidden_field :account_id, value: current_user.account.id %>
<%= values_form.label :name, 'Value' %>
<%= values_form.text_field :name, class: 'form-control' %>
</fieldset>
<% end %>
<% end %>
<% end %>
Also you need to build nested records in the controller. Another option is to build them dynamically via javascript (look at the cocoon gem, for example). To build 3 options with 3 values each:
def new
#account = current_user.account
# it is better to create associated product
#product = #account.products.new
3.times do
option = #product.options.build
3.times { option.option_values.build }
end
end
Update:
Because I was trying to follow along this Nested Form Railscast, the biggest problem was that I didn't realize that the version Ryan Bates has working is on "Edit", not "New", so I added the Products, Options, and Values via the console and got the form working with this code:
_form.html.erb
<%= f.fields_for :options do |builder| %>
<%= render 'option_fields', f: builder %>
<% end %>
_option_fields.html.erb
<fieldset class='form-group'>
<%= f.hidden_field :account_id, value: current_user.account.id %>
<%= f.label :name, 'Option' %>
<%= f.text_field :name, class: 'form-control' %>
<br>
<%= f.check_box :_destroy, class: 'form-check-input' %>
<%= f.label :_destroy, 'Remove Option' %>
<small id="optionHelp" class="form-text text-muted">
(e.g. "Size" or "Color")
</small>
<%= f.fields_for :option_values do |builder| %>
<%= render 'option_value_fields', f: builder %>
<% end %>
</fieldset>
_option_value_fields.html.erb
<fieldset class='form-group'>
<%= f.hidden_field :account_id, value: current_user.account.id %>
<%= f.label :name, 'Value' %>
<%= f.text_field :name, class: 'form-control' %>
<br>
<%= f.check_box :_destroy, class: 'form-check-input' %>
<%= f.label :_destroy, 'Remove Value' %>
<small id="optionValueHelp" class="form-text text-muted">
(e.g. "Small, Medium, Large" or "Red, Green, Blue")
</small>
</fieldset>
Also, the only difference with the Railscast is using strong parameters in the controller, so you'll just need to nest them like this:
ProductsController
def product_params
params.require(:product).permit(:account_id, :name, options_attributes [:id, :account_id, :name, :_destroy, option_values_attributes: [:id, :account_id, :name, :_destroy]])
end
OptionsController
def option_params
params.require(:option).permit(:account_id, :name, option_values_attributes [:id, :account_id, :name, :_destroy])
end
OptionValuesController
def option_value_params
params.require(:option_value).permit(:account_id, :option_id, :name)
end
Rather than building the nested objects in the controller I'm going to do it with Javascript as in the Railscast episode or with the Cocoon gem as Vasilisa suggested in her answer.
Just wanted to share the code that actually ended up working in case someone else runs into similar problems. I think the Railscast, though old, is still a great introduction to nested forms in Rails, but you just have to be aware of the changes required to use form_with and strong parameters. Big thanks to Vasilisa for helping me figure this out.
The main "gotchas" that you need to look out for when following the Rails Nested Form Railscast are this:
form_with has some different syntax than the older rails form_tag
Make sure you don't have any typos or name problems when creating your form blocks because these are nested twice
Same with nesting parameters in your controllers, just be aware of your syntax and typos
Be aware that Ryan Bates is demoing with data that wasn't added through the form he's building so if you'd like to follow along you'll need to create some data in the console
With strong parameters you'll have to explicitly list :_destroy as a parameter in order for his "Remove" checkboxes to work
Related
I'm struggling with understanding this.
I have two models Person and Address. Person accepts nested attributes for Address.
class Person < ApplicationRecord
has_one :address, dependent: :destroy
accepts_nested_attributes_for :address
I have a form app/views/person/new.html.erb:
<%= bootstrap_form_for(#person) do |f| %>
#...
<%= f.fields_for :address do |ff| %>
<%= ff.text_field :apartment_number %>
<%= ff.text_field :building_name %>
<%= ff.text_field :building_number %>
<%= ff.text_field :street %>
<%= ff.text_field :town %>
<%= ff.text_field :postcode, required: true %>
<% end %>
#...
I have set up my strong params and I create the new record as follows:
def create
#person = Person.new(safe_params)
if #person.save
binding.pry
else
render 'new'
end
end
private
def safe_params
params.require(:person).permit(
:name,
:date_of_birth,
address: %i[
apartment_number
building_name
building_number
street
town
postcode
]
)
end
However, there's two things going on here. If #person fails validation, the re-rendered new view does not contain the submitted address data. Secondly, if the save is successful I can see that the associated address is not created:
#person.address
=> nil
What do I need to do to ensure that an address is created and saved? Have I misunderstood something obvious?
As per the docs on nested attributes the strong parameters have to end with _attributes
params.require(:person).permit(
:name,
:date_of_birth,
address_attributes: %i[ apartment_number ]
)
I'm trying to come up with a contact form that creates a contact record and potentially multiple location records, if multiple locations are checked in a list of checkboxes. I thought of having all location records created and then destroyed, if they aren't checked. I don't think that's optimal though.
I'm using many to many relationships in the models.
This is what they look like at the moment:
contact.rb
class Contact < ApplicationRecord
has_many :contact_locations, dependent: :destroy
has_many :locations, through: :contact_locations
accepts_nested_attributes_for :contact_locations, allow_destroy: true, reject_if: :empty_location?
private
def empty_location?(att)
att['location_id'].blank?
end
end
location.rb
class Location < ApplicationRecord
has_many :locations, dependent: :destroy
has_many :contacts, :through => :contact_locations
has_many :contact_locations
end
contact_location.rb
class ContactLocation < ApplicationRecord
belongs_to :location
belongs_to :contact
end
contacts_controller.rb
def new
#contact = Contact.new
#locations = Location.all
4.times {#contact.contact_locations.new}
end
private
def contact_params
params.require(:contact).permit(:name, :phone, ..., contact_locations_attributes: [:location_ids])
end
new.html.rb
<%= form_with model: #contact do |f| %>
...
<%= #locations.each do |location| %>
<%= f.fields_for :contact_locations do |l| %>
<%= l.check_box :location_id, {}, location.id, nil %><%= l.label location.name %>
<% end %>
<% end %>
...
<% end %>
Does anyone how to make it work properly?
I'm working on Ruby 2.5.1 and Rails 5.2.1.
Thanks a lot.
I think your solution is the form objects pattern.
You can have something like this:
<%= form_for #user do |f| %>
<%= f.email_field :email %>
<%= f.fields_for #user.build_location do |g| %>
<%= g.text_field :country %>
<% end %>
<% end%>
And convert it in something more readable that permits you to instance the locations inside the registration object, checking the value of the checkboxes.
<%= form_for #registration do |f| %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.input :password %>
<%= f.text_field :password %>
<%= f.input :country %>
<%= f.text_field :country %>
<%= f.input :city %>
<%= f.text_field :city %>
<%= f.button :submit, 'Create account' %>
<% end %>
Here you will find how to apply the pattern: https://revs.runtime-revolution.com/saving-multiple-models-with-form-objects-and-transactions-2c26f37f7b9a
I ended up making it work with Kirti's suggestion on the following question:
Rails Nested attributes with check_box loop
It turns out I needed to make a small adjustment in my form's fields_for tag.
Thanks a lot the help!
In my models I have
class Blog < ActiveRecord::Base
has_many :tags, :dependent => :destroy
accepts_nested_attributes_for :tags, :allow_destroy => true
end
class Tag < ActiveRecord::Base
belongs_to :blog
validates :blog, :name, presence: true
end
Blog Controller
def new
#blog = Blog.new
#blog.tags.build
end
_form.html.erb
<%= form_for #blog, html: { multipart: true } do |f| %>
<div class="form-group">
<%= f.text_field :title, placeholder: 'Title', class: ('form-control') %>
</div><br>
<%= f.fields_for :tags do |builder| %>
<div class="form-group">
<%= builder.text_field :name, placeholder: 'Tags' %>
</div><br>
<% end %>
<div class="actions text-center">
<%= f.submit 'Submit', class: 'btn btn-primary' %>
</div>
<% end %>
Blog Controller
def create
#blog = Blog.new(blog_params)
binding.pry
end
def blog_params
params.require(:blog).permit(:title, :author, :text, :avatar, :banner, :tags_attributes => [:id, :name])
end
At my binding, it says #blog's error message is that it can't be saved because the Tag object is missing a blog_id. I have looked everywhere and I have tried to replicate my code to match other solutions but to no success.
If it helps, in my params when I submit the form I get this
"tags_attributes"=>{"0"=>{"name"=>"dsfsf"}}
that's because your #blog is not persisted in the db yet, so you won't have the id.
In your Tag model, remove :id from validation.
You should be able to just do Blog.create(blog_params)
Rails should handle the rest for you.
I am creating a nested form with attributes from different models. I expect all the required attributes to be valid, before a new object is saved.
<%= form for #product do |f| %>
<%= f.fields_for #customer do |g| %>
<%= g.label :name %>
<%= g.text_field :name %>
<%= g.label :email %>
<%= g.text_field :email %>
<%= g.label :city %>
<%= g.text_field :city %>
<%= g.label :state %>
<%= g.text_field :state %>
<%= g.label :zipcode %>
<%= g.text_field :zipcode %>
<% end %>
<%= f.label :product %>
<%= f.text_field :product %>
<%= f.label :quantity %>
<%= number_field(:quantity, in 1..10) %>
<% end %>
Here are my models
class Product < ActiveRecord::Base
belongs_to :customer
validates_associated :customer
validates :product, :presence => "true"
end
class Customer < ActiveRecord::Base
has_one :product
validates :name, :email, presence: true
validates :email, format: { with: /[A-Za-z\d+][#][A-Za-z\d+][.][A-Za-z]{2,20}\z/ }
validates :city, presence: true
validates :zipcode, format: { with: /\A\d{5}\z/ }
end
I added validates_associated to my Product Model, so my form_for #product should require all the customer validations to pass. That means name, email, city and zipcode have to be there and have to be formatted properly.
I fiddled around, and submitted the form without filling in the Customer required fields, and the form was considered valid.
I don't understand where my mistake is.
EDIT
Alright, so by adding validates :customer, the customer attributes are now required. But they aren't actually saved to the database. I think this has to do with my params
def product_params
params.require(:product).permit(:product, :quantity)
end
Do I need to add my Customer Params to my permitted params list?
The validates_associated method only validates the associated object if the object exists, so if you leave the form fields blank, the Product you are creating/editing will validate, because there is no associated Customer.
Instead, assuming you're using Rails 4+, you want to use accepts_nested_attributes_for :customer, along with validates :customer, presence: true in order to required the customer fields in your product form.
If you're using Rails 3, then accepts_nested_attributes_for will not work for a belongs_to association. Instead, your Customer class will need to use accepts_nested_attributes_for :product, and you will need to alter your form view accordingly.
UPDATE
You also need to allow your controller action to accept parameters for the :customer association:
def product_params
params.require(:product).permit(:product, :quantity, :customer_attributes => [:name, :email, :city, :state, :zipcode])
end
It's worth noting that because there is no :id field in your customer form fields, and no :customer_id field in your product form fields, you will create a new customer every time you successfully submit the product form.
try this out:
In Controller create an instance of a product and associated customer as follows:
#product = Product.new
#customer = #product.build_customer
in use this code for form
<%= form for #product do |f| %>
<%= f.fields_for :customer do |g| %>
<%= g.label :name %>
<%= g.text_field :name %>
<%= g.label :email %>
<%= g.text_field :email %>
<%= g.label :city %>
<%= g.text_field :city %>
<%= g.label :state %>
<%= g.text_field :state %>
<%= g.label :zipcode %>
<%= g.text_field :zipcode %>
<% end %>
<%= f.label :product %>
<%= f.text_field :product %>
<%= f.label :quantity %>
<%= number_field(:quantity, in 1..10) %>
<% end %>
i.e use :customer symbol instead of #customer instance variable.
and use accepts_nested_attributes_for helper method in Product model as #Charles said
Complementing the other answers, I control what I receive in the controller, avoiding further action and noticing if a value is not the one I want.
def update
if params[:customer][:product_attributes]["0"][:name] == ""
redirect_to customer_path(#incident), alert: 'You need to add a name'
else
respond_to do |format|
if #customer.update(customer_params)
format.html { redirect_to customer_path(#customer), notice: 'Succesfully updated' }
format.json { render :show, status: :ok, location: #customer }
else
format.html { render :edit }
format.json { render json: #customer.errors, status: :unprocessable_entity }
end
end
end
end
The nested form in the view just won't render, unless I remove the f attribute, in which case the submit button will not work. I have two models, job and employer. I've been following the railscast here
job.rb
attr_accessible :title, :location, :employers_attributes,
belongs_to :employers
accepts_nested_attributes_for :employers
employer.rb
attr_accessible :companyname, :url
has_many :jobs
jobs_controller.rb
def new
#job = Job.new
#employer = Employer.new
end
_form.html
<%= form_for(#job) do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :location %>
<%= f.text_field :location %>
<%= f.fields_for :employers do |builder| %>
<%= builder.label :companyname, "Company Name" %>
<%= builder.text_field :companyname %>
<%= builder.label :url, "Web Address" %>
<%= builder.text_field :url %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Any input would be brilliant - thanks
This happens because your job has no employers.
Change your code to this:
def new
#job = Job.new
#job.employer = #job.build_employer
end
In your job.rb change:
attr_accessible :title, :location, :employer_attributes,
belongs_to :employer
accepts_nested_attributes_for :employer
This line:
belongs_to :employers
Should be singulars:
belongs_to :employer
With this association you not need nested form you can use select for pick employer for each job.
But if you need many employers for each job and each job can have many employers see this screencast