I have a simple bids model that embeds a supplier:
class Bid
include Mongoid::Document
field :amount, type: Integer
embeds_one :supplier
accepts_nested_attributes_for :supplier
end
Many bids have the same supplier. If I was using a scaffolded edit view, how would I update all of the instances of the embedded supplier in bids in the update method?
I have tried, unsuccessfully, something like
def update
#supplier.update(supplier_params)
#or
#bids = Bid.where('supplier._id' => #supplier.id)
#bids.supplier.update_attributes!(supplier_params)
redirect_to #supplier
end
Since you are using nested attributes you should be able to update the nested record by passing supplier_attributes.
Bid.find(1).update(supplier_attributes: { foo: 'bar' } )
To whitelist nested params you use a hash option:
params.require(:bid).permit(supplier_attributes: [:foo])
To generate the form fields you use fields_for:
<%= form_for(#bid) do |f| %>
<%= f.fields_for(:supplier) do |s| %>
<%= s.label :foo do %>
<%= s.text_field :foo %>
<% end %>
<% end %>
<% end %>
Related
I have models Software and Version. A Software has_many Version's and has_one :first_version
class Software < ApplicationRecord
has_many :versions
has_one :first_version, -> { order(created_at: :asc) },
class_name: "Version", dependent: :destroy
accepts_nested_attributes_for :versions
end
class Version < ApplicationRecord
belongs_to :software
end
I'm building the nested object in the new controller action.
class SoftwaresController < ApplicationController
def new
#software = current_account.softwares.build
#software.build_first_version
end
def create
#software = current_account.softwares.build(software_params)
if #software.save
redirect_to software_path(#software)
else
render :new
end
end
def software_params
params.require(:software).permit(
:name,
first_version_attributes: %i[id release_date],
)
end
end
form:
<%= simple_form_for :software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for :first_version do |v|%>
<%= v.input :release_date %>
<% end %>
<% end %>
With the above code, if something fails during creation, the nested object is persisted even though the object itself and it's parent do not have an id yet, and so errors are displayed under each field with invalid values.
At the same time, if I comment out the line where I build the nested object, the form does not break, just no nested fields are displayed. This is good.
Now, because the form is reused in the new and edit views and I don't want to let users edit the :first_version through this form nor rely on the view to render it conditionally if #software.new_record? I put the nested object in a global variable and point the nested form to that variable hoping that the same result will be achieved in the edit view because no global variable will exist.
def new
#software = current_account.softwares.build
#first_version = #software.build_first_version
end
form:
<%= simple_form_for :software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for #first_version do |v|%>
<%= v.input :release_date %>
<% end %>
<% end %>
Problem:
If something goes wrong during creation the object is no longer persisted and the view breaks due to #first_version being nil. So why is the nested object persisted when I do #parent.build_nested_object but not when #nested_object = #parent.build_nested_object ?
Solving the problem by creating more i_vars can lead to bugs. I think the best option is to disable the field based on a condition and change your view to the following.
<%= simple_form_for #software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for #software.first_version || #software.build_first_version do |v| %>
<%= v.input :release_date, disabled: (true if #software.first_version.id) %>
<% end %>
<% end %>
Using this view means that you can initialize only #software on your controller.
class SoftwaresController < ApplicationController
def new
#software = current_account.softwares.build
end
end
An article called Triple nested Forms in Rails presents a good description of creating a form for saving three nested objects. The example given is of creating a Show that has_many Seasons, and each Season has_many Episodes. Also, Episode --> belongs_to --> Season --> belongs_to --> Show.
Shows are created like this:
def new
#show = Show.new
#show.seasons.build.episodes.build
end
The form looks like this:
<%= form.fields_for :seasons do |s| %>
<%= s.label :number %>
<%= s.number_field :number %> <%= s.fields_for :episodes do |e| %>
<%= e.label :title %>
<%= e.text_field :title %>
<% end %>
<% end %>
<% end %>
This seems straightforward because all the associations run in one direction. I'm trying to do something that's similar, but more complicated. I have a Parent model where each Parent has multiple Children and each Child is enrolled in a School. After specifying that Children is the plural of Child, the association would have to be like this:
Parent has_many Children, accepts_nested_attributes_for :children
Child belongs_to Parent, belongs_to School, accepts_nested_attributes_for :school
School has_many Children, accepts_nested_attributes_for :children
Graphically, it would look like this:
Parent <-- belongs_to <-- Child --> belongs_to --> School
Each Parent is also associated with a User, like this:
User has_many :parents
The data on Parents, Children, and Schools is entered in the following form (generated using the Simple Form gem), where the schools are entered as a dropdown selector populated from the schools table:
#schools = School.all
<%= simple_form_for (#parent) do |f| %>
<%= f.input :name, label: 'name' %>
<%= f.simple_fields_for :children, #children do |child_form| %>
<%= child_form.input :name, label: "Child Name" %>
<%= child_form.simple_fields_for :school, #school do |school %>
<%= school.collection_select :id, #schools, :id, :name, {}, {} %>
<% end %>
<% end %>
<% end %>
I set up the new controller method to create a Parent having three Children enrolled in an existing School. Then I tried to associate the Children with a School that already exists in the schools table with id = 1.
def new
#parent = Parent.new
# creating 3 children
#children = Array.new(3) {#parent.children.build}
#school = School.find(1)
#school.children.build
end
This throws an error
Couldn't find School with ID=1 for Child with ID=
The error is located in the first line of the create method, which looks like this:
def create
#parent = Parent.new(parent_params.merge(:user => current_user))
if #parent.save
redirect_to root_path
else
render :new, :status => :unprocessable_entity
end
end
def parent_params
params.require(:parent).permit(:name, :child_attributes => [:id, :name, age, :school_attributes => [:id, :name]])
end
Since the error text states "Child with ID= ", the error must be thrown before ids for new Children are assigned. Why can a School with ID=1 not be found when it exists in the schools table? Or, does this mean that a School record has not been properly associated with an instance of Child before an attempt is made to save that instance? If so, how can I fix the association?
One of the most common missconceptions/misstakes with nested attributes is to think that you need it to do simple association assignment. You don't. You just need to pass an id to the assocation_name_id= setter.
If fact using nested attributes won't even do what you want at all. It won't create an assocation from an existing record when you do child.school_attributes = [{ id: 1 }] rather it will attempt to create a new record or update an existing school record.
You would only need to accept nested attributes for school if the user is creating a school at the same time. And in that case its probally a better idea to use Ajax rather than stuffing everything into one mega action.
<%= simple_form_for (#parent) do |f| %>
<%= f.input :name, label: 'name' %>
<%= f.simple_fields_for :children, #children do |child_form| %>
<%= child_form.input :name, label: "Child Name" %>
<%= child_form.associaton :school,
collection: #schools, label_method: :name %>
<% end %>
<% end %>
def parent_params
params.require(:parent).permit( :name,
child_attributes: [:id, :name, :age, :school_id]]
)
end
I created a many to many relationship between work packages and tasks:
class WorkPackage < ActiveRecord::Base
has_and_belongs_to_many :tasks
end
class Task < ActiveRecord::Base
has_and_belongs_to_many :work_packages
end
def change
create_table :tasks_work_packages, id: false do |t|
t.belongs_to :work_package, index: true
t.belongs_to :task, index: true
end
end
And if I assign tasks to workpackages it works. But now I want the user to add tasks to a workpackage, what do I have to add to the controller and especially the form to achieve that?
My current solution doesn't work:
work_package_controller:
def work_package_params
params.require(:work_package).permit(:name, :price, tasks_attributes: [:id, :work_package_id, :task_id])
end
work_packages_form (3 different options so far):
<% 3.times do %>
<%= f.fields_for #work_package.tasks.build do |task_fields| %>
<%= task_fields.collection_select(:id, Task.all, :id, :name, {:include_blank => true }) %>
<% end %>
<% end %>
<% #work_package.tasks.each do |task| %>
<%= f.fields_for :tasks, task do |task_fields| %>
<%= task_fields.collection_select(:id, Task.all, :id, :name, {:include_blank => true }) %>
<% end %>
<% end %>
<%= select_tag("work_package[task_ids][]", options_for_select(Task.all.collect { |task| [task.name, task.id] }, #work_package.tasks.collect { |task| task.id}), {:multiple=>true, :size=>5}) %>
What am I missing?
Selecting associated records
If you want a user to be able to choose existing tasks you would use collection_select or collection_checkboxes.
Note that this has nothing to do with nested attributes! Don't confuse the two.
<%= form_for(#work_package) do |f| %>
<div>
<%= f.label :task_ids, "Tasks" %>
<%= f.collection_check_boxes :task_ids, Task.all, :id, :name %>
</div>
<% end %>
This creates a task_ids param which contains an array of ids.
When you use has_many or has_and_belongs_to_many ActiveRecord creates has a special relation_name_ids attribute. In this case task_ids. When you set task_ids and call .save on the model AR will add or remove rows from the tasks_work_packages table accordingly.
To whitelist the task_ids param use:
require(:work_package).permit(task_ids: [])
Nested attributes
You would use nested attributes if you want users to be able to create or modify a work package and the related tasks at the same time.
What fields_for does is create scoped inputs for a model relation. Which means that it loops through the associated records and creates inputs for each:
<% form_for(#work_package) do |f| %>
<%= f.fields_for(:tasks) do |f| %>
<div class="field">
<% f.string :name %>
</div>
<% end %>
<% end %>
fields_for will give us an array of hashes in params[:tasks_attributes].
One big gotcha here is that no fields for tasks will be shown for a new record and that you can't add new tasks from the edit action.
To solve this you need to seed the work package with tasks:
class WorkPackagesController < ApplicationController
def new
#work_package = WorkPackage.new
seed_tasks
end
def edit
seed_tasks
end
def create
# ...
if #work_package.save
# ...
else
seed_tasks
render :new
end
end
def update
# ...
if #work_package.save
# ...
else
seed_tasks
render :edit
end
end
private
# ...
def seed_tasks
3.times { #work_package.tasks.new }
end
end
To whitelist the nested attributes you would do:
params.require(:work_package).permit(tasks_attributes: [:name])
Conclusion
While these are very different tools that do separate things they are not exclusive. You would combine collection_checkboxes and fields_for/nested_attributes to create a form that allows the user to both select and create new tasks on the fly for example.
I have a has_many through join table setup for a recipe app where Ingredient and Meal connect through MealIngredient. Within MealIngredient, I have meal_id, ingredient_id, and amount.
My question is: How can I save and update the amount column in the meal form?
My form field for adding an ingredient looks like this:
<% Ingredient.all.each do |ingredient| %>
<label>
<%= check_box_tag "meal[ingredient_ids][]", ingredient.id, f.object.ingredients.include?(ingredient) %>
<%= ingredient.name %>
</label>
<br />
<% end %>
How do I save the amount for each ingredient?
I am referencing this question found here: Rails 4 Accessing Join Table Attributes
I made a demo for you: http://meals-test2.herokuapp.com/new
--
If you're using a form, you need to use fields_for and edit it that way:
#app/controllers/meals_controller.rb
class MealsController < ApplicationController
def edit
#meal = Meal.find params[:id]
end
private
def meal_params
params.require(:meal).permit(meal_ingredient_attributes: [:amount])
end
end
#app/views/meals/edit.html.erb
<%= form_for #meal do |f| %>
<%= fields_for :meal_ingredients do |i| %>
<%= f.object.ingredient.name #-> meal_ingredient belongs_to ingredient %>
<%= i.number_field :amount %>
<% end %>
<%= f.submit %>
<% end %>
The above will output a list of ingredients for the meal and allow you to input the "amount" value.
As for checkboxes, I'd have to make a demo app to see if I can get that working. I can do this if you feel it necessary.
Another way is with has_and_belongs_to_many:
#app/models/meal.rb
class Meal < ActiveRecord::Base
has_and_belongs_to_many :ingredients do
def amount #-> #meal.ingredients.first.amount
...........
end
end
end
#app/models/ingredient.rb
class Ingredient < ActiveRecord::Base
has_and_belongs_to_many :meals
end
This way, you'll be able to add as many meals / ingredients as required, allowing you to find the "amount" with #meal.ingredients.where(ingredients: {id: "x" }).size. You could also make a method to simplify it (above).
You wouldn't need to use fields_for for this:
#app/controllers/meals_controller.rb
class MealsController < ApplicationController
def new
#meal = Meal.new
end
def edit
#meal = Meal.find params[:id]
end
def update
#meal = Meal.find params[:id]
#meal.save
end
def create
#meal = Meal.new meal_params
#meal.save
end
private
def meal_params
params.require(:meal).permit(ingredient_ids: [])
end
end
Because the HABTM record uses the has_many association in your model, it provides you with the collection_singular_ids method. This allows you to override the associated data without fields_for:
#app/views/meals/new.html.erb
<%= form_for #meal do |f| %>
<%= f.collection_check_boxes :ingredient_ids, Ingredient.all, :id, :name %>
<%= f.submit %>
<% end %>
If you wanted to add extra ingredients, you'd need to create JS to duplicate the checkbox element. This will then allow you to submit multiple ids to the controller, which will just insert them blindly into the db.
This method overrides the ingredients list, and only works if you don't have any uniqueness constraints on the habtm association / table.
If I have the models with the following associations:
class Business
has_many :products
class Product
belongs_to :business
And I generate 3 products in the controller:
def new
#business = Business.new
3.times do
#business.products.build
end
end
Making my form look like this:
<%= form_for #business do |f| %>
<% f.text_field :business_name %>
<%= f.fields_for :products do |pf| %> # x2 more products generated
<% pf.text_field :name %>
<% pf.text_field :price %>
<% pf.text_field :date %>
<% end %>
If I want one of the fields to act as a global field for the rest of the products how could I take a field like the :price and put it outside of the f.fields_for :products to have it be the :price for all of the products?
Thank you.
If you need to initialize the price, do it in the controller. But if you need a field that doesn't map to a model directly, use the regular form helpers:
<%= text_field_tag 'global_price' %>
and then in the controller on the create action, it is available as
params[:global_price]
Alternately, you could define a method in your Business model:
def global_price=
#do something with the global price, such as updating child object...
# I'm not sure if the child form objects have been instantiated yet though
end
and then you can use it in your business form:
<%= f.text_field :global_price %>
If you need to update the child objects, you might have to do that at a later time; instead of that method, make it
attr_accessor :global_price
Which makes it an instance variable. Then you can use a before_save filter to update the child objects.
before_save :update_global_price
def update_global_price
#do something with #global_price
end