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

如何在Rails中创建支持可变数量jsonb项的表单?

Rails + Stimulus 实现动态JSON字段表单

一、Rails 模型与表单端配置

1. 模型设置

假设主模型为 Product,新增 JSONB 类型字段存储修饰符:

# db/migrate/xxxxxx_add_year_modifiers_to_products.rb
class AddYearModifiersToProducts < ActiveRecord::Migration[7.0]
  def change
    add_column :products, :year_modifiers, :jsonb, default: [], null: false
  end
end

模型中可添加基础验证,确保每个修饰符包含必填的类型字段:

# app/models/product.rb
class Product < ApplicationRecord
  validate :validate_year_modifiers

  private

  def validate_year_modifiers
    year_modifiers.each do |mod|
      errors.add(:year_modifiers, "每个修饰符必须指定类型") unless mod["type"].present?
    end
  end
end

2. 表单基础结构

在表单中渲染已有修饰符,同时提供新增模板与操作按钮:

# app/views/products/_form.html.erb
<%= form_with model: @product do |form| %>
  <!-- 其他业务字段 -->

  <div data-controller="modifiers" data-modifiers-template-value="<%= j render("modifier_fields", form: form, index: "{{index}}") %>">
    <h3>年份修饰符</h3>
    <div id="modifiers-container">
      <% @product.year_modifiers.each_with_index do |mod, index| %>
        <%= render "modifier_fields", form: form, modifier: mod, index: index %>
      <% end %>
    </div>

    <button type="button" data-action="modifiers#add">添加更多修饰符</button>
  </div>

  <%= form.submit %>
<% end %>

创建修饰符字段局部模板 _modifier_fields.html.erb:

<div class="modifier-fields">
  <%= form.select "year_modifiers[#{index}][type]", ["discount", "feature", "restriction"], 
                  selected: modifier["type"], 
                  prompt: "选择修饰符类型",
                  data: { action: "modifiers#switchType" } %>

  <div class="modifier-details">
    <% case modifier["type"] %>
    <% when "discount" %>
      <%= form.text_field "year_modifiers[#{index}][year]", value: modifier["year"], placeholder: "年份" %>
      <%= form.number_field "year_modifiers[#{index}][percentage]", value: modifier["percentage"], placeholder: "折扣百分比" %>
    <% when "feature" %>
      <%= form.text_field "year_modifiers[#{index}][year]", value: modifier["year"], placeholder: "年份" %>
      <%= form.text_area "year_modifiers[#{index}][description]", value: modifier["description"], placeholder: "特性描述" %>
    <% when "restriction" %>
      <%= form.text_field "year_modifiers[#{index}][year]", value: modifier["year"], placeholder: "年份" %>
      <%= form.text_field "year_modifiers[#{index}][rule]", value: modifier["rule"], placeholder: "限制规则" %>
    <% end %>
  </div>

  <button type="button" data-action="modifiers#remove">删除</button>
</div>

二、Stimulus 控制器实现

创建 Stimulus 控制器处理动态字段的增删与类型切换:

// app/javascript/controllers/modifiers_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { template: String }

  connect() {
    // 初始化时确保已有字段匹配对应类型的详情
    this.container.querySelectorAll(".modifier-fields").forEach(field => {
      const select = field.querySelector("select")
      this.switchType({ target: select })
    })
  }

  add() {
    const newIndex = this.container.children.length
    const template = this.templateValue.replace(/{{index}}/g, newIndex)
    this.container.insertAdjacentHTML("beforeend", template)
  }

  switchType(event) {
    const type = event.target.value
    const field = event.target.closest(".modifier-fields")
    const detailsContainer = field.querySelector(".modifier-details")
    const index = Array.from(this.container.children).indexOf(field)

    let fieldsHtml = ""
    switch(type) {
      case "discount":
        fieldsHtml = `
          <input type="text" name="product[year_modifiers][${index}][year]" placeholder="年份">
          <input type="number" name="product[year_modifiers][${index}][percentage]" placeholder="折扣百分比">
        `
        break
      case "feature":
        fieldsHtml = `
          <input type="text" name="product[year_modifiers][${index}][year]" placeholder="年份">
          <textarea name="product[year_modifiers][${index}][description]" placeholder="特性描述"></textarea>
        `
        break
      case "restriction":
        fieldsHtml = `
          <input type="text" name="product[year_modifiers][${index}][year]" placeholder="年份">
          <input type="text" name="product[year_modifiers][${index}][rule]" placeholder="限制规则">
        `
        break
      default:
        fieldsHtml = ""
    }
    detailsContainer.innerHTML = fieldsHtml
  }

  remove(event) {
    event.target.closest(".modifier-fields").remove()
    this.reindexFields()
  }

  reindexFields() {
    this.container.children.forEach((field, newIndex) => {
      field.querySelectorAll("input, textarea, select").forEach(input => {
        input.name = input.name.replace(/\[year_modifiers\]\[\d+\]/, `[year_modifiers][${newIndex}]`)
      })
    })
  }

  get container() {
    return document.getElementById("modifiers-container")
  }
}

三、后端参数处理

在控制器中配置强参数,确保修饰符字段能正确解析:

# app/controllers/products_controller.rb
def product_params
  params.require(:product).permit(
    # 其他业务字段
    year_modifiers: [:type, :year, :percentage, :description, :rule]
  )
end

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 02:43:25