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?
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: [])
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])
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.