jtron
jtron

Reputation: 47

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>

Upvotes: 2

Views: 2281

Answers (1)

Rene Hernandez
Rene Hernandez

Reputation: 1576

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

Upvotes: 4

Related Questions