Search code examples
ruby-on-railsmany-to-many

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?


Solution

  • 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.