更多内容请访问 rubyonrails.org:

Action View 表单助手

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

阅读本指南后,您将了解

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

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

1. 使用基本表单

主要的表单助手是 form_with

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

当不带参数调用时,它会创建一个 HTML <form> 标签,其中 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 元素。非 GET 表单提交需要此 authenticity_token 隐藏输入。此令牌是 Rails 中的一个安全功能,用于防止跨站点请求伪造 (CSRF) 攻击,表单助手会自动为每个非 GET 表单生成它(假设安全功能已启用)。您可以在保护 Rails 应用程序指南中阅读更多相关信息。

1.1. 通用搜索表单

Web 上最基本的表单之一是搜索表单。此表单包含

  • 一个方法为“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 元素,其 id 属性是根据其名称(上面示例中的 "query")生成的。这些 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.new
# => #<Book id: nil, title: nil, author: nil>

以及以下用于创建新书的表单

<%= 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 哈希章节中阅读有关输入名称重要性的更多信息。
  • 提交按钮会自动获得适当的文本值,在本例中为“创建图书”。

通常,您的表单输入会反映模型属性。但是,它们不必如此。如果您需要其他信息,您可以在表单中包含一个字段,并通过 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 关联与“主”表单模型相关。例如,如果您有一个 Person 模型和一个关联的 ContactDetail 模型,您可以创建一个包含两个模型输入的单个表单,如下所示

<%= 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 文档中了解更多关于 fields_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>

解析 POSTed 数据时,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_yearselect_monthselect_dayselect_hourselect_minuteselect_second。这些助手是“裸”方法,这意味着它们不调用表单构建器实例。例如

<%= select_year 2024, prefix: "party" %>

上面将输出一个选择框,如下所示

<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 模型以及与 Person 关联的相应 belongs_to :city 关系

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 助手

要生成一个选择框,我们可以使用 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 助手指定选项时使用的顺序相反,在 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 的实例。此类别可以扩展以添加应用程序的自定义助手。

例如,如果您想在整个应用程序中显示带有 labeltext_field,您可以将以下助手方法添加到 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 partial,并将 partial 的对象设置为表单构建器。如果表单构建器是 LabellingFormBuilder 类,那么将渲染 labelling_form partial。

表单构建器自定义,例如 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. 带索引的哈希

假设您想为每个人的地址渲染一组字段。带有 :index 选项的 fields_for 助手可以提供帮助

<%= 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 会为关联的每个元素渲染其块一次。特别是,如果一个人没有地址,它什么也不会渲染。

一种常见的模式是控制器构建一个或多个空子项,以便至少一组字段显示给用户。下面的示例将导致在新人表单上渲染 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

如果对象的属性哈希包含键 _destroy,其值为计算结果为 true(例如 1'1'true'true'),则该对象将被销毁。此表单允许用户删除地址

<%= 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,但您仍然可以在某些代码库中找到它们。



回到顶部