更多内容请访问 rubyonrails.org:

Action View 表单助手

表单是 Web 应用程序中用户输入的常见界面。但是,由于需要处理表单控件、命名和属性,表单标记可能很繁琐且难以维护。Rails 通过提供视图助手简化了这一过程,视图助手是输出 HTML 表单标记的方法。本指南将帮助您了解不同的助手方法以及何时使用每个方法。

阅读本指南后,您将了解

  • 如何创建基本表单,例如搜索表单。
  • 如何使用基于模型的表单来创建和编辑特定的数据库记录。
  • 如何从多种数据类型生成下拉框。
  • Rails 提供的日期和时间助手。
  • 是什么使文件上传表单与众不同。
  • 如何将表单发布到外部资源并指定设置 authenticity_token
  • 如何构建复杂表单。

本指南并非旨在列出所有可用的表单助手。有关表单助手及其参数的详尽列表,请参阅 Rails API 文档

1 使用基本表单

主要的表单助手是 form_with

<%= form_with do |form| %>
  Form contents
<% end %>

当不带参数调用时,它会创建一个带有 <form> 标签的 HTML,其中 method 属性的值设置为 postaction 属性的值设置为当前页面。例如,假设当前页面是位于 /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>

注意,表单包含一个类型为 hiddeninput 元素。这个 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_withurl 选项。设置 url: "/search" 会将表单操作值从默认的当前页面路径更改为 action="/search"

通常,将 url: my_path 传递给 form_with 会告诉表单将请求发送到哪里。另一种选择是将 Active Model 对象传递给表单,您将在 下面 了解。您还可以使用 URL 助手

上面的搜索表单示例还展示了 表单生成器 对象。您将在下一节中了解表单生成器对象提供的许多助手(如 form.labelform.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_valueunchecked_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_idid。提交后,控制器可以从参数中 提取每个主键值 并更新记录,就像使用单个主键一样。

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

如果你有一个 单数资源,你需要调用 resourceresolve 来使它与 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 设计,这意味着你应用程序中的表单将发出请求,其中 methodPATCHPUTDELETE,以及 GETPOST。但是,HTML 表单在提交表单时 不支持 GETPOST 以外的其他方法。

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 覆盖声明的表单方法,除了 GETPOST

Rails 通过结合 formmethodvaluename 属性来模拟 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_fieldtime_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.newPerson#update,就像我们使用单个字段表示完整日期一样。

除了 date_select 辅助方法之外,Rails 还提供了 time_select,它输出用于小时和分钟的下拉菜单。此外还有 datetime_select,它组合了日期和时间下拉菜单。

4.1 用于时间或日期组件的下拉菜单

Rails 还提供了一些辅助方法来渲染单个日期和时间组件的 select 框:select_yearselect_monthselect_dayselect_hourselect_minuteselect_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>

对于每个辅助方法,你可以指定一个 DateTime 对象,而不是一个数字作为默认值(例如 <%= 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>

如果你需要从任意对象的集合中生成一组选项,Rails 有 collection_selectcollection_radio_buttoncollection_checkboxes 辅助方法。

要查看这些辅助方法何时有用,假设你有一个 City 模型和一个相应的 belongs_to :cityPerson 的关联

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_withfields_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 %>

如果 fActionView::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] 成为一个哈希数组。数组中的每个哈希将具有 line1line2city 键,类似于以下内容

{ "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_tagform_for

在 Rails 5.1 引入 form_with 之前,其功能在 form_tagform_for 之间拆分。现在不再鼓励使用这两种方法,而是建议使用 form_with,但你仍然可以在一些代码库中找到它们。



返回顶部