I'm a student learning Rails, and my project is to make a Recipes app. My recipes app has models Recipe
, Ingredient
, and other things, but the important thing is that an Ingredient
must be the only ingredient of it's kind in the table. A row would be just an id and name. There can be only one "rice" in the table, so any Recipe
that uses rice as an ingredient, uses that same row. That way (I guess) I can filter Recipe
s by Ingredient
s.
I have to use a nested form when creating/editing a Recipe
, so the user can fill out the recipe info and ingredients all in one screen, including adding new Ingredients
, and choosing ingredients from a drop list to select. In edit, the user can also delete (disassociate) and Ingredient
from a Recipe
.
I understand that I will need a join table (recipes_ingredients?? if so, what is the model called, RecipesIngredient
?
I don't really understand how this nested form will work. I am to create a fields_for
for all Ingredient
s? How do disassociate and create?
Maybe someone could point me in the right direction where I can read about this. I've been racking my brain for a long time, even starting the project over twice. I'm getting frustrated but I feel like I'm so close to understanding it.
I also tried using simple_form and cocoon, but I feel like that just confused me even more.
Any light shed on this subject would be amazing. Thank you.
This is a fairly typical join table setup:
Here we have a normalized table named ingredients
which serves as the master record for an ingredient and recipe_ingredients
which joins with the recipe
table.
In Rails we will set this up as a has_many through:
association:
class Recipe < ApplicationRecord
has_many :recipe_ingredients
has_many :ingredients, through: :recipe_ingredients
end
class Ingredient < ApplicationRecord
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
end
class RecipeIngredient < ApplicationRecord
belongs_to :recipe
belongs_to :ingredient
end
The reason you want to use has_many through:
and not has_and_belongs_to_many
is that the later is "headless" as there is no model. Which might seem like a good idea until you realize that the is no way to access additional columns (like for example the quantity).
When naming join tables for has_many through:
you should follow the scheme of [thing in singular]_[thing in plural]
due to the way that Rails resolves class names from table names. Using recipes_ingredients
for example would result in a missing constant error as Rails would attempt to load Recipes::Ingredient
. The join model itself should be named SingularSingular
.
You can of course also name join tables whatever fits the domain.
To add the nested rows you would use nested attributes and fields_for :recipe_ingredients
:
<%= form_for(@recipe) do |f| %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<fieldset>
<legend>Ingredients</legend>
<%= fields_for :recipe_ingredients do |ri| %>
<div class="nested_fields">
<div class="field">
<%= ri.label :ingredient %>
<%= ri.collection_select(:ingredient_id, Ingredient.all, :id, :name) %>
</div>
<div class="field">
<%= ri.number_field :quantity %>
</div>
</div>
<% end %>
</fieldset>
<% end %>
However nested fields are in many ways a cludge to allow the creation / modification of several models at once. The UX and application flow of moshing everything together is hardly ideal.
To provide a good UX its probably a better idea to apply incremental saves (the user saves the recipe before adding ingredients in the background with AJAX) and add the ingredients through a series of ajax POST requests to /recipies/:recipe_id/ingredients
. But this is the subject of entire tutorial on its own and should most likely be revisited after you understand the basics.
See: