I am using the following code to create a drop down in a view to associate a model with another one. This works but I have to prepopulate the tasks in the database. I'd like to be able to create tasks during the creation of a project instead of just selecting ones that are already there.
<%= f.label :task %>
<%= f.collection_select( :task_id,
Task.all,
:id, :name, {selected: #project.task_id, include_blank: false}) %>
From what I understand I'm going to have to do this in the controller as well, but a lot of the code I've seen is out of date and I want to do it the Rails 4 way. Thanks!
If you're creating a new project & trying to create tasks too, you'll need to use a nested model form (accepts_nested_attributes_for):
#app/models/Project.rb
def Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks
end
#app/controllers/projects_controller.rb
def new
#project = Project.new
#project.tasks.build #-> do this for as many tasks as you want
end
def create
#project = Project.new(project_params)
#project.save
end
private
def project_params
params.require(:project).permit(:new, :project, :attrs, tasks_attributes: [:task_name])
end
#app/views/projects/new.html.erb
<%= form_for #project do |f| %>
<%= f.text_field :name %>
<%= f.fields_for :tasks do |t| %>
<%= t.text_field :name %>
<% 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
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 three models. The two I am having trouble with (recipe and ingredient) each have a has_and_belongs_to_many relationship with the other. The form seems to be getting all the information I ask for, but I can't seem to get the name attribute of the ingredient into my permitted params.
Form:
<%= form_for(#recipe, :url => create_path) do |f| %>
<%= f.label :category %>
<%= f.select :category_id, options_for_select(Category.all.map{|c|[c.title, c.id]}) %>
<%= f.label :title %>
<%= f.text_field :title%>
<%= f.label :instruction %>
<%= f.text_area(:instruction, size: "50x10") %>
<%= f.fields_for :indgredient do |i| %>
<%= i.label :name %>
<%= i.text_field :name %>
<% end %>
<%= f.submit "Submit" %>
Relevant action in Recipes Controller:
def create
safe_params = params.require(:recipe).permit(:title, :instruction,
:category_id, {ingredient: :name})
#recipe = Recipe.new(safe_params)
#recipe.save
#recipe.ingredients.create(name: safe_params[:name])
render body: YAML::dump(safe_params)
end
What the YAML dump gives me:
--- !ruby/hash:ActionController::Parameters
title: foo
instruction: bar
category_id: '1'
Code for models:
class Category < ActiveRecord::Base
has_many :recipes
end
class Recipe < ActiveRecord::Base
has_and_belongs_to_many :ingredients
accepts_nested_attributes_for :ingredients
belongs_to :category
end
class Ingredient < ActiveRecord::Base
has_and_belongs_to_many :recipes
end
the create method does create a new ingredient, but the name is nil. Thanks in advance for the help.
Did you add accepts_nested_attributes_for :ingredients in the model of Recipe ?
Moreover there is a gem to handle nested forms called cocoon.
You can read this article which is explaining exactly what you are trying to do.
https://hackhands.com/building-has_many-model-relationship-form-cocoon/
Firstly, change <%= f.fields_for :indgredient do |i| %> to <%= f.fields_for :ingredients do |i| %>.
And change the new and create actions like below
def new
#recipe = Recipe.new
#recipe.ingredients.build
end
def create
#recipe = Recipe.new(safe_params)
if #recipe.save
redirect_to #recipe
else
render 'new'
end
end
private
def safe_params
params.require(:recipe).permit(:title, :instruction, :category_id, ingredients_attributes: [:name])
end
To add to #Pavan's answer, you have to remember that Ruby is building objects (it's an object orientated language), and as such, whenever you pass associated data, you have to refer to the objects Ruby has in memory.
In your case, you're trying to create new Ingredient objects through Recipe:
#app/models/recipe.rb
class Recipe < ActiveRecord::Base
has_and_belongs_to_many :ingredients
accepts_nested_attributes_for :ingredients
end
... thus, you need to reference ingredients:
<%= f.fields_for :ingredients do ... %>
--
You also want to make sure you're only processing the Recipe object in your create action:
def create
#recipe = Recipe.new safe_params
#recipe.save
end
private
def safe_params
params.require(:recipe).permit(:title, :instruction, :category_id, ingredients_attributes: [:name] )
end
I'm just trying to generate a simple nested form, like so:
<%= simple_form_for #profile do |f| %>
<%= f.input :first_name %>
<%= f.input :last_name %>
<%= f.input :phone_number %>
<%= f.simple_fields_for :addresses do |p| %>
<%= p.input :street %>
<%= p.input :city %>
<%= p.input :state, collection: us_states %>
<%= p.input :zip_code %>
<% end %>
<%= f.button :submit %>
My models:
class Profile < ActiveRecord::Base
belongs_to :customer
has_many :addresses
accepts_nested_attributes_for :addresses
end
class Address < ActiveRecord::Base
belongs_to :profile
end
My controller:
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = Profile.new
end
end
Unfortunately that nested attribute addresses doesn't populate anything on the page, I would expect to see fields like street or city but I get nothing.
However, if I change <%= f.simple_fields_for :addresses do |p| %> to <%= f.simple_fields_for :address do |p| %> the fields display correctly.
Unfortunately doing this causes issues because I can't use the accepts_nested_attributes_for helper as outlined in the docs (as far as I can tell). Any idea why this isn't working?
The reason is because nested forms require created objects to work. It looks like Profile gets instantiated but Address does not.
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = Profile.new
#profile.addresses.create # this will create the address object that the nested form will use
end
end
I think you will need to create Profile as well rather than create an instance of it.
#profile = Profile.create
I've just been working with nested forms myself and this is how it worked for me.
The solution was to build the profile and the addresses in the #new action for it to work. Revised working code:
class ProfilesController < ApplicationController
before_action :authenticate_customer!
def new
#profile = current_customer.build_profile
#profile.addresses.build
end
end
You'll need to look at how your params come through, but since I have a has_many, they came through hashed with a key of a record id.
I have migrated the :bank_name and :bank_account objects in User model.
I want two objects can be define from the Listings model in the listings/view to the User model columns.
I have already done (belongs_to, has_many)relations between two models.
But when I filled the bank_name and bank_account text_fields in Listing/view, I get the following error:
undefined method `bank_name' for #Listing:400123298
Here is my listing/view code:
<%= form_for(#listing, :html => { :multipart => true }) do |f| %>
...
<div class="form-group">
<%= f.label :bank_name %><br>
<%= f.text_field :bank_name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :bank_account %><br>
<%= f.text_field :bank_account, class: "form-control" %>
</div>
</end>
listing/controller:
def new
#listing = Listing.new
end
def create
#listing = Listing.new(listing_params)
#listing.user_id = current_user.id
#listing.user_id = User.bank_name.build(params[:bank_name])
#listing.user_id = User.bank_account.build(params[:bank_account])
end
Several issues for you
Nested
As mentioned in the comments, what you're looking at is a nested model structure.
Simply, this means you'll be able to create an associative model from your "parent" - giving you the ability to define the attributes you need in your "parent" model, passing them through to the nested. This functionality is handled by accepts_nested_attributes_for in your parent model
The best resource you can use is this Railscast (only the start):
--
Fix
Here's how you can fix the problem:
#app/models/listing.rb
class Listing < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
#app/models/user.rb
class User < ActiveRecord::Base
has_one :bank_account
accepts_nested_attributes_for :bank_account
end
#app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
belongs_to :user
end
#app/controllers/listings_controller.rb
class ListingsController < ApplicationController
def new
#listing = current_user.listings.new
#listing.user.build_bank_account
end
def create
#listing = Listing.new listing_params
#listing.save
end
private
def listing_params
params.require(:listing).permit(:listing, :params, user_attributes: [ bank_account_attributes: [] ])
end
end
This will help you do the following:
#app/views/listings/new.html.erb
<%= form_for #listing do |f| %>
...
<%= f.fields_for :user do |u| %>
<%= u.fields_for :bank_account do |b| %>
<%= b.text_field :name %>
<%= b.text_field :number %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
There is a slight twist to this tail, in that I'm not sure whether your passing of attributes through to your User model. This would be okay if the user was being created at the same time as your other attributes, but as it isn't, we may need to refactor the process of passing the nested data through
If this does not work, please comment & we can work to fix it!