You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为食谱应用表单添加最多5个唯一食材输入字段

Solution for Your Recipe App Ingredient Form

Hey there! I’ve built similar nested forms for recipe apps before, so let’s walk through a solid implementation that meets your requirements: supporting pre-selected ingredients, custom input, and a max of 5 fields.

Step 1: Model Setup (Critical for Nested Attributes)

First, make sure your models are set up correctly to handle nested ingredients. Add these associations and nested attribute support:

# app/models/recipe.rb
class Recipe < ApplicationRecord
  has_many :ingredients, dependent: :destroy
  accepts_nested_attributes_for :ingredients, 
                                reject_if: ->(attrs) { attrs[:name].blank? },
                                allow_destroy: true
end

# app/models/ingredient.rb
class Ingredient < ApplicationRecord
  belongs_to :recipe
  validates :name, presence: true
end

The reject_if option ensures empty ingredient fields don’t get saved, and allow_destroy lets us remove fields if needed.

Step 2: Controller Setup

In your recipes controller, initialize up to 5 empty ingredient fields so the form loads with the max allowed slots (we’ll also handle re-rendering with errors properly):

# app/controllers/recipes_controller.rb
def new
  @recipe = Recipe.new
  # Initialize 5 empty ingredient fields (max allowed)
  5.times { @recipe.ingredients.build }
end

def create
  @recipe = Recipe.new(recipe_params)
  if @recipe.save
    redirect_to @recipe, notice: "Recipe created successfully!"
  else
    # Re-render form with errors, re-initializing missing fields to keep 5 total
    (5 - @recipe.ingredients.size).times { @recipe.ingredients.build }
    render :new
  end
end

private
def recipe_params
  params.require(:recipe).permit(:name, :description, ingredients_attributes: [:id, :name, :_destroy])
end

Step 3: ERB Form Implementation

Now, let’s build the form with both a dropdown for existing ingredients and a text field for custom inputs. We’ll also add controls to remove fields while enforcing the 5-field limit:

<%= form_with(model: @recipe) do |f| %>
  <% if @recipe.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@recipe.errors.count, "error") %> prohibited this recipe from being saved:</h2>
      <ul>
        <% @recipe.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :description %>
    <%= f.text_area :description %>
  </div>

  <h3>Ingredients (Max 5)</h3>
  <%= f.fields_for :ingredients do |ingredient_form| %>
    <div class="ingredient-field-group">
      <%= ingredient_form.hidden_field :_destroy %>

      <%# Dropdown for existing ingredients %>
      <%= ingredient_form.label :existing_ingredient, "Choose existing or enter custom:" %>
      <%= select_tag "existing_ingredients[]", 
                     options_for_select(["Select an ingredient"] + Ingredient.pluck(:name).uniq),
                     class: "existing-ingredient-select" %>

      <%# Custom input field %>
      <%= ingredient_form.label :name, "Custom ingredient name" %>
      <%= ingredient_form.text_field :name, class: "custom-ingredient-input" %>

      <%# Remove button (only show if there's more than 1 field) %>
      <% if @recipe.ingredients.size > 1 %>
        <%= button_tag type: "button", class: "remove-ingredient-btn" do %>
          Remove Ingredient
        <% end %>
      <% end %>
    </div>
  <% end %>

  <%# Disable add button if we already have 5 ingredients %>
  <%= button_tag type: "button", id: "add-ingredient-btn", disabled: @recipe.ingredients.size >=5 do %>
    Add Ingredient
  <% end %>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Step 4: JavaScript for Dynamic Behavior

Add this JavaScript to handle auto-filling custom inputs, removing fields, and enforcing the 5-field limit:

document.addEventListener('DOMContentLoaded', function() {
  // Auto-fill custom input when selecting an existing ingredient
  document.querySelectorAll('.existing-ingredient-select').forEach(select => {
    select.addEventListener('change', function() {
      const customInput = this.closest('.ingredient-field-group').querySelector('.custom-ingredient-input');
      customInput.value = this.value !== "Select an ingredient" ? this.value : "";
    });
  });

  // Remove ingredient field (mark for destruction instead of deleting)
  document.addEventListener('click', function(e) {
    if (e.target.classList.contains('remove-ingredient-btn')) {
      const group = e.target.closest('.ingredient-field-group');
      const destroyInput = group.querySelector('input[name$="_destroy"]');
      destroyInput.value = '1';
      group.style.display = 'none';

      // Re-enable add button if we now have fewer than 5 visible fields
      const visibleGroups = document.querySelectorAll('.ingredient-field-group:not([style*="display: none"])').length;
      document.getElementById('add-ingredient-btn').disabled = visibleGroups >=5;
    }
  });

  // Add new ingredient field (clone and update nested attribute names)
  document.getElementById('add-ingredient-btn').addEventListener('click', function() {
    const lastGroup = document.querySelector('.ingredient-field-group:last-of-type');
    const newGroup = lastGroup.cloneNode(true);
    
    // Reset values and update field IDs/names for Rails nested attributes
    newGroup.querySelectorAll('input, select').forEach(input => {
      input.value = "";
      const regex = /(\d+)/;
      const currentIndex = input.name.match(regex)[1];
      const newIndex = parseInt(currentIndex) + 1;
      input.name = input.name.replace(regex, newIndex);
      input.id = input.id.replace(regex, newIndex);
    });

    lastGroup.after(newGroup);
    // Disable add button if we now have 5 visible fields
    const visibleGroups = document.querySelectorAll('.ingredient-field-group:not([style*="display: none"])').length;
    this.disabled = visibleGroups >=5;
  });
});

Key Notes

  • The accepts_nested_attributes_for setup ensures Rails correctly processes the nested ingredient fields.
  • The dropdown uses Ingredient.pluck(:name).uniq to pull unique existing ingredient names—you might want to cache this if you have a large dataset.
  • The JavaScript handles dynamic field management while respecting the 5-field limit, and ensures nested attribute names are correctly updated when adding new fields.

This implementation should fix the issues you were facing with your initial fields_for setup, and it fully supports both existing and custom ingredients.

内容的提问来源于stack exchange,提问作者Ron

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:07:47