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

Rails 7 Hotwire动态表单无法更新值,新增重复记录求助

问题描述

使用Hotwire遵循GoRails动态嵌套表单教程开发,ProgramPart与RichContent为has_many/belongs_to关联,已配置accepts_nested_attributes_for。编辑ProgramPart时,更新已有RichContent的值后提交表单,旧记录会保留,同时新增一条包含更新后值的新记录,而非直接更新原记录。提交参数显示rich_contents_attributes的键是一个非现有记录ID的数值。

相关代码

模型代码

class ProgramPart < ApplicationRecord
  has_many :rich_contents, dependent: :destroy
  accepts_nested_attributes_for :rich_contents, reject_if: :all_blank, allow_destroy: true
end

class RichContent < ApplicationRecord
  belongs_to :program_part
  has_rich_text :content
end

表单相关代码

<div id="block_elements" class="col-span-6 space-y-6">
  <% @program_part.rich_contents.each do |content| %>
    <%= render(RichContent::RichContentFormComponent.new(rich_content: content)) %>
  <% end %>
</div>
  
<div class="col-span-6">
  <span>
    <%= link_to "Text and media", new_company_program_program_part_rich_content_path(@company, @program, @program_part.persisted? ? @program_part.id : Time.now.to_i), data: { turbo_stream: true } %>
  </span>
</div>

<div class="bg-gray-50">      
  <%= part_form.submit class: "#{submit_classes}" %>
</div>

Turbo Stream文件

<%= turbo_stream.append "block_elements" do %>
  <%= render(RichContent::RichContentFormComponent.new(rich_content: @rich_content)) %>
<% end %>

# 控制器中 @rich_content = RichContent.new

ViewComponent代码

<%= fields_for "program_part[rich_contents_attributes][#{@rich_content.persisted? ? @rich_content.id : Time.now.to_i }]", @rich_content do |rich_content_form| %>
  <%= render(Forms::InputLabelComponent.new(label: "Text and media", form_builder:  rich_content_form, model_attribute: :content)) do %>
    <%= rich_content_form.rich_text_area :content %>
  <% end %>
<% end %>

提交参数示例

Processing by ProgramPartsController#update as TURBO_STREAM
14:28:07 web.1  |   Parameters: {"authenticity_token"=>"[FILTERED]",   "program_part"=>{"title"=>"Test", "description"=>"", "rich_contents_attributes"=>{"152"=>{"content"=>"<div>2</div>"}}}, "commit"=>"Update Program part",   "company_id"=>"1", "program_id"=>"9", "id"=>"27"}
问题原因

核心问题出在ViewComponent的fields_for手动构造逻辑上:

  • 对于已持久化的RichContent记录,代码用Time.now.to_i生成的临时值作为rich_contents_attributes的键,且没有在嵌套字段中传入记录的id。
  • Rails的accepts_nested_attributes_for识别更新操作的关键是:嵌套属性中必须包含对应子记录的id,这样Rails才能匹配到要更新的旧记录。
  • 提交参数里的"152"是临时生成的时间戳,和现有RichContent的ID完全不匹配,且参数中没有id字段,所以Rails会将这个请求判定为创建新记录,而非更新原有记录。
修复方案

方案1:使用父表单构建器生成嵌套字段(推荐)

不要手动构造字段名,借助父表单的part_form来生成嵌套字段,Rails会自动处理ID和属性键的匹配:

  1. 修改父表单的渲染逻辑,传递表单构建器给组件:
<div id="block_elements" class="col-span-6 space-y-6">
  <%= part_form.fields_for :rich_contents do |rich_content_form| %>
    <%= render(RichContent::RichContentFormComponent.new(form: rich_content_form)) %>
  <% end %>
</div>
  1. 更新ViewComponent,直接使用传入的表单构建器:
<%= render(Forms::InputLabelComponent.new(label: "Text and media", form_builder: @form, model_attribute: :content)) do %>
  <%= @form.rich_text_area :content %>
<% end %>

方案2:手动添加ID字段(不推荐,易出错)

如果一定要手动构造字段名,必须在嵌套属性中添加隐藏的id字段,让Rails识别出要更新的记录:

<%= fields_for "program_part[rich_contents_attributes][#{@rich_content.persisted? ? @rich_content.id : Time.now.to_i }]", @rich_content do |rich_content_form| %>
  <% if @rich_content.persisted? %>
    <%= rich_content_form.hidden_field :id %>
  <% end %>
  <%= render(Forms::InputLabelComponent.new(label: "Text and media", form_builder:  rich_content_form, model_attribute: :content)) do %>
    <%= rich_content_form.rich_text_area :content %>
  <% end %>
<% end %>

新增记录的Turbo Stream适配

对于通过Turbo Stream新增的RichContent,控制器创建空记录后,同样建议用父表单构建器的fields_for来渲染组件,确保字段符合Rails嵌套属性的规范。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 07:03:22