Search code examples
ruby-on-railsruby-on-rails-4nested-attributes

Form with nested attributes and has_many :through relationship create new record instead of using existing one



What I try to do is a simple form for Plate where you can choose which Ingredients you want. The number and name of Ingredients may vary by day.
Every time a new Plate is created, it created 4 new Choices with plate_id and ingredient_id and a boolean chosen.
plate_id is from the new plate create and ingredient_id should be the id of the already existing ingredient in my database, and chosen it's if the ingredient should be in the plate.

Here is my classes Plate, Choice and Ingredient :

class Plate < ActiveRecord::Base
    has_many :choices
    has_many :ingredients, through: :choices
    accepts_nested_attributes_for :choices
end

class Choice < ActiveRecord::Base
    belongs_to :plate
    belongs_to :ingredient
    accepts_nested_attributes_for :ingredient
end

class Ingredient < ActiveRecord::Base
    has_many :choices
    has_many :plates, through: :choices
end

And my Plate nested form looks like this :

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>

  <%= f.fields_for :choices do |fc| %>
    <%= fc.check_box :chosen %>
    <%= fc.fields_for :ingredient do |fi| %>
        <%= fi.text_field(:name)%> <br />
    <% end %>
  <% end %>

And finally my Plate controller :

  def new
    @plate = Plate.new
    @choice1 = @plate.choices.build
    @choice2 = @plate.choices.build
    @choice3 = @plate.choices.build
    @choice4 = @plate.choices.build
    @ingredient1 = Ingredient.find_by_name('peach')
    @ingredient2 = Ingredient.find_by_name('banana')
    @ingredient3 = Ingredient.find_by_name('pear')
    @ingredient4 = Ingredient.find_by_name('apple')
    @choice1.ingredient_id = @ingredient1.id
    @choice2.ingredient_id = @ingredient2.id
    @choice3.ingredient_id = @ingredient3.id
    @choice4.ingredient_id = @ingredient4.id

  end

  def plate_params
      params.require(:plate).permit(:name, choices_attributes: [:chosen, ingredient_attributes: [ :name]])
  end

My problem is that when I create a new plate, it create new ingredients with same name as the chosen one (but with different id of course) and Choices had the ingredient_id of the new ingredients created.

I tried to add the :id in the nested attributes :
params.require(:plate).permit(:name, choices_attributes: [:chosen, ingredient_attributes: [:id, :name]])
Bu when I do that, I go an error :

Couldn't find Ingredient with ID=1 for Choice with ID=

I searched for answers but couldn't find any, I guess I don't know Rails params, form and nested attributes enough to understand where is the problem.

Thanks for your help !
ps : It's my first question on Stack Overflow, please let my know if anything is wrong with my question


Solution

  • You don't need accepted_nested_parameters_for :ingredient since the form isn't designed to let you create ingredients or edit ingredients.

    Better that you just use collection_selectto select an existing ingredient as part of the choice record (that is, just save the ingredient_id).

      <%= f.fields_for :choices do |fc| %>
        <%= fc.check_box :chosen %>
        <%= fc.collection_select(:ingredient_id, Ingredient.all, :id, :name, prompt: true) %>
      <% end %>
    

    And your attributes should then be...

    params.require(:plate).permit(:name, choices_attributes: [:chosen, :ingredient_id]) 
    

    If you just want to show the ingredient but not allow the user to change which ingredient, you can do

      <%= f.fields_for :choices do |fc| %>
        <%= fc.check_box :chosen %>
        <%= fc.hidden_field :ingredient_id %>
        <%= Ingredient.find(fc.object.ingredient_id).name %>
      <% end %>
    

    You're finding the ingredient_id in the object encompassed by the form object fc and using that id to access the Ingredient, and retrieving the name attribute.

    Also notice the hidden field for the :ingredient_id ... to make sure it's returned in the attribute hash.