2
\$\begingroup\$

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:

recipe.rb

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

ingredient.rb

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

rec_item.rb

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):

recipes_controller.rb

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:

rec_items.create!(ingredient_id: ingredient.id)

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:

views/recipes/new.html.erb

<%= link_to 'Return to recipe index', recipes_path %>
<%= render 'form' %>

_form.html.erb

<%= 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 %>

_rec_item_fields.html.erb

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

Thanks!

\$\endgroup\$
  • \$\begingroup\$ Check out this Railscast. It is doing a bit more than you're trying, but there is a working version you can compare with yours. railscasts.com/episodes/196-nested-model-form-part-1 and part 2 \$\endgroup\$ – slothbear Feb 24 '12 at 3:45
  • \$\begingroup\$ @slothbear - I did watch those Railscasts - they are very good and chock full of info, but he is just using has_many relationships without a has_many through: join table in the middle. I've yet to see a nice, simple tutorial on doing this with has_many through (maybe I'll have to write one once I get a handle on this stuff) \$\endgroup\$ – Jim Feb 27 '12 at 14:28
  • \$\begingroup\$ may not explain this that well but this railscast - railscasts.com/episodes/139-nested-resources \$\endgroup\$ – user26287 Jun 18 '13 at 14:14
2
\$\begingroup\$

I think you may want to look at route nesting to achieve what you want.
I have found that hidden fields often point to a need for a better solution and they are not a good approach themselves.

It can be confusing initially as there are usually two distinct areas to consider.

The first area is the model relationships, the accepts_nested_attributes_for from one model to another.
The other area is how resources are nested and whether one resources is accessed from another, i.e. in routes if one resources is nested in another.

I may not explain this that well but this railscast -
http://railscasts.com/episodes/139-nested-resources
and this link -
http://weblog.jamisbuck.org/2007/2/5/nesting-resources may help.
These guides are somewhat old but the principles still apply.

I would strongly recommend that you call the join table IngredientRecipe following the rails convention of alphabetically ordering the two (I before R) and the professional programmers convention of using real names now we're not short of bytes. You probably want to either have this code for a production system or want to use it to showcase your talents or use it to get into good habits to get a good job. Good naming conventions are a part of that.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the info - I'll check out those links and report back. Thanks for the naming convention help as well! \$\endgroup\$ – Jim Feb 27 '12 at 14:29
  • \$\begingroup\$ I was doing some research on the naming conventions. Am I correct in stating that the convention you mentioned would be appropriate for a HABTM relationship? In my case, since I'm using has_many through and the join table is represented by an actual model, it looks like it would follow regular model conventions (so, RecItem as the model and rec_items as the table, while not the best names, are appropriate). When I tried to use IngredientRecipe as my model name and ingredients_recipes as the table, rails gave an error bec it was looking for ingredient_recipes as the table for the model \$\endgroup\$ – Jim Feb 27 '12 at 16:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.