关于 form-helpers
面向 Rails 开发者的表单技能,指导使用 Rails 8 的 form_with 和相关辅助方法构建表单。涵盖嵌套属性、文件上传、强参数及自定义表单构建器。
主要功能
- 01使用 form_with 构建模型或 URL 表单
- 02添加常见输入、选择及日期字段
- 03处理嵌套属性和强参数
- 04配置文件上传与富文本字段
- 05创建自定义表单构建器
适用场景
- 01构建模型编辑或搜索表单
- 02实现带子记录的嵌套表单
- 03添加文件上传或富文本输入
- 04将旧表单辅助方法迁移到 form_with
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill form-helpers安装后在 Claude Code 中输入/form-helpers确认已加载。
附带文件reference.md
详细介绍
Rails 表单辅助工具专家
使用 form_with 和相关辅助工具在 Rails 8 中构建正确、现代的表单。
何时使用此技能
- 在 Rails 视图中构建任何表单(form_with、嵌套表单、选择框、复选框)
- 使用 fields_for 和 accepts_nested_attributes_for 添加嵌套属性
- 选择合适的表单辅助工具(text_field、select、collection_select 等)
- 构建自定义表单构建器
- 将表单与 Stimulus 控制器集成
唯一规则
你只能使用 form_with 这个表单辅助工具。没有例外。
- ❌
form_for— 自 Rails 5.1 起已弃用。不要使用。 - ❌
form_tag— 自 Rails 5.1 起已弃用。不要使用。 - ✅
form_with— 唯一正确的表单辅助工具。
如果你在现有代码中看到 form_for 或 form_tag,修改该文件时请将其迁移到 form_with。如果你在编写新代码,就没有任何理由使用其他工具。
form_with 的两种模式
由模型支持的表单(最常见)
<%= form_with model: @article do |form| %>
<%= form.text_field :title %>
<%= form.submit %>
<% end %>
Rails 会推断所有内容:操作 URL、HTTP 方法(新记录使用 POST,已持久化记录使用 PATCH)、字段名称作用域(article[title])、提交按钮文本(“Create Article” 或 “Update Article”)。
基于 URL 的表单(无模型)
<%= form_with url: search_path, method: :get do |form| %>
<%= form.search_field :query %>
<%= form.submit "Search" %>
<% end %>
适用于搜索表单、外部 API,或任何未关联模型的表单。
说明
第 1 步:确定表单类型
| 场景 | 使用 |
|---|---|
| 创建或编辑模型记录 | form_with model: @record |
| 搜索表单 | form_with url: path, method: :get |
| 向外部 URL 提交的表单 | form_with url: "https://...", authenticity_token: false |
| 命名空间资源(例如 admin) | form_with model: [:admin, @article] |
| 嵌套资源 | form_with model: [@parent, @child] |
第 2 步:检查现有模式
务必先检查项目中现有的表单:
# Find existing form patterns
rg "form_with" app/views/ --type erb
# Check for custom form builders
rg "FormBuilder" app/ --type ruby
# Check for any deprecated form helpers (migration candidates)
rg "form_for\|form_tag" app/views/ --type erb
**遵循项目现有约定。**如果应用使用自定义表单构建器,请使用它。
第 3 步:构建表单
在表单构建器对象上使用合适的输入辅助工具。每个辅助工具都将属性名称作为第一个参数。
常用输入辅助工具:
<%= form_with model: @user do |form| %>
<%= form.text_field :name %>
<%= form.email_field :email %>
<%= form.password_field :password %>
<%= form.telephone_field :phone %>
<%= form.url_field :website %>
<%= form.textarea :bio, size: "70x5" %>
<%= form.number_field :age, in: 18..120 %>
<%= form.range_field :satisfaction, in: 1..10 %>
<%= form.date_field :birthday %>
<%= form.time_field :preferred_time %>
<%= form.datetime_local_field :available_at %>
<%= form.color_field :favorite_color %>
<%= form.hidden_field :referrer, value: "homepage" %>
<%= form.checkbox :terms_accepted %>
<%= form.submit %>
<% end %>
第 4 步:使用正确的选择框辅助工具
选择框是 Agent 最容易出错的地方。请阅读参考资料,了解完整模式。
静态选项:
<%= form.select :status, ["Draft", "Published", "Archived"] %>
<%= form.select :status, [["Draft", "draft"], ["Published", "published"]] %>
从集合中选择(belongs_to):
<%= form.collection_select :city_id, City.order(:name), :id, :name %>
带选项分组:
<%= form.grouped_collection_select :city_id, Country.order(:name), :cities, :name, :id, :name %>
带 prompt/include_blank:
<%= form.select :category_id, categories, prompt: "Select a category" %>
<%= form.collection_select :author_id, Author.all, :id, :name, include_blank: "None" %>
第 5 步:正确处理嵌套属性
这是 Rails 表单中最难的部分。请严格遵循此模式。
1. 配置模型:
class Person < ApplicationRecord
has_many :addresses, inverse_of: :person, dependent: :destroy
accepts_nested_attributes_for :addresses, allow_destroy: true,
reject_if: :all_blank
end
2. 在控制器中构建空子记录:
def new
@person = Person.new
@person.addresses.build # at least one empty set of fields
end
def edit
@person = Person.find(params[:id])
@person.addresses.build if @person.addresses.empty?
end
3. 构建嵌套表单:
<%= form_with model: @person do |form| %>
<%= form.text_field :name %>
<h3>Addresses</h3>
<%= form.fields_for :addresses do |address_form| %>
<div class="nested-fields">
<%= address_form.hidden_field :id %>
<%= address_form.text_field :street %>
<%= address_form.text_field :city %>
<%= address_form.label :_destroy, "Remove" %>
<%= address_form.checkbox :_destroy %>
</div>
<% end %>
<%= form.submit %>
<% end %>
4. 允许嵌套参数:
def person_params
params.expect(person: [
:name,
addresses_attributes: [[:id, :street, :city, :_destroy]]
])
end
嵌套表单的重要说明:
- 如果关联为空,
fields_for将不会渲染任何内容 — 你必须至少构建一个子记录 allow_destroy: true要求包含_destroy字段,并在强参数中允许:_destroyreject_if: :all_blank可防止保存空的嵌套记录- 对于现有记录,始终包含隐藏的
:id字段(fields_for 会自动添加) params.expect中的双层数组[[:id, :street, ...]]是有意为之 — 它表示“键为这些内容的哈希数组”
第 6 步:处理文件上传
<%= form_with model: @user do |form| %>
<%= form.file_field :avatar %>
<%= form.submit %>
<% end %>
- 存在
file_field时,form_with会自动设置enctype="multipart/form-data" - 在控制器中,
params[:user][:avatar]是一个ActionDispatch::Http::UploadedFile - 在生产环境中处理文件时,请使用 Active Storage — 不要自行实现
多个文件:
<%= form.file_field :documents, multiple: true %>
按数组允许:params.expect(user: [documents: []])
第 7 步:复杂表单的强参数
简单模型:
params.expect(article: [:title, :body, :published])
带嵌套属性:
params.expect(person: [
:name, :email,
addresses_attributes: [[:id, :street, :city, :state, :zip, :_destroy]]
])
带数组(复选框、多选):
params.expect(article: [:title, tag_ids: []])
带富文本(Action Text):
params.expect(article: [:title, :body]) # :body is the rich text attribute name
核心概念
CSRF 保护
每个非 GET 表单都会自动包含一个 authenticity_token 隐藏字段。这是 Rails 的 CSRF 保护。除非要向外部服务提交,否则不要禁用它:
<%# External API - disable token %>
<%= form_with url: "https://external.api/webhook", authenticity_token: false do |form| %>
HTTP 方法模拟
HTML 表单仅支持 GET 和 POST。Rails 通过隐藏的 _method 字段模拟 PATCH、PUT、DELETE:
<%# This generates method="post" with a hidden _method="patch" %>
<%= form_with model: @article, method: :patch do |form| %>
对于已持久化的记录,form_with model: @article 会自动设置 PATCH。
记录标识
form_with model: @article 会自动判断:
- 新记录? → 向
/articles发送 POST 请求(创建) - 已持久化的记录? → 向
/articles/:id发送 PATCH 请求(更新) - 提交文本 → “创建文章”或“更新文章”
这需要在路由中配置 resources :articles。
命名空间和嵌套资源
# Admin namespace
form_with model: [:admin, @article]
# → POST /admin/articles or PATCH /admin/articles/:id
# Nested resource
form_with model: [@magazine, @article]
# → POST /magazines/:magazine_id/articles
# Deep nesting
form_with model: [:admin, @magazine, @article]
自定义表单构建器
如果重复使用相同的表单模式,可以创建自定义构建器:
# app/form_builders/application_form_builder.rb
class ApplicationFormBuilder < ActionView::Helpers::FormBuilder
def text_field(attribute, options = {})
label(attribute) + super(attribute, options.merge(class: "form-input"))
end
end
使用它:
<%= form_with model: @user, builder: ApplicationFormBuilder do |form| %>
<%= form.text_field :name %> <%# Automatically includes label + CSS class %>
<% end %>
也可以创建一个 helper,让它成为默认构建器:
# app/helpers/application_helper.rb
def app_form_with(**options, &block)
options[:builder] = ApplicationFormBuilder
form_with(**options, &block)
end
富文本(Action Text)
<%= form.rich_text_area :body %>
需要安装 Action Text(rails action_text:install)。该属性在模型中声明:
class Article < ApplicationRecord
has_rich_text :body
end
单选按钮和复选框
单选按钮 — 用户选择一项:
<%= form.radio_button :flavor, "vanilla" %>
<%= form.label :flavor_vanilla, "Vanilla" %>
<%= form.radio_button :flavor, "chocolate" %>
<%= form.label :flavor_chocolate, "Chocolate" %>
集合单选按钮 — 从集合中选择:
<%= form.collection_radio_buttons :city_id, City.all, :id, :name %>
复选框 — 单个布尔值:
<%= form.checkbox :terms_accepted %>
<%= form.label :terms_accepted, "I accept the terms" %>
集合复选框 — has_many 或 HABTM:
<%= form.collection_checkboxes :interest_ids, Interest.all, :id, :name %>
注意:checkbox(而非 check_box)会生成一个值为 "0" 的隐藏字段,因此即使未勾选复选框,也会提交一个值。
日期和时间辅助方法
优先使用 HTML5 原生输入控件(现代且适合移动设备):
<%= form.date_field :born_on %>
<%= form.time_field :starts_at %>
<%= form.datetime_local_field :event_at %>
基于选择框的日期/时间输入(旧式,多选下拉菜单):
<%= form.date_select :born_on %>
<%= form.time_select :starts_at %>
<%= form.datetime_select :event_at %>
日期选择框会生成类似 born_on(1i)、born_on(2i)、born_on(3i) 的多参数属性。Active Record 知道如何重新组合这些参数。
反模式
- 使用
form_for或form_tag— 始终使用form_with - 忘记构建嵌套子记录 — 对于空关联,
fields_for不会渲染任何内容 - 强参数中缺少
_destroy— 复选框虽然存在,但销毁操作会悄然失败 - 为 belongs_to 使用不带
_id后缀的select— 字段名必须是外键 - 硬编码表单操作 URL — 让
model:推断 URL,或使用路由辅助方法 - 在
accepts_nested_attributes_for中忘记设置allow_destroy: true - 未添加
reject_if: :all_blank— 空的嵌套表单会创建空白记录 - 手动设置文件上传的
enctype—form_with搭配file_field会自动处理 - 在表单构建器代码块中使用
_tag辅助方法 — 应改用表单构建器方法 - 嵌套属性中未允许
id— 更新而非重复创建现有记录时需要它
动态添加/移除嵌套字段
如需动态添加嵌套字段(不重新加载页面),请使用 Stimulus:
<%# Render a hidden template for new fields %>
<template data-nested-form-target="template">
<%= form.fields_for :addresses, Address.new, child_index: "NEW_RECORD" do |af| %>
<div class="nested-fields" data-new-record>
<%= af.text_field :street %>
<%= af.text_field :city %>
<%= af.hidden_field :_destroy, value: false, data: { nested_form_target: "destroy" } %>
<button type="button" data-action="nested-form#remove">Remove</button>
</div>
<% end %>
</template>
<div data-nested-form-target="container">
<%= form.fields_for :addresses do |af| %>
<div class="nested-fields">
<%= af.text_field :street %>
<%= af.text_field :city %>
<%= af.hidden_field :_destroy, value: false, data: { nested_form_target: "destroy" } %>
<button type="button" data-action="nested-form#remove">Remove</button>
</div>
<% end %>
</div>
<button type="button" data-action="nested-form#add">Add Address</button>
Stimulus 控制器会将模板中的 NEW_RECORD 替换为唯一时间戳,以确保参数名称唯一。完整的 Stimulus 控制器代码请参阅 reference.md。
快速参考:输入辅助方法 → HTML 类型
| 辅助方法 | HTML type |
|---|---|
text_field | text |
email_field | email |
password_field | password |
telephone_field | tel |
url_field | url |
search_field | search |
number_field | number |
range_field | range |
date_field | date |
time_field | time |
datetime_local_field | datetime-local |
month_field | month |
week_field | week |
color_field | color |
hidden_field | hidden |
file_field | file |
textarea | <textarea> |
checkbox | checkbox |
radio_button | radio |
表单调试
# Check what params your form submits
# In controller:
Rails.logger.debug params.inspect
# Check generated HTML
# In browser dev tools, inspect the <form> element for:
# - action (correct URL?)
# - method (post?)
# - hidden _method field (patch/delete?)
# - authenticity_token present?
# - field names correct (model[attribute]?)
# Common param issues with nested forms:
# - addresses_attributes vs addresses (must be _attributes)
# - Missing id field (creates duplicates instead of updating)
# - _destroy not permitted (silently ignored)