本指南并非旨在列出所有可用的表单助手。有关表单助手及其参数的详尽列表,请参阅 Rails API 文档。
1 使用基本表单
主要的表单助手是 form_with
。
<%= form_with do |form| %>
Form contents
<% end %>
当不带参数调用时,它会创建一个带有 <form>
标签的 HTML,其中 method
属性的值设置为 post
,action
属性的值设置为当前页面。例如,假设当前页面是位于 /home
的首页,生成的 HTML 将如下所示
<form action="/home" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="Lz6ILqUEs2CGdDa-oz38TqcqQORavGnbGkG0CQA8zc8peOps-K7sHgFSTPSkBx89pQxh3p5zPIkjoOTiA_UWbQ" autocomplete="off">
Form contents
</form>
注意,表单包含一个类型为 hidden
的 input
元素。这个 authenticity_token
隐藏输入对于非 GET 表单提交是必需的。此标记是 Rails 中的安全功能,用于防止跨站点请求伪造 (CSRF) 攻击,表单助手会自动为每个非 GET 表单生成它(假设启用了安全功能)。您可以在 保护 Rails 应用程序 指南中阅读有关它的更多信息。
1.1 通用搜索表单
网络上最基本的表单之一是搜索表单。此表单包含
- 一个带有“GET”方法的表单元素,
- 输入的标签,
- 一个文本输入元素,以及
- 一个提交元素。
以下是使用 form_with
创建搜索表单的方法
<%= form_with url: "/search", method: :get do |form| %>
<%= form.label :query, "Search for:" %>
<%= form.search_field :query %>
<%= form.submit "Search" %>
<% end %>
这将生成以下 HTML
<form action="/search" accept-charset="UTF-8" method="get">
<label for="query">Search for:</label>
<input type="search" name="query" id="query">
<input type="submit" name="commit" value="Search" data-disable-with="Search">
</form>
注意,对于搜索表单,我们使用 form_with
的 url
选项。设置 url: "/search"
会将表单操作值从默认的当前页面路径更改为 action="/search"
。
通常,将 url: my_path
传递给 form_with
会告诉表单将请求发送到哪里。另一种选择是将 Active Model 对象传递给表单,您将在 下面 了解。您还可以使用 URL 助手。
上面的搜索表单示例还展示了 表单生成器 对象。您将在下一节中了解表单生成器对象提供的许多助手(如 form.label
和 form.text_field
)。
对于每个表单 input
元素,都会从其名称(上面的示例中的 "query"
)生成一个 id
属性。这些 ID 对使用 CSS 进行样式设置或使用 JavaScript 操作表单控件非常有用。
将“GET”用作搜索表单的方法。通常,Rails 约定鼓励对控制器操作使用正确的 HTTP 动词。对搜索使用“GET”允许用户将特定搜索添加到书签。
1.2 用于生成表单元素的助手
form_with
生成的表单生成器对象提供了许多助手方法,用于生成常见的表单元素,例如文本字段、复选框和单选按钮。
这些方法的第一个参数始终是输入的名称。这很重要,因为当提交表单时,该名称将与表单数据一起传递给控制器,并放在 params
哈希中。该名称将是 params
中用户为该字段输入的值的键。
例如,如果表单包含 <%= form.text_field :query %>
,那么您可以在控制器中使用 params[:query]
获取此字段的值。
在命名输入时,Rails 使用某些约定,这些约定使得提交具有非标量值(如数组或哈希)的参数成为可能,这些参数也将可以在 params
中访问。您可以在本指南的 表单输入命名约定和 Params 哈希 部分中阅读更多相关内容。有关这些助手的具体使用方式,请参阅 API 文档。
1.2.1 复选框
复选框是一种表单控件,允许选择或取消选择单个值。复选框组通常用于允许用户从组中选择一个或多个选项。
以下是一个在表单中使用三个复选框的示例
<%= form.checkbox :biography %>
<%= form.label :biography, "Biography" %>
<%= form.checkbox :romance %>
<%= form.label :romance, "Romance" %>
<%= form.checkbox :mystery %>
<%= form.label :mystery, "Mystery" %>
上面的代码将生成以下内容
<input name="biography" type="hidden" value="0" autocomplete="off"><input type="checkbox" value="1" name="biography" id="biography">
<label for="biography">Biography</label>
<input name="romance" type="hidden" value="0" autocomplete="off"><input type="checkbox" value="1" name="romance" id="romance">
<label for="romance">Romance</label>
<input name="mystery" type="hidden" value="0" autocomplete="off"><input type="checkbox" value="1" name="mystery" id="mystery">
<label for="mystery">Mystery</label>
checkbox
的第一个参数是输入的名称,可以在 params
哈希中找到。如果用户只选中了“Biography”复选框,则 params
哈希将包含
{
"biography" => "1",
"romance" => "0",
"mystery" => "0"
}
您可以使用 params[:biography]
检查用户是否选中了该复选框。
复选框的值(将出现在 params
中的值)可以使用 checked_value
和 unchecked_value
参数选择性地指定。有关更多详细信息,请参阅 API 文档。
还有一个 collection_checkboxes
,您可以在 与集合相关的助手部分 中了解它。
1.2.2 单选按钮
单选按钮是一种表单控件,它允许用户从一组选项中一次只选择一个选项。
例如,用于选择你最喜欢的冰淇淋口味的单选按钮
<%= form.radio_button :flavor, "chocolate_chip" %>
<%= form.label :flavor_chocolate_chip, "Chocolate Chip" %>
<%= form.radio_button :flavor, "vanilla" %>
<%= form.label :flavor_vanilla, "Vanilla" %>
<%= form.radio_button :flavor, "hazelnut" %>
<%= form.label :flavor_hazelnut, "Hazelnut" %>
以上将生成以下 HTML 代码
<input type="radio" value="chocolate_chip" name="flavor" id="flavor_chocolate_chip">
<label for="flavor_chocolate_chip">Chocolate Chip</label>
<input type="radio" value="vanilla" name="flavor" id="flavor_vanilla">
<label for="flavor_vanilla">Vanilla</label>
<input type="radio" value="hazelnut" name="flavor" id="flavor_hazelnut">
<label for="flavor_hazelnut">Hazelnut</label>
radio_button
的第二个参数是输入的值。因为这些单选按钮共享同一个名称(flavor
),用户只能选择其中一个,params[:flavor]
将包含 "chocolate_chip"
、"vanilla"
或 "hazelnut"
。
始终为复选框和单选按钮使用标签。它们使用 for
属性将文本与特定选项相关联,并且通过扩展可点击区域,使用户更容易点击输入框。
1.3 其他值得关注的辅助方法
还有许多其他表单控件,包括文本、电子邮件、密码、日期和时间。以下示例展示了一些更常用的辅助方法及其生成的 HTML 代码。
日期和时间相关的辅助方法
<%= form.date_field :born_on %>
<%= form.time_field :started_at %>
<%= form.datetime_local_field :graduation_day %>
<%= form.month_field :birthday_month %>
<%= form.week_field :birthday_week %>
输出
<input type="date" name="born_on" id="born_on">
<input type="time" name="started_at" id="started_at">
<input type="datetime-local" name="graduation_day" id="graduation_day">
<input type="month" name="birthday_month" id="birthday_month">
<input type="week" name="birthday_week" id="birthday_week">
具有特殊格式的辅助方法
<%= form.password_field :password %>
<%= form.email_field :address %>
<%= form.telephone_field :phone %>
<%= form.url_field :homepage %>
输出
<input type="password" name="password" id="password">
<input type="email" name="address" id="address">
<input type="tel" name="phone" id="phone">
<input type="url" name="homepage" id="homepage">
其他常见辅助方法
<%= form.textarea :message, size: "70x5" %>
<%= form.hidden_field :parent_id, value: "foo" %>
<%= form.number_field :price, in: 1.0..20.0, step: 0.5 %>
<%= form.range_field :discount, in: 1..100 %>
<%= form.search_field :name %>
<%= form.color_field :favorite_color %>
输出
<textarea name="message" id="message" cols="70" rows="5"></textarea>
<input value="foo" autocomplete="off" type="hidden" name="parent_id" id="parent_id">
<input step="0.5" min="1.0" max="20.0" type="number" name="price" id="price">
<input min="1" max="100" type="range" name="discount" id="discount">
<input type="search" name="name" id="name">
<input value="#000000" type="color" name="favorite_color" id="favorite_color">
隐藏输入对用户不可见,但可以像任何文本输入一样保存数据。它们内部的值可以使用 JavaScript 更改。
如果你使用密码输入字段,你可能需要配置你的应用程序以防止这些参数被记录。你可以在 安全 Rails 应用程序 指南中了解相关内容。
2 使用模型对象创建表单
2.1 将表单绑定到对象
form_with
辅助方法有一个 :model
选项,允许你将表单生成器对象绑定到模型对象。这意味着表单将作用域到该模型对象,并且表单的字段将使用该模型对象的值填充。
例如,如果我们有一个 @book
模型对象
@book = Book.find(42)
# => #<Book id: 42, title: "Walden", author: "Henry David Thoreau">
以及以下创建新书籍的表单
<%= form_with model: @book do |form| %>
<div>
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div>
<%= form.label :author %>
<%= form.text_field :author %>
</div>
<%= form.submit %>
<% end %>
它将生成以下 HTML 代码
<form action="/books" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="ChwHeyegcpAFDdBvXvDuvbfW7yCA3e8gvhyieai7DhG28C3akh-dyuv-IBittsjPrIjETlQQvQJ91T77QQ8xWA" autocomplete="off">
<div>
<label for="book_title">Title</label>
<input type="text" name="book[title]" id="book_title">
</div>
<div>
<label for="book_author">Author</label>
<input type="text" name="book[author]" id="book_author">
</div>
<input type="submit" name="commit" value="Create Book" data-disable-with="Create Book">
</form>
使用 form_with
与模型对象时,需要注意以下几点:
- 表单
action
将自动填充一个适当的值,action="/books"
。如果你正在更新一本书,它将是action="/books/42"
。 - 表单字段名称使用
book[...]
作用域。这意味着params[:book]
将是一个包含所有这些字段值的哈希表。你可以在本指南的 表单输入命名约定和参数哈希表 一章中了解有关输入名称重要性的更多信息。 - 提交按钮会自动赋予一个适当的文本值,在本例中为“创建书籍”。
通常你的表单输入会镜像模型属性。但是,它们不必这样做。如果你需要其他信息,可以在表单中包含一个字段,并通过 params[:book][:my_non_attribute_input]
访问它。
2.1.1 复合主键表单
如果你有一个具有 复合主键 的模型,表单构建语法与使用单主键时相同,但输出略有不同。
例如,要更新具有复合主键 [:author_id, :id]
的 @book
模型对象,如下所示
@book = Book.find([2, 25])
# => #<Book id: 25, title: "Some book", author_id: 2>
以下表单
<%= form_with model: @book do |form| %>
<%= form.text_field :title %>
<%= form.submit %>
<% end %>
将生成以下 HTML 输出
<form action="/books/2_25" method="post" accept-charset="UTF-8" >
<input name="authenticity_token" type="hidden" value="ChwHeyegcpAFDdBvXvDuvbfW7yCA3e8gvhyieai7DhG28C3akh-dyuv-IBittsjPrIjETlQQvQJ91T77QQ8xWA" />
<input type="text" name="book[title]" id="book_title" value="Some book" />
<input type="submit" name="commit" value="Update Book" data-disable-with="Update Book">
</form>
注意生成的 URL 包含用下划线分隔的 author_id
和 id
。提交后,控制器可以从参数中 提取每个主键值 并更新记录,就像使用单个主键一样。
2.1.2 fields_for
辅助方法
fields_for
辅助方法用于在同一个表单中渲染相关模型对象的字段。关联的“内部”模型通常通过 Active Record 关联与“主”表单模型相关联。例如,如果你有一个具有关联 ContactDetail
模型的 Person
模型,你可以创建一个包含两个模型输入的单个表单,如下所示
<%= form_with model: @person do |person_form| %>
<%= person_form.text_field :name %>
<%= fields_for :contact_detail, @person.contact_detail do |contact_detail_form| %>
<%= contact_detail_form.text_field :phone_number %>
<% end %>
<% end %>
以上将产生以下输出
<form action="/people" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="..." autocomplete="off" />
<input type="text" name="person[name]" id="person_name" />
<input type="text" name="contact_detail[phone_number]" id="contact_detail_phone_number" />
</form>
fields_for
生成的对象是一个类似于 form_with
生成的表单生成器。fields_for
辅助方法创建了类似的绑定,但没有渲染 <form>
标签。你可以在 API 文档 中了解更多有关 field_for
的信息。
2.2 依赖记录识别
在处理 RESTful 资源时,对 form_with
的调用可以通过依赖 记录识别 来简化。这意味着你传递模型实例,让 Rails 推断出模型名称、方法和其他内容。在以下创建新记录的示例中,对 form_with
的两次调用都生成了相同的 HTML 代码
# longer way:
form_with(model: @article, url: articles_path)
# short-hand:
form_with(model: @article)
类似地,对于以下编辑现有文章的示例,对 form_with
的两次调用也都会生成相同的 HTML 代码
# longer way:
form_with(model: @article, url: article_path(@article), method: "patch")
# short-hand:
form_with(model: @article)
注意,无论记录是新的还是已存在的,简写 form_with
调用都非常方便。记录识别足够智能,可以通过询问 record.persisted?
来判断记录是否为新的。它还会选择要提交到的正确路径以及基于对象类的名称。
假设 Article
模型在路由文件中声明为 resources :articles
。
如果你有一个 单数资源,你需要调用 resource
和 resolve
来使它与 form_with
一起工作
resource :article
resolve("Article") { [:article] }
声明一个资源会产生许多副作用。有关设置和使用资源的更多信息,请参阅 Rails 路由从外向内 指南。
当你使用 单表继承 你的模型时,如果只声明了父类为资源,你无法依赖子类的记录识别。你需要显式地指定 :url
和 :scope
(模型名称)。
2.3 使用命名空间
如果你有命名空间路由,form_with
有一个简写形式。例如,如果你的应用程序有一个 admin
命名空间
form_with model: [:admin, @article]
以上将创建一个表单,该表单提交到 admin
命名空间中的 Admin::ArticlesController
,因此在更新情况下将提交到 admin_article_path(@article)
。
如果你有多级命名空间,语法类似
form_with model: [:admin, :management, @article]
有关 Rails 路由系统和相关约定的更多信息,请参阅 Rails 路由从外向内 指南。
2.4 使用 PATCH、PUT 或 DELETE 方法的表单
Rails 框架鼓励 RESTful 设计,这意味着你应用程序中的表单将发出请求,其中 method
为 PATCH
、PUT
或 DELETE
,以及 GET
和 POST
。但是,HTML 表单在提交表单时 不支持 GET
和 POST
以外的其他方法。
Rails 通过使用一个名为 "_method"
的隐藏输入模拟 POST 上的其他方法。例如
form_with(url: search_path, method: "patch")
以上表单将生成以下 HTML 输出
<form action="/search" accept-charset="UTF-8" method="post">
<input type="hidden" name="_method" value="patch" autocomplete="off">
<input type="hidden" name="authenticity_token" value="R4quRuXQAq75TyWpSf8AwRyLt-R1uMtPP1dHTTWJE5zbukiaY8poSTXxq3Z7uAjXfPHiKQDsWE1i2_-h0HSktQ" autocomplete="off">
<!-- ... -->
</form>
解析 POST 数据时,Rails 会考虑特殊的 _method
参数,并像请求的 HTTP 方法是 _method
的值(本例中为 PATCH
)一样进行操作。
渲染表单时,提交按钮可以通过 formmethod:
关键字覆盖声明的 method
属性
<%= form_with url: "/posts/1", method: :patch do |form| %>
<%= form.button "Delete", formmethod: :delete, data: { confirm: "Are you sure?" } %>
<%= form.button "Update" %>
<% end %>
与 <form>
元素类似,大多数浏览器 不支持 通过 formmethod 覆盖声明的表单方法,除了 GET
和 POST
。
Rails 通过结合 formmethod、value 和 name 属性来模拟 POST 上的其他方法
<form accept-charset="UTF-8" action="/posts/1" method="post">
<input name="_method" type="hidden" value="patch" />
<input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
<!-- ... -->
<button type="submit" formmethod="post" name="_method" value="delete" data-confirm="Are you sure?">Delete</button>
<button type="submit" name="button">Update</button>
</form>
在这种情况下,“更新”按钮将被视为 PATCH
,“删除”按钮将被视为 DELETE
。
3 方便地创建下拉菜单
下拉菜单也称为下拉列表,允许用户从一组选项中进行选择。下拉菜单的 HTML 代码需要大量的标记 - 每个要选择的选项都需要一个 <option>
元素。Rails 提供了辅助方法来帮助生成这些标记。
例如,假设我们有一个供用户选择的城市列表。我们可以使用 select
辅助方法
<%= form.select :city, ["Berlin", "Chicago", "Madrid"] %>
以上将生成以下 HTML 输出
<select name="city" id="city">
<option value="Berlin">Berlin</option>
<option value="Chicago">Chicago</option>
<option value="Madrid">Madrid</option>
</select>
选择结果将按预期出现在 params[:city]
中。
我们还可以指定与标签不同的 <option>
值
<%= form.select :city, [["Berlin", "BE"], ["Chicago", "CHI"], ["Madrid", "MD"]] %>
输出
<select name="city" id="city">
<option value="BE">Berlin</option>
<option value="CHI">Chicago</option>
<option value="MD">Madrid</option>
</select>
这样,用户将看到完整的城市名称,但 params[:city]
将是 "BE"
、"CHI"
或 "MD"
之一。
最后,我们可以使用 :selected
参数为下拉菜单指定默认选项
<%= form.select :city, [["Berlin", "BE"], ["Chicago", "CHI"], ["Madrid", "MD"]], selected: "CHI" %>
输出
<select name="city" id="city">
<option value="BE">Berlin</option>
<option value="CHI" selected="selected">Chicago</option>
<option value="MD">Madrid</option>
</select>
3.1 下拉菜单的选项组
在某些情况下,我们可能希望通过将相关的选项分组在一起来改善用户体验。我们可以通过将一个 Hash
(或类似的 Array
)传递给 select
来做到这一点
<%= form.select :city,
{
"Europe" => [ ["Berlin", "BE"], ["Madrid", "MD"] ],
"North America" => [ ["Chicago", "CHI"] ],
},
selected: "CHI" %>
输出
<select name="city" id="city">
<optgroup label="Europe">
<option value="BE">Berlin</option>
<option value="MD">Madrid</option>
</optgroup>
<optgroup label="North America">
<option value="CHI" selected="selected">Chicago</option>
</optgroup>
</select>
3.2 将下拉菜单绑定到模型对象
与其他表单控件一样,下拉菜单可以绑定到模型属性。例如,如果我们有一个 @person
模型对象,如下所示
@person = Person.new(city: "MD")
以下表单
<%= form_with model: @person do |form| %>
<%= form.select :city, [["Berlin", "BE"], ["Chicago", "CHI"], ["Madrid", "MD"]] %>
<% end %>
将输出以下下拉菜单
<select name="person[city]" id="person_city">
<option value="BE">Berlin</option>
<option value="CHI">Chicago</option>
<option value="MD" selected="selected">Madrid</option>
</select>
唯一的区别是,选择的选项将出现在 params[:person][:city]
中,而不是 params[:city]
中。
注意,合适的选项已自动标记为 selected="selected"
。由于此下拉菜单绑定到现有的 @person
记录,因此我们不需要指定 :selected
参数。
4 使用日期和时间表单辅助方法
除了 前面 提到的 date_field
和 time_field
辅助方法之外,Rails 还提供了其他日期和时间表单辅助方法,这些方法渲染普通的下拉菜单。date_select
辅助方法为年份、月份和日期分别渲染一个下拉菜单。
例如,如果我们有一个 @person
模型对象,如下所示
@person = Person.new(birth_date: Date.new(1995, 12, 21))
以下表单
<%= form_with model: @person do |form| %>
<%= form.date_select :birth_date %>
<% end %>
将输出类似以下的下拉菜单
<select name="person[birth_date(1i)]" id="person_birth_date_1i">
<option value="1990">1990</option>
<option value="1991">1991</option>
<option value="1992">1992</option>
<option value="1993">1993</option>
<option value="1994">1994</option>
<option value="1995" selected="selected">1995</option>
<option value="1996">1996</option>
<option value="1997">1997</option>
<option value="1998">1998</option>
<option value="1999">1999</option>
<option value="2000">2000</option>
</select>
<select name="person[birth_date(2i)]" id="person_birth_date_2i">
<option value="1">January</option>
<option value="2">February</option>
<option value="3">March</option>
<option value="4">April</option>
<option value="5">May</option>
<option value="6">June</option>
<option value="7">July</option>
<option value="8">August</option>
<option value="9">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12" selected="selected">December</option>
</select>
<select name="person[birth_date(3i)]" id="person_birth_date_3i">
<option value="1">1</option>
...
<option value="21" selected="selected">21</option>
...
<option value="31">31</option>
</select>
注意,当提交表单时,params
哈希表中不会出现包含完整日期的单个值。相反,将出现多个具有特殊名称的值,如 "birth_date(1i)"
。但是,Active Model 知道如何根据模型属性的声明类型将这些值组装成一个完整的日期。因此,我们可以将 params[:person]
传递给 Person.new
或 Person#update
,就像我们使用单个字段表示完整日期一样。
除了 date_select
辅助方法之外,Rails 还提供了 time_select
,它输出用于小时和分钟的下拉菜单。此外还有 datetime_select
,它组合了日期和时间下拉菜单。
4.1 用于时间或日期组件的下拉菜单
Rails 还提供了一些辅助方法来渲染单个日期和时间组件的 select 框:select_year
、select_month
、select_day
、select_hour
、select_minute
和 select_second
。这些辅助方法是“裸”方法,这意味着它们不是在表单构建器实例上调用的。例如
<%= select_year 2024, prefix: "party" %>
以上输出一个类似于以下的 select 框
<select id="party_year" name="party[year]">
<option value="2019">2019</option>
<option value="2020">2020</option>
<option value="2021">2021</option>
<option value="2022">2022</option>
<option value="2023">2023</option>
<option value="2024" selected="selected">2024</option>
<option value="2025">2025</option>
<option value="2026">2026</option>
<option value="2027">2027</option>
<option value="2028">2028</option>
<option value="2029">2029</option>
</select>
对于每个辅助方法,你可以指定一个 Date
或 Time
对象,而不是一个数字作为默认值(例如 <%= select_year Date.today, prefix: "party" %>
而不是上面的),并且将提取和使用相应的日期和时间部分。
4.2 选择时区
当你需要询问用户他们所在的时区时,有一个非常方便的 time_zone_select
辅助方法可以使用。
通常,你必须为用户提供一个时区选项列表供他们选择。如果没有预定义的 ActiveSupport::TimeZone
对象列表,这可能会很乏味。time_with_zone
辅助方法封装了这一点,可以用以下方式使用
<%= form.time_zone_select :time_zone %>
输出
<select name="time_zone" id="time_zone">
<option value="International Date Line West">(GMT-12:00) International Date Line West</option>
<option value="American Samoa">(GMT-11:00) American Samoa</option>
<option value="Midway Island">(GMT-11:00) Midway Island</option>
<option value="Hawaii">(GMT-10:00) Hawaii</option>
<option value="Alaska">(GMT-09:00) Alaska</option>
...
<option value="Samoa">(GMT+13:00) Samoa</option>
<option value="Tokelau Is.">(GMT+13:00) Tokelau Is.</option>
</select>
5 与集合相关的辅助方法
如果你需要从任意对象的集合中生成一组选项,Rails 有 collection_select
、collection_radio_button
和 collection_checkboxes
辅助方法。
要查看这些辅助方法何时有用,假设你有一个 City
模型和一个相应的 belongs_to :city
与 Person
的关联
class City < ApplicationRecord
end
class Person < ApplicationRecord
belongs_to :city
end
假设我们有以下存储在数据库中的城市
City.order(:name).map { |city| [city.name, city.id] }
# => [["Berlin", 1], ["Chicago", 3], ["Madrid", 2]]
我们可以允许用户使用以下表单从城市中选择
<%= form_with model: @person do |form| %>
<%= form.select :city_id, City.order(:name).map { |city| [city.name, city.id] } %>
<% end %>
以上将生成以下 HTML
<select name="person[city_id]" id="person_city_id">
<option value="1">Berlin</option>
<option value="3">Chicago</option>
<option value="2">Madrid</option>
</select>
上面的示例展示了如何手动生成选项。但是,Rails 有些辅助方法可以从集合中生成选项,而无需显式地对其进行迭代。这些辅助方法通过对集合中的每个对象调用指定的方法来确定每个选项的值和文本标签。
在为 belongs_to
关联渲染字段时,你必须指定外键的名称(上面的示例中的 city_id
),而不是关联本身的名称。
5.1 collection_select
辅助方法
要生成一个 select 框,我们可以使用 collection_select
<%= form.collection_select :city_id, City.order(:name), :id, :name %>
以上输出与上面的手动迭代相同的 HTML
<select name="person[city_id]" id="person_city_id">
<option value="1">Berlin</option>
<option value="3">Chicago</option>
<option value="2">Madrid</option>
</select>
collection_select
的参数顺序不同于 select
的参数顺序。使用 collection_select
,我们首先指定值方法(上面的示例中的 :id
),然后指定文本标签方法(上面的示例中的 :name
)。这与为 select
辅助方法指定选项时使用的顺序相反,其中文本标签放在前面,值放在后面(前面示例中的 ["Berlin", 1]
)。
5.2 collection_radio_buttons
辅助方法
要生成一组单选按钮,我们可以使用 collection_radio_buttons
<%= form.collection_radio_buttons :city_id, City.order(:name), :id, :name %>
输出
<input type="radio" value="1" name="person[city_id]" id="person_city_id_1">
<label for="person_city_id_1">Berlin</label>
<input type="radio" value="3" name="person[city_id]" id="person_city_id_3">
<label for="person_city_id_3">Chicago</label>
<input type="radio" value="2" name="person[city_id]" id="person_city_id_2">
<label for="person_city_id_2">Madrid</label>
5.3 collection_checkboxes
辅助方法
要生成一组复选框——例如,要支持 has_and_belongs_to_many
关联——我们可以使用 collection_checkboxes
<%= form.collection_checkboxes :interest_ids, Interest.order(:name), :id, :name %>
输出
<input type="checkbox" name="person[interest_id][]" value="3" id="person_interest_id_3">
<label for="person_interest_id_3">Engineering</label>
<input type="checkbox" name="person[interest_id][]" value="4" id="person_interest_id_4">
<label for="person_interest_id_4">Math</label>
<input type="checkbox" name="person[interest_id][]" value="1" id="person_interest_id_1">
<label for="person_interest_id_1">Science</label>
<input type="checkbox" name="person[interest_id][]" value="2" id="person_interest_id_2">
<label for="person_interest_id_2">Technology</label>
6 上传文件
表单中常见的一项任务是允许用户上传文件。它可能是头像图片或包含要处理数据的 CSV 文件。文件上传字段可以使用 file_field
辅助方法渲染。
<%= form_with model: @person do |form| %>
<%= form.file_field :csv_file %>
<% end %>
关于文件上传,最重要的是要记住,渲染的表单的 enctype
属性 **必须** 设置为 multipart/form-data
。如果你在 form_with
中使用 file_field
,这将自动完成。你也可以手动设置该属性
<%= form_with url: "/uploads", multipart: true do |form| %>
<%= file_field_tag :csv_file %>
<% end %>
两者都输出以下 HTML 表单
<form enctype="multipart/form-data" action="/people" accept-charset="UTF-8" method="post">
<!-- ... -->
</form>
请注意,根据 form_with
约定,上面两个表单中的字段名称将不同。在第一个表单中,它将是 person[csv_file]
(可通过 params[:person][:csv_file]
访问),而在第二个表单中,它将只是 csv_file
(可通过 params[:csv_file]
访问)。
6.1 CSV 文件上传示例
使用 file_field
时,params
哈希中的对象是 ActionDispatch::Http::UploadedFile
的实例。以下是如何将上传的 CSV 文件中的数据保存到应用程序中的记录的示例
require "csv"
def upload
uploaded_file = params[:csv_file]
if uploaded_file.present?
csv_data = CSV.parse(uploaded_file.read, headers: true)
csv_data.each do |row|
# Process each row of the CSV file
# SomeInvoiceModel.create(amount: row['Amount'], status: row['Status'])
Rails.logger.info row.inspect
#<CSV::Row "id":"po_1KE3FRDSYPMwkcNz9SFKuaYd" "Amount":"96.22" "Created (UTC)":"2022-01-04 02:59" "Arrival Date (UTC)":"2022-01-05 00:00" "Status":"paid">
end
end
# ...
end
如果该文件是需要与模型一起存储的图像(例如用户的个人资料图片),则需要考虑许多任务,例如将文件存储在哪里(在磁盘、Amazon S3 等)、调整图像文件大小以及生成缩略图等。 Active Storage 旨在帮助完成这些任务。
7 自定义表单构建器
我们称由 form_with
或 fields_for
生成的对象为表单构建器。表单构建器允许你生成与模型对象关联的表单元素,并且是 ActionView::Helpers::FormBuilder
的实例。可以扩展此类以向应用程序添加自定义辅助方法。
例如,如果你想在整个应用程序中显示一个 text_field
和一个 label
,你可以将以下辅助方法添加到 application_helper.rb
中
module ApplicationHelper
def text_field_with_label(form, attribute)
form.label(attribute) + form.text_field(attribute)
end
end
并在表单中像往常一样使用它
<%= form_with model: @person do |form| %>
<%= text_field_with_label form, :first_name %>
<% end %>
但你也可以创建 ActionView::Helpers::FormBuilder
的子类,并在其中添加辅助方法。在定义此 LabellingFormBuilder
子类之后
class LabellingFormBuilder < ActionView::Helpers::FormBuilder
def text_field(attribute, options = {})
# super will call the original text_field method
label(attribute) + super
end
end
上面的表单可以用以下内容代替
<%= form_with model: @person, builder: LabellingFormBuilder do |form| %>
<%= form.text_field :first_name %>
<% end %>
如果你经常重复使用它,你可以定义一个 labeled_form_with
辅助方法,该方法会自动应用 builder: LabellingFormBuilder
选项
module ApplicationHelper
def labeled_form_with(**options, &block)
options[:builder] = LabellingFormBuilder
form_with(**options, &block)
end
end
上面可以用来代替 form_with
<%= labeled_form_with model: @person do |form| %>
<%= form.text_field :first_name %>
<% end %>
以上三种情况(text_field_with_label
辅助方法、LabellingFormBuilder
子类和 labeled_form_with
辅助方法)将生成相同的 HTML 输出
<form action="/people" accept-charset="UTF-8" method="post">
<!-- ... -->
<label for="person_first_name">First name</label>
<input type="text" name="person[first_name]" id="person_first_name">
</form>
使用的表单构建器还决定当你执行以下操作时会发生什么
<%= render partial: f %>
如果 f
是 ActionView::Helpers::FormBuilder
的实例,那么这将渲染 form
部分,并将部分对象设置为表单构建器。如果表单构建器是 LabellingFormBuilder
类,那么将渲染 labelling_form
部分。
表单构建器自定义(如 LabellingFormBuilder
)确实隐藏了实现细节(对于上面的简单示例来说可能显得过分)。根据你的表单使用自定义元素的频率,在不同的自定义方法、扩展 FormBuilder
类或创建辅助方法之间进行选择。
8 表单输入命名约定和 params
哈希
上面描述的所有表单辅助方法都有助于生成表单元素的 HTML,以便用户可以输入各种类型的输入。如何在控制器中访问用户输入值?params
哈希是答案。你已经在上面的示例中看到了 params
哈希。本节将更明确地介绍有关表单输入在 params
哈希中如何构建的命名约定的内容。
params
哈希可以包含数组和数组的哈希。值可以位于 params
哈希的顶层,也可以嵌套在另一个哈希中。例如,在 Person
模型的标准 create
操作中,params[:person]
将是 Person
对象所有属性的哈希。
请注意,HTML 表单没有用户输入数据的内在结构,它们生成的只是名称-值字符串对。你在应用程序中看到的数组和哈希是 Rails 使用的参数命名约定导致的结果。
params
哈希中的字段需要在 控制器中被允许。
8.1 基本结构
用户输入表单数据的两种基本结构是数组和哈希。
哈希反映了用于访问 params
中值的语法。例如,如果表单包含
<input id="person_name" name="person[name]" type="text" value="Henry"/>
则 params
哈希将包含
{ "person" => { "name" => "Henry" } }
并且 params[:person][:name]
将在控制器中检索提交的值。
哈希可以根据需要嵌套到多个级别,例如
<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>
以上将导致 params
哈希为
{ "person" => { "address" => { "city" => "New York" } } }
另一种结构是数组。通常,Rails 会忽略重复的参数名称,但如果参数名称以一组空的方括号 []
结尾,则参数将累积到数组中。
例如,如果你想让用户能够输入多个电话号码,你可以在表单中放置以下内容
<input name="person[phone_number][]" type="text"/>
<input name="person[phone_number][]" type="text"/>
<input name="person[phone_number][]" type="text"/>
这将导致 params[:person][:phone_number]
成为一个数组,包含提交的电话号码
{ "person" => { "phone_number" => ["555-0123", "555-0124", "555-0125"] } }
8.2 结合数组和哈希
你可以混合使用这两个概念。哈希的一个元素可能是一个数组,如前面的示例 params[:person]
哈希有一个名为 [:phone_number]
的键,其值为一个数组。
你也可以拥有一个哈希数组。例如,你可以通过重复以下表单片段来创建任意数量的地址
<input name="person[addresses][][line1]" type="text"/>
<input name="person[addresses][][line2]" type="text"/>
<input name="person[addresses][][city]" type="text"/>
<input name="person[addresses][][line1]" type="text"/>
<input name="person[addresses][][line2]" type="text"/>
<input name="person[addresses][][city]" type="text"/>
这将导致 params[:person][:addresses]
成为一个哈希数组。数组中的每个哈希将具有 line1
、line2
和 city
键,类似于以下内容
{ "person" =>
{ "addresses" => [
{ "line1" => "1000 Fifth Avenue",
"line2" => "",
"city" => "New York"
},
{ "line1" => "Calle de Ruiz de Alarcón",
"line2" => "",
"city" => "Madrid"
}
]
}
}
重要的是要注意,虽然哈希可以任意嵌套,但只允许一层“数组性”。数组通常可以用哈希替换。例如,与使用模型对象的数组不同,你可以使用一个以其 ID 或类似键为键的模型对象哈希。
数组参数与 checkbox
辅助方法配合使用效果不佳。根据 HTML 规范,未选中的复选框不会提交任何值。但是,复选框始终提交一个值通常很方便。checkbox
辅助方法通过创建具有相同名称的辅助隐藏输入来模拟这一点。如果未选中复选框,则只提交隐藏输入。如果选中,则两个都提交,但复选框提交的值优先。有一个 include_hidden
选项可以设置为 false
,如果你想省略此隐藏字段。默认情况下,此选项为 true
。
8.3 带有索引的哈希
假设你想渲染一个表单,其中包含一个人所有地址的字段集。[fields_for
][] 助手及其 :index
选项可以提供帮助
<%= form_with model: @person do |person_form| %>
<%= person_form.text_field :name %>
<% @person.addresses.each do |address| %>
<%= person_form.fields_for address, index: address.id do |address_form| %>
<%= address_form.text_field :city %>
<% end %>
<% end %>
<% end %>
假设这个人有两个地址,ID 分别为 23 和 45,则上述表单将渲染以下输出
<form accept-charset="UTF-8" action="/people/1" method="post">
<input name="_method" type="hidden" value="patch" />
<input id="person_name" name="person[name]" type="text" />
<input id="person_address_23_city" name="person[address][23][city]" type="text" />
<input id="person_address_45_city" name="person[address][45][city]" type="text" />
</form>
这将导致一个看起来像这样的 params
哈希
{
"person" => {
"name" => "Bob",
"address" => {
"23" => {
"city" => "Paris"
},
"45" => {
"city" => "London"
}
}
}
}
所有表单输入都映射到 "person"
哈希,因为我们在 person_form
表单构建器上调用了 fields_for
。此外,通过指定 index: address.id
,我们将每个城市输入的 name
属性渲染为 person[address][#{address.id}][city]
而不是 person[address][city]
。这样,你就可以知道在处理 params
哈希时应该修改哪些 Address
记录。
你可以在 API 文档 中找到有关 fields_for
索引选项的更多详细信息。
9 构建复杂表单
随着你的应用程序的增长,你可能需要创建更复杂的表单,不仅仅是编辑单个对象。例如,在创建 Person
时,你可以允许用户在同一表单中创建多个 Address
记录(家庭、工作等)。稍后在编辑 Person
记录时,用户也应该能够添加、删除或更新地址。
9.1 为嵌套属性配置模型
为了编辑给定模型(在本例中为 Person
)的关联记录,Active Record 通过 accepts_nested_attributes_for
方法提供模型级支持
class Person < ApplicationRecord
has_many :addresses, inverse_of: :person
accepts_nested_attributes_for :addresses
end
class Address < ApplicationRecord
belongs_to :person
end
这会在 Person
上创建一个 addresses_attributes=
方法,允许你创建、更新和销毁地址。
9.2 视图中的嵌套表单
以下表单允许用户创建 Person
及其关联地址。
<%= form_with model: @person do |form| %>
Addresses:
<ul>
<%= form.fields_for :addresses do |addresses_form| %>
<li>
<%= addresses_form.label :kind %>
<%= addresses_form.text_field :kind %>
<%= addresses_form.label :street %>
<%= addresses_form.text_field :street %>
...
</li>
<% end %>
</ul>
<% end %>
当一个关联接受嵌套属性时,fields_for
会为关联的每个元素渲染其块一次。特别是,如果一个人没有地址,它不会渲染任何内容。
一个常见的模式是让控制器构建一个或多个空子项,以便至少向用户显示一组字段。下面的示例将在新建的 person 表单中渲染 2 组地址字段。
例如,上面的 form_with
进行了以下更改
def new
@person = Person.new
2.times { @person.addresses.build }
end
将输出以下 HTML
<form action="/people" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="lWTbg-4_5i4rNe6ygRFowjDfTj7uf-6UPFQnsL7H9U9Fe2GGUho5PuOxfcohgm2Z-By3veuXwcwDIl-MLdwFRg" autocomplete="off">
Addresses:
<ul>
<li>
<label for="person_addresses_attributes_0_kind">Kind</label>
<input type="text" name="person[addresses_attributes][0][kind]" id="person_addresses_attributes_0_kind">
<label for="person_addresses_attributes_0_street">Street</label>
<input type="text" name="person[addresses_attributes][0][street]" id="person_addresses_attributes_0_street">
...
</li>
<li>
<label for="person_addresses_attributes_1_kind">Kind</label>
<input type="text" name="person[addresses_attributes][1][kind]" id="person_addresses_attributes_1_kind">
<label for="person_addresses_attributes_1_street">Street</label>
<input type="text" name="person[addresses_attributes][1][street]" id="person_addresses_attributes_1_street">
...
</li>
</ul>
</form>
fields_for
会生成一个表单构建器。参数名称将是 accepts_nested_attributes_for
所期望的。例如,当创建一个有 2 个地址的人时,提交的参数在 params
中将如下所示
{
"person" => {
"name" => "John Doe",
"addresses_attributes" => {
"0" => {
"kind" => "Home",
"street" => "221b Baker Street"
},
"1" => {
"kind" => "Office",
"street" => "31 Spooner Street"
}
}
}
}
:addresses_attributes
哈希中键的实际值并不重要。但它们需要是整数字符串,并且每个地址都不同。
如果关联对象已保存,fields_for
会自动生成一个包含已保存记录 id
的隐藏输入。你可以通过将 include_id: false
传递给 fields_for
来禁用此功能。
{
"person" => {
"name" => "John Doe",
"addresses_attributes" => {
"0" => {
"id" => 1,
"kind" => "Home",
"street" => "221b Baker Street"
},
"1" => {
"id" => "2",
"kind" => "Office",
"street" => "31 Spooner Street"
}
}
}
}
9.3 允许控制器中的参数
像往常一样,你需要在将参数传递给模型之前在控制器中声明允许的参数
def create
@person = Person.new(person_params)
# ...
end
private
def person_params
params.expect(person: [ :name, addresses_attributes: [[ :id, :kind, :street ]] ])
end
9.4 删除关联对象
你可以通过将 allow_destroy: true
传递给 accepts_nested_attributes_for
来允许用户删除关联对象
class Person < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses, allow_destroy: true
end
如果对象的属性哈希包含一个值为 true
(例如 1
、'1'
、true
或 'true'
)的键 _destroy
,则该对象将被销毁。此表单允许用户删除地址
<%= form_with model: @person do |form| %>
Addresses:
<ul>
<%= form.fields_for :addresses do |addresses_form| %>
<li>
<%= addresses_form.checkbox :_destroy %>
<%= addresses_form.label :kind %>
<%= addresses_form.text_field :kind %>
...
</li>
<% end %>
</ul>
<% end %>
_destroy
字段的 HTML 代码
<input type="checkbox" value="1" name="person[addresses_attributes][0][_destroy]" id="person_addresses_attributes_0__destroy">
你还需要更新控制器中的允许参数,以包含 _destroy
字段
def person_params
params.require(:person).
permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy])
end
9.5 防止空记录
忽略用户未填写的字段集通常很有用。你可以通过将一个 :reject_if
proc 传递给 accepts_nested_attributes_for
来控制这一点。此 proc 将使用表单提交的每个属性哈希调用。如果 proc 返回 true
,则 Active Record 不会为该哈希构建关联对象。下面的示例只尝试在设置了 kind
属性的情况下构建地址。
class Person < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses, reject_if: lambda { |attributes| attributes["kind"].blank? }
end
为了方便起见,你可以改为传递符号 :all_blank
,它将创建一个 proc,该 proc 将拒绝所有属性为空的记录,但不包括 _destroy
的任何值。
10 外部资源的表单
Rails 表单助手可用于构建用于将数据发布到外部资源的表单。如果外部 API 为资源期望 authenticity_token
,则可以将其作为 authenticity_token: 'your_external_token'
参数传递给 form_with
<%= form_with url: 'http://farfar.away/form', authenticity_token: 'external_token' do %>
Form contents
<% end %>
在其他情况下,表单中可用的字段受外部 API 限制,并且可能不希望生成 authenticity_token
。要不发送令牌,可以将 false
传递给 :authenticity_token
选项
<%= form_with url: 'http://farfar.away/form', authenticity_token: false do %>
Form contents
<% end %>
11 在没有表单构建器的情况下使用标签助手
如果你需要在表单构建器上下文之外渲染表单字段,Rails 提供了用于常见表单元素的标签助手。例如,checkbox_tag
<%= checkbox_tag "accept" %>
输出
<input type="checkbox" name="accept" id="accept" value="1" />
通常,这些助手的名称与其表单构建器对应项的名称相同,并在后面加一个 _tag
后缀。有关完整列表,请参阅 FormTagHelper
API 文档。
12 使用 form_tag
和 form_for
在 Rails 5.1 引入 form_with
之前,其功能在 form_tag
和 form_for
之间拆分。现在不再鼓励使用这两种方法,而是建议使用 form_with
,但你仍然可以在一些代码库中找到它们。