3

I've been spinning may wheels with Rails on this for a few days. I'm working with three models: IngredientBase, Ingredient, and IngredientList. IngredientBase contains a string that may be "Chicken", "Eggs", or "Bacon." Ingredient will have a single Ingredient and a quantity. IngredientList will have many Ingredients.

I want the user to be able to ingredient_lists/new and be able to create several Ingredients before submitting the IngredientList. How could I do this without having the user submit multiple forms?

Richard Peck
  • 76,116
  • 9
  • 93
  • 147
Reimus Klinsman
  • 898
  • 2
  • 12
  • 23

2 Answers2

3

Associative

What you're looking at is nested models.

Nested models basically allow you to append attributes to an instance of one model; those attributes being sent to an associative model.

This is achieved with accepts_nested_attributes_for:

#app/models/ingredient_list.rb
class IngrendientList < ActiveRecord::Base
   has_many :ingredients
   accepts_nested_attributes_for :ingredients
end

#app/models/ingredient.rb
class Ingredient < ActiveRecord::Base
   belongs_to :ingredient_list
end

Because your models made me confused, I rewrote the structure for you. I think you're getting confused with join models etc:

#app/models/ingredient.rb
class Ingredient < ActiveRecord::Base
   #columns id | name | weight | etc | created_at | updated_at
   has_many :recipe_ingredients
   has_many :recipes, through: :recipe_ingredients
end

#app/models/recipe_ingredient.rb
class RecipeIngredient < ActiveRecord::Base
    #columns id | recipe_id | ingredient_id | quantity | created_at | updated_at
    belongs_to :recipe
    belongs_to :ingredient
    accepts_nested_attributes_for :ingredient
end

#app/models/recipe.rb
class Recipe < ActiveRecord::Base
   #columns id | name | etc | etc | created_at | updated_at
   has_many :recipe_ingredients #-> allows extra attributes
   has_many :ingredients, through: :recipe_ingredients 
   accepts_nested_attributes_for :recipe_ingredients
end

This will give you the ability to create a Recipe, add new Ingredients, and generally keep your model much more robust.

Here's how you can make it work with controller and views etc:

#app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
   def new
      @recipe = Recipe.new
   end
   def create
      @recipe = Recipe.new recipe_params
      @recipe.save
   end

   private

   def recipe_params
      params.require(:recipe).permit(:recipe, :params, recipe_ingredients_attributes: [:quantity, ingredient_attributes:[:name]])
   end
end

This will create a basic form, but we need to include the associated fields. There are several ways to do this, including the "manual" method from RailsCasts and using Cocoon.

The important thing to note is thus:

Every time you call a nested form field, you're basically getting Rails to add another instance of f.fields_for to your form. The VITAL thing to note here is you need to have the child_index of the fields_for block. This is what Rails uses to identify the fields, and needs to be kept unique.

You can see more about this on an answer I wrote some time back.

For your form, you need the following:

#app/views/recipes/new.html.erb
<%= form_for @recipe do |f| %>
   <%= f.text_field :title %> 
   <%= render "ingredients", locals: {f: f, child_index: Time.now.to_i}  %>
   <%= link_to "Add Ingredient", recipes_add_field_path, class: "ingredient" %>
   <%= f.submit %>
<% end %>

#app/views/recipes/_ingredients.html.erb
<%= f.fields_for :recipe_ingredients, child_index: child_index do |ri| %>
    <%= ri.text_field :quantity %>
    <%= ri.fields_for :ingredient do |ingredient| %>
       <%= ingredient.text_field :name %>
       <%= ingredient.text_field :weight %>
    <% end %>
<% end %>

#config/routes.rb
resources :recipes do
   member "add_field", to: "recipes#add_field"
end

#app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
   def add_field
      @recipe = Recipe.new
      @recipe.recipe_ingredients.build.build_ingredient
      render "new"
   end
end

#app/assets/javascripts/application.js
$(document).on("click", "a.ingredient", function(){
   $.ajax({
      url: '/recipes/add_field',
      success: function(data){
        el_to_add = $(data).html();
        $('#recipes').append(el_to_add);
      }
      error: function(data){
         alert("Sorry, There Was An Error!");
      }
   });
});
Community
  • 1
  • 1
Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • Thank you, I'm trying out this answer but I am getting an error in routes.rb why I run rake db:migrate on the line member "add_field", to: "recipes#add_field". The error I get is: `rake aborted! ArgumentError: wrong number of arguments (2 for 0) /home/reimus/mplan/config/routes.rb:5:in `block (2 levels) in ' /home/reimus/mplan/config/routes.rb:4:in `block in ' /home/reimus/mplan/config/routes.rb:1:in `' /home/reimus/mplan/config/environment.rb:5:in `' Tasks: TOP => routes => environment` – Reimus Klinsman Sep 26 '15 at 01:59
0

Best way to handle nested relations is to use accept_nested_attributes

For better understating here is a link

And to manage ui level form within form here is a gem

Arpit Vaishnav
  • 4,739
  • 6
  • 39
  • 57