如何在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
相关产品推荐
相关产品推荐

