I'm building a rails app which uses a has many through relationship between three models. I have a Recipes model and an Ingredients model. The join table is called rec_items and it contains a few additional data fields. So, recipes have_many ingredients through rec_items, and ingredients have_many recipes through rec_items.
Getting a nested form to work has been an exercise in frustration. I finally have it working, but the code I'm using really bothers me. Take a look:
First, here are my models:
class Recipe < ActiveRecord::Base has_many :rec_items, :dependent => :destroy has_many :ingredients, :through => :rec_items accepts_nested_attributes_for :rec_items attr_accessible :name, :style, :batch_size, :rec_items_attributes validates :name, presence: true end
class Ingredient < ActiveRecord::Base has_many :rec_items, :dependent => :destroy has_many :recipes, :through => :rec_items attr_accessible :name, :price validates :name, presence: true end
class RecItem < ActiveRecord::Base belongs_to :recipe belongs_to :ingredient validates :recipe_id, presence: true validates :ingredient_id, presence: true end
Here is my recipes controller (snipped all but the new, create and update methods for brevity):
class RecipesController < ApplicationController def new @recipe = Recipe.new rec_item = @recipe.rec_items.build end def create @recipe = Recipe.new(params[:recipe]) if @recipe.save flash[:success] = "Recipe saved!" redirect_to @recipe else render 'new' end end def update @recipe = Recipe.find(params[:id]) if @recipe.update_attributes(params[:recipe]) flash[:success] = "Recipe updated!" redirect_to @recipe else render 'edit' end end end
With this code in place, everything seemed to be working ok via the console, and I was able to create new rec_item records using commands from the Recipe model like:
The trouble started when I tried building this into my new form, so I could have users add ingredients to a new recipe from the same form. To simplify things at this stage, I am working with only one ingredient. Here is what I cobbled together after a lot of trial and error:
<%= link_to 'Return to recipe index', recipes_path %> <%= render 'form' %>
<%= form_for(@recipe) do |f| %> <%= render 'shared/error_messages' %> <div class="field"> <%= f.label :name %> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :batch_size %> <%= f.text_field :batch_size %> </div> <table> <tr> <td>Ingredients</td> <td>Amount</td> </tr> <%= f.fields_for :rec_items do |builder| %> <%= render 'rec_item_fields', :f => builder %> <% end %> </table> <div class="actions"> <%= f.submit "Save recipe" %> </div> <% end %>
<tr> <td><%= f.collection_select(:ingredient_id, Ingredient.all, :id, :name) %></td> <td><%= f.text_field :amount %></td> </tr>
So, this did not work at all. I was getting errors on save saying "Rec item recipe_id could not be blank." I was testing various things and realized if I used this line at the end of _rec_item_fields.html.erb
<%= f.hidden_field :recipe_id, value: 1 %>
It worked! My recipe would save, and a rec_item would be created with the proper ingredient_id and recipe_id! I revised this to:
<%= f.hidden_field :recipe_id, value: :recipe_id %>
And it was good. Until I tried to edit an existing recipe. Then, I would get no errors on save, and I found out it was updating the recipe_id in the rec_item record to 0.
So, finally, I have this version of that line:
<%= f.hidden_field :recipe_id, value: recipe_id ||= 1 %>
and my call to this partial in the _form.html.erb (feeding in the current recipe.id):
<%= f.fields_for :rec_items do |builder| %> <%= render 'rec_item_fields', :f => builder, :recipe_id => @recipe.id %> <% end %>
This works for both new records and edits, but I hate it. I know why it is working -- now it is passing the right recipe.id for existing records, and using a "placeholder" value if it's a new record until the recipe is saved and assigned an ID (at which point I guess Rails Magic somehow supplies that id to the rec_item object). However, I have not seen this in any example, it feels wrong to me, and hopefully someone out there can help me understand the right way to do this.