Pre-populated association for nested form rendered empty - ruby-on-rails

My PlanList has_many :items.
I want my plan_list#new to have a nested form, where I can pre-populate items.
I tried
# View
<%= form_for #plan_list do |f| %>
<%= f.fields_for :items do |item| %>
<%= item.text_field :quantity %>
<% end %>
with
# Controller
def new
#plan_list = PlanList.new
#plan_list.items.build(quantity:1)
#plan_list.items.build(quantity:2)
end
However I only see empty inputs for items.
I also tried <%= f.fields_for #plan_list.items do |item| %> but it will only show one item (the last one with quantity 2). How can I achieve my goal?

You should use the build method of associations as:
def new
#plan_list = PlanList.new
#plan_list.items << #plan_list.items.build(quantity:1)
#plan_list.items << #plan_list.items.build(quantity:2)
end

I did not have accepts_nested_attributes_for :items.
I thought I could add this later when I implement the create method, but it is essential for both form creation and attribute mass assignment

Yep, this is must and should resolve the issue accepts_nested_attributes_for :items
<%= form_for #plan_list do |f| %>
<%= f.fields_for :items do |item| %>
<%= item.text_field :quantity %>
<% end %>
def new
#plan_list = PlanList.new
#plan_list.items.build(quantity:1)
#plan_list.items.build(quantity:2)
end

Related

How do you persist an instantiated nested form object when creation fails?

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

Rails/ActiveRecord - association not saving

I can't get my CheckIn record to save because the associated Tenancy isn't saving.
I have three models with associations:
class Property < ApplicationRecord
has_many :tenancies
end
class Tenancy < ApplicationRecord
belongs_to :property
has_many :check_ins
end
class CheckIn < ApplicationRecord
belongs_to :tenancy
accepts_nested_attributes_for :tenancy
end
I want the CheckIn new action to create both the CheckIn and the associated Tenancy:
def new
#check_in = CheckIn.new
#check_in.build_tenancy.property_id = params[:property_id]
end
I have to include the property_id part otherwise the Tenancy won't save.
The form in check_ins/new.html.erb:
<%= form_for #check_in, url: property_check_ins_path do |f| %>
<%= f.label :date_time %>
<%= f.datetime_select :date_time, {minute_step: 15} %>
<%= f.label :tenancy %>
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<% end %>
<%= f.submit "Create Check In" %>
<% end %>
I've added tenancy attributes to the strong params in the CheckInsController:
def check_in_params
params.require(:check_in).permit(:tenancy_id, :date_time, tenancy_attributes: [:start_date])
end
It's worth noting that the check_ins routes are nested in properties:
resources :properties do
resources :check_ins, only: [:new, :create]
end
So the problem is that by the time I get to the create action in the CheckInsController, the tenancy that I built has disappeared. I'm not sure how and when each of the records should be being saved and the slight complexity of what I'm trying to achieve has made it quite difficult to find relevant help so any ideas?
I'm using Rails 5.
The problem was that the property attached to the tenancy was being forgotten. I removed the property attachment from the new action:
def new
#check_in = CheckIn.new
#check_in.build_tenancy
end
Added a hidden field for property_id to the form (as well as adding :property_id to the strong params):
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<%= i.hidden_field :property_id, value: params[:property_id] %>
<% end %>
And saved the tenancy in the CheckIn create action, prior to saving the check in itself:
def create
#check_in = CheckIn.new(check_in_params)
#check_in.tenancy.save
if #check_in.save
redirect_to property_check_in_path(#check_in.tenancy.property.id, #check_in)
else
render :new
end
end
I'd certainly be interested if anyone could pick holes in this solution or offer a better one.
Using nested resources (check_ins depends from properties) you create a namespaces routes. form_for helper ( rails guides - form helpers ) when you build your form, need a Property reference also.
I try to explain me better with an example:
#checks_controller.rb
def new
#property = Property.new
#check_in = #property.build_check_ins
#check_in.build_tenancy
end
#check_ins/new.html.erb
<%= form_for [#property, #check_in], url: property_check_ins_path do |f| %>
<%= f.label :date_time %>
<%= f.datetime_select :date_time, {minute_step: 15} %>
<%= f.label :tenancy %>
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<% end %>
<%= f.submit "Create Check In" %>
<% end %>
I haven't tried this code, but I hope this give you at least a way to follow to solve your problem.

Rails Many_To_Many Selection in form

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.

Nested model form with mutliple has_many/belongs_to associations

I have three models:
class Rate < ActiveRecord::Base
attr_accessible :user_id, :car_id, :rate
belongs_to :user
belongs_to :car
end
class User < ActiveRecord::Base
attr_accessible :name
has_many :rates
accepts_nested_attributes_for :rates
end
class Car < ActiveRecord::Base
attr_accessible :name
has_many :rates
accepts_nested_attributes_for :rates
end
And one controller:
class UsersController < ResourceController
def new
# Assume user is loaded
#user.rates.build
end
end
I'm trying to build a nested form that will associate a list of users/cars and their associated rates.
Something like:
<% form_for #user do |f| %>
<%= #user.name %><br />
<% Car.all.each do |car| %>
<%= car.name %><br />
<%= f.fields_for :rates do |r| %>
<%= r.number_field :rate %>
<% end %>
<% end %>
<% end %>
The problem is that I would like the Rate model to store data as follows:
USER_ID CAR_ID RATE
1 1 10
1 2 20
1 3 30
2 1 40
3 2 50
I cannot figure out how to properly build the fields_for helper to build the proper params for both the user_id and the car_id.
Something like:
user[car=1][rate]
user[car=2][rate]
I've tried being more explicit with the fields_for like this:
<%= r.fields_for 'user[car][rate]' %>
But it still doesn't build out the nested parameters properly. The car parameter is not correctly identified.
Any help would be appreciated! Thanks.
EDIT:
The controller action has to be under user. The example above has been shortened for brevity but other user-related attributes are available through the form so it has to use the users controller.
ANSWER:
I figured out a way to do it. I've added my own answer that explains it.
<% form_for #user do |f| %>
<%= #user.name %><br />
<%= f.fields_for :rates do |r| %>
<% Car.all.each do |car| %>
<%= car.name %><br />
<%= r.number_field :rate %>
<% end %>
<% end %>
<% end %>
This may be solution of your problem. Just check it.
The form is going to create a new rate instead of a new user, so the method should be in RatesController instead of UsersController.
With this logic the problem seems solved. You can write field_for rate[user] and field_for rate[car]
I think I've got it figured out.
In my controller, I've modified the build method as follows:
Car.all.each { |c| #user.rates.build(car_id: c.id) } if #user.rates.count == 0
Then, in my model, I need the following:
attr_accessible :rates_attributes
Finally, the fields_for block should look like this (remember, this is in the #user form object f):
<%= f.fields_for :rates do |r| %>
<%= r.hidden_field :car_id %>
<%= r.object.car.name %><br />
<%= r.number_field :rate %>
<% end %>
This builds the params hash properly and create the rate model entries when the form is submitted.
The check on existing user rates in the controller will ensure that the existing values are used in the form and new ones are not built (which I thought build took into consideration... ?).

Ruby on rails: fields_for do nothing if defined submodel_attributes=

I have such code in new.erb.html:
<% form_for(#ratification) do |f| %>
<%= f.error_messages %>
<% f.fields_for :user do |fhr| %>
<p>
<%= fhr.label :url %><br />
<%= fhr.text_field_with_auto_complete :url %>
</p>
<% end %>
<% end %>
If i have empty Ratification.rb it is ok, fields_for works ok.
But if I wrote:
class Ratification < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
or
class Ratification < ActiveRecord::Base
belongs_to :user
def user_attributes=(attr)
...
end
end
f.fields_for yields nothing! Why!?
Rails: 2.3.8
Plugin for autocomplete: repeated_auto_complete
just add an = (EQUAL) after <% in the f.fields_for
like this:
<%= f.fields_for :user do |fhr| %>
<p>
<%= fhr.label :url %><br />
<%= fhr.text_field_with_auto_complete :url %>
</p>
<% end %>
obs: you have to do the same in Rails 3
I believe you need to build a user in your controller, like
# controller
def new
#ratification = Ratification.new
#ratification.build_user
end
What about
<% f.fields_for :user, #ratification.user do |fhr| %>
# ...
<% end %>
?
I believe if you use
<% f.fields_for :user do |fhr| %>
you should have #user as instance variable. But in your case you have #ratification.user.
You cannot redefine user_attributes since you will overwrite the standar behaviour that ActiveRecord specifies for it. If still knowing this you need to redefine user_attributes try using alias_method_chain.
Aren't you doing this wrong? If Ratification belongs to a user, then the user model should accept nested attributes for ratifications, not the other way around.
So if users have many ratifications, and if you want to submit multiple ratifications for a user in a single form, then you would use accept nested attributes for ratification in the user model.
And you would do somewhere in the users controller
#user = User.new
2.times { #user.ratifications.build } # if you want to insert 2 at a time
I tried to do something similar in the console:
#user = User.new
#user.ratifications.build # this works
But if I did
#ratification = Ratification.new
#ratification.user.build # this fails
I just encountered the same problem and was able to fix it. The problem was that fields_for takes :user as an argument while the argument should be #ratification.user
Hence, replace
<% f.fields_for :user do |fhr| %>
with
<% f.fields_for #ratification.user do |fhr| %>
That's it.

Resources