ruby-on-railsrubynested-attributesfields-for

Rails not saving nested attributes


I have the tables Task and Item. I have a form for Item where I record all the possible items that my Tasks may have, which is working fine. Then I have the form for Task where all the Items are displayed alongside a field to put a cost value for each item. This will result in a join between Task and Item: TaskItem (this table contains task_id, item_id and cost).

When I submit the form, it's saving the Task but not the TaskItems associated. I don't see what I'm missing as I searched a lot for this problem and nothing seems to work. Please, see the code below.   

Model:

class Task < ApplicationRecord
    has_many :task_items
    has_many :items, :through => :task_items

    accepts_nested_attributes_for :task_items, :allow_destroy => true
end

class Item < ApplicationRecord
    has_many :task_items
    has_many :tasks, :through => :task_items
end

class TaskItem < ApplicationRecord
    belongs_to :task
    belongs_to :item

    accepts_nested_attributes_for :item, :allow_destroy => true
end

Controller:

def new
    @items = Item.all

    @task = Task.new
    @task.task_items.build
end

def create
    @task = Task.new(task_params)
    @task.save
    redirect_to action: "index"
end

private def task_params
    params.require(:task).permit(:id, :title, task_items_attributes: [:id, :item_id, :cost])
end

My view:

<%= form_for :task, url:tasks_path do |f| %>
<p>
    <%= f.label :title %><br>
    <%= f.text_field(:title, {:class => 'form-control'}) %><br>
</p>

<% @items.each do |item| %>
    <% @task_items = TaskItem.new %>
    <%= f.fields_for :task_items do |ti| %>
        <%= ti.label item.description %>
        <%= ti.text_field :cost %>
        <%= ti.hidden_field :item_id, value: item.id %>
    <% end %>
<% end %>

<p>
    <%= f.submit({:class => 'btn btn-primary'}) %>
</p>

Solution

  • You need to add inverse_of option to the has_many method in class Task:

    class Task < ApplicationRecord
      has_many :task_items, inverse_of: :task
      has_many :items, through: :task_items
    
      accepts_nested_attributes_for :task_items, :allow_destroy => true
    end
    

    This is due to the when creating a new TaskItem instance, it requires that the Task instance already exists in database to be able to grab the id fo the Task instance. Using this option, it skips the validation.

    You can read this post about inverse_of option and its use cases.

    fields_for has an option to specify the object which is going to store the information. This combined with building each of the TaskItem from the has_many collection should ensure that all the relationship are set correctly.

    View Code:

    <%= form_for @task do |f| %>
      <p>
        <%= f.label :title %><br>
        <%= f.text_field(:title, {:class => 'form-control'}) %><br>
      </p>
    
      <% @items.each do |item| %>
        <% task_item = @task.task_items.build %>
        <%= f.fields_for :task_items, task_item do |ti| %>
          <%= ti.label item.description %>
          <%= ti.text_field :cost %>
          <%= ti.hidden_field :item_id, value: item.id %>
        <% end %>
      <% end %>
    
      <p>
        <%= f.submit({:class => 'btn btn-primary'}) %>
      </p>  
    <% end %>
    

    Controller Code:

    def index
    end
    
    def new
      @items = Item.all
      @task = Task.new
    end
    
    def create
      @task = Task.new(task_params)
      @task.save
      redirect_to action: "index"
    end
    
    private
    
    def task_params
      params.require(:task).permit(:id, :title, task_items_attributes: [:id, :item_id, :cost])
    end