Nested Forms in Rails

Posted by Eli Cusic on March 7, 2021

Context: Ok, so I’m doing a little recipe management system for a project. I know, everyone has done this a thousand times. I’m hoping to take mine to a bit of higher lever, though. I used to manage kitchens and even worked as a sous chef, so the end goal is to have a basic kitchen supply management system that I can use not just to store recipes, but also to track the raw cost of ingredients and recipes as well as generate weekly menus and grocery lists. As I develop this application, I’ll put the subsequent blog posts right here:

  • no additional posts yet -

This will be very backend heavy with some interesting associations, but for now let’s look at a basic frontend rails form that involves an association - recipes with ingredients. Naturally a recipe has_many ingredients and an ingredient belongs_to a recipe. Now, we just need to make a quick form for a new recipe in order to get things kicked off, so let’s take a look at the recipe model and controller:

class Recipe < ApplicationRecord

  belongs_to :user
  has_many :ingredients, dependent: :destroy
  accepts_nested_attributes_for :ingredients

end

class RecipesController < ApplicationController
    
		def new
        @recipe = current_user.recipes.build
        10.times do
            @recipe.ingredients.build
        end
     end    
		
end

First off, we need to establish in the model that Recipe accepts_nested_attributes_for. We won’t bother ourselves with the rest of the controller just yet and focus on setting up the form. Naturally, we set up our new variable @recipe to belong to the current user. Then, we need to generate some new ingredients that belong to this new recipe. In this example, I’ve just set up an even 10. This is rather clumsy, and I hope to come back and make this more precise and elegant, but in the interest of moving forward it will do for now. On to the view’s erb file:

<%= form_with model: @recipe, local: true do |form| %>  

    <%= form.label :name, "Title: " %>
    <%= form.text_field :name %>

    <%= form.label :servings, "Servings: " %>
    <%= form.number_field :servings %><br><br>

    <%= form.fields_for :ingredients do |inner_form| %>

        <%= inner_form.label :ingredient, "Ingredient: " %>
        <%= inner_form.text_field :name %>
        <%= inner_form.number_field :quantity %><br><br>

    <% end %>

    <%= form.label :instructions, "Instructions: " %>
    <%= form.text_area :instructions%><br><br>

    <%= form.hidden_field :user_id, value: @recipe.user_id %>
    <%= form.submit "Create New Recipe" %>

<% end %>

Here we have the recipe’s form, which has its title, servings, and instructions fields. Then, inside this form we nest the ingredients with an inner form. Because all the new ingredient objects are the same, I only need to put one set for a new ingredient and then rails generates the other 9. Now a user can bring up the page, insert the recipe and ingredient information and the submit button will send a ‘POST’ request back to the controller, so let’s go back there and see what we need to do.

First, we’ll set up the recipe_params which will permit it’s own attributes, plus its nested attributes. This is achieved by including ingredients_attributes: [:name, :quantity], which will cycle through each new ingredient from the form and its attributes. Then, we’ll pass those recipe_params through Recipe.create and it will not only generate the new recipe, but also generate new ingredients associated with it. Here is the controller:

class RecipesController < ApplicationController
    
		def new
        @recipe = current_user.recipes.build
        10.times do
            @recipe.ingredients.build
        end
     end    
		 
		 def create
        new_recipe = Recipe.create(recipe_params)
        redirect_to user_recipes_path(new_recipe.user)
    end
    
		private

    def recipe_params
      params.require(:recipe).permit(
			:user_id,
      :title,
			:servings,
			:instructions,
      ingredients_attributes: [ :name, :quantity ]
    )
    end
		
end

At the moment, there is a bit of an issue with our setup. Each time we create a new recipe, we are also creating new ingredients. Theoretically, these ingredients already exist in the database. Checking for duplicates can be solved by implementing an ingredients_attributes=(ingredient) method which will use Ingredient.find_or_create_by(:name) to check for a duplicate. However, each ingredient needs to have its own quantity associated with that particular recipe as well, so we’ll forgo implementing this at the moment. Ultimately, this will require some join tables and some trickier associations - ingredients will belong to a Pantry as well, and some recipes (like sauces) will also be an ingredient!

Tackling these backend associations will be next task and most likely my next blog post as well. Stay tuned!