v7.0.0
更多資訊請前往 rubyonrails.org: 更多在 Ruby on Rails

Action View 表格 Helpers

Web 應用程式中的表單是使用者輸入的基本介面。然而,由於需要處理表單控制元件命名及其眾多屬性,表單標記的編寫和維護很快就會變得乏味。 Rails 通過提供 view helpers 來產生表單標記來消除這種複雜性。但是,由於這些 helpers 具有不同的用例,因此開發人員需要在使用它們之前瞭解輔助方法之間的差異。

閱讀本指南後,您將瞭解:

本指南並不是關於可用表格 helpers 及其引數的完整文件。請訪問 Rails API 文件 以獲取完整參考。

1 處理基本表格

helper的主要形式是form_with

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

當像這樣不帶引數呼叫時,它會建立一個表單標籤,當提交時,將 POST 到當前頁面。例如,假設當前頁面是主頁,產生的 HTML 將如下所示:

<form accept-charset="UTF-8" action="/" method="post">
  <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
  Form contents
</form>

您會注意到 HTML 包含一個 input 型別的 hidden 元素。這個 input 很重要,因為沒有它就不能成功提交非 GET 表單。 名為 authenticity_token 的隱藏輸入元素是 Rails 的一項安全特性,稱為跨站請求偽造保護,表單 helpers 為每個非 GET 表單產生它(前提是啟用了此安全功能)。您可以在 Securing Rails Applications 指南中閱讀更多相關資訊。

1.1 通用搜索表單

您在網路上看到的最基本的表單之一是搜尋表單。該表格包含:

  • 一個帶有“GET”方法的表單元素,
  • 輸入標籤,
  • 一個文字輸入元素,以及
  • 一個提交元素。

要建立此表單,您將使用 form_with 及其產生的表單構建器物件。像這樣:

<%= form_with url: "/search", method: :get do |form| %>
  <%= form.label :query, "Search for:" %>
  <%= form.text_field :query %>
  <%= form.submit "Search" %>
<% end %>

這將產生以下 HTML:

<form action="/search" method="get" accept-charset="UTF-8" >
  <label for="query">Search for:</label>
  <input id="query" name="query" type="text" />
  <input name="commit" type="submit" value="Search" data-disable-with="Search" />
</form>

提示:將 url: my_specified_path 傳遞給 form_with 告訴表單在哪裡發出請求。但是,如下所述,您也可以將 ActiveRecord 物件傳遞給表單。

提示:對於每個表單輸入,都會根據其名稱產生一個 ID 屬性(上例中的 "query")。這些 ID 對於 CSS 樣式或使用 JavaScript 操作表單控制元件非常有用。

重要提示:使用“GET”作為搜尋表單的方法。這允許使用者為特定搜尋新增書籤並返回到它。更一般地說,Rails 鼓勵您對 action 使用正確的 HTTP 動詞。

1.2 Helpers 用於產生表單元素

form_with 產生的表單構建器物件提供了許多 helper 方法來產生表單元素,例如文字欄位、複選框和單選按鈕。這些方法的第一個引數始終是 輸入。提交表單時,名稱將與表單一起傳遞 資料,並將通過 controller 進入 params value 由使用者為該欄位輸入。例如,如果表單包含 <%= form.text_field :query %>,那麼你就可以得到這個的value controller 中的欄位與 params[:query]

在命名輸入時,Rails 使用某些約定使得可以使用非標量 values 提交引數,例如陣列或雜湊,這些引數也可以在 params 中訪問。您可以在本指南的 瞭解引數命名約定 一章中閱讀有關它們的更多資訊。這些helpers的具體用法請參考API文件

1.2.1 複選框

複選框是表單控制元件,為使用者提供一組可以啟用或禁用的選項:

<%= form.check_box :pet_dog %>
<%= form.label :pet_dog, "I own a dog" %>
<%= form.check_box :pet_cat %>
<%= form.label :pet_cat, "I own a cat" %>

這會產生以下內容:

<input type="checkbox" id="pet_dog" name="pet_dog" value="1" />
<label for="pet_dog">I own a dog</label>
<input type="checkbox" id="pet_cat" name="pet_cat" value="1" />
<label for="pet_cat">I own a cat</label>

check_box 的第一個引數是輸入的名稱。第二個引數是輸入的 value。選中該複選框後,此 value 將包含在表單資料中(並出現在 params 中)。

1.2.2 單選按鈕

單選按鈕雖然類似於複選框,但它們是指定一組選項的控制元件,其中它們是互斥的(即,使用者只能選擇一個):

<%= form.radio_button :age, "child" %>
<%= form.label :age_child, "I am younger than 21" %>
<%= form.radio_button :age, "adult" %>
<%= form.label :age_adult, "I am over 21" %>

輸出:

<input type="radio" id="age_child" name="age" value="child" />
<label for="age_child">I am younger than 21</label>
<input type="radio" id="age_adult" name="age" value="adult" />
<label for="age_adult">I am over 21</label>

check_box 一樣,radio_button 的第二個引數是輸入的 value。由於這兩個單選按鈕具有相同的名稱 (age),因此使用者只能選擇其中之一,而 params[:age] 將包含 "child""adult"

始終為複選框和單選按鈕使用標籤。他們將文字與特定選項相關聯,並且, 通過擴充套件可點選區域, 使使用者更容易點選輸入。

1.3 其他感興趣的 Helper

其他值得一提的表單控制元件有文字區域、隱藏欄位、密碼欄位、數字欄位、日期和時間欄位等等:

<%= form.text_area :message, size: "70x5" %>
<%= form.hidden_field :parent_id, value: "foo" %>
<%= form.password_field :password %>
<%= form.number_field :price, in: 1.0..20.0, step: 0.5 %>
<%= form.range_field :discount, in: 1..100 %>
<%= 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 %>
<%= form.search_field :name %>
<%= form.email_field :address %>
<%= form.telephone_field :phone %>
<%= form.url_field :homepage %>
<%= form.color_field :favorite_color %>

輸出:

<textarea name="message" id="message" cols="70" rows="5"></textarea>
<input type="hidden" name="parent_id" id="parent_id" value="foo" />
<input type="password" name="password" id="password" />
<input type="number" name="price" id="price" step="0.5" min="1.0" max="20.0" />
<input type="range" name="discount" id="discount" min="1" max="100" />
<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" />
<input type="search" name="name" id="name" />
<input type="email" name="address" id="address" />
<input type="tel" name="phone" id="phone" />
<input type="url" name="homepage" id="homepage" />
<input type="color" name="favorite_color" id="favorite_color" value="#000000" />

隱藏輸入不會向用戶顯示,而是像任何文字輸入一樣儲存資料。其中的 Value 可以用 JavaScript 更改。

重要:搜尋、電話、日期、時間、顏色、日期時間、本地日期時間、 月、周、URL、電子郵件、數字和範圍輸入是 HTML5 控制元件。 如果您需要您的應用在舊版瀏覽器中獲得一致的體驗, 你需要一個 HTML5 polyfill(由 CSS 和/或 JavaScript 提供)。 肯定不乏解決方案,儘管目前流行的工具是 Modernizr,它提供了一種簡單的方法來新增根據存在的功能 檢測到 HTML5 功能。

提示:如果您使用密碼輸入欄位(出於任何目的),您可能需要設定您的應用程式以防止記錄這些引數。您可以在 保護 Rails 應用程式 指南中瞭解這一點。

2 處理 Model 物件

2.1 將表單繫結到物件

form_with:model 引數允許我們將表單構建器物件繫結到 model 物件。這意味著表單的範圍將限定為該 model 物件,並且表單的欄位將使用該 model 物件中的 values 填充。

例如,如果我們有一個 @article model 物件,如:

@article = Article.find(42)
# => #<文章 id: 42, title: "My Title", body: "My Body">

以下表格:

<%= form_with model: @article do |form| %>
  <%= form.text_field :title %>
  <%= form.text_area :body, size: "60x10" %>
  <%= form.submit %>
<% end %>

輸出:

<form action="/articles/42" method="post" accept-charset="UTF-8" >
  <input name="authenticity_token" type="hidden" value="..." />
  <input type="text" name="article[title]" id="article_title" value="My Title" />
  <textarea name="article[body]" id="article_body" cols="60" rows="10">
    My Body
  </textarea>
  <input type="submit" name="commit" value="Update Article" data-disable-with="Update Article">
</form>

這裡有幾件事情需要注意:

  • 表格 action 會自動為 @article 填寫適當的 value。
  • 表單欄位會自動填寫來自 @article 的相應 values。
  • 表單欄位名稱的範圍為 article[...]。這意味著 params[:article] 將是包含所有這些欄位的 values 的雜湊。您可以在本指南的 瞭解引數命名約定 一章中閱讀有關輸入名稱重要性的更多資訊。
  • 提交按鈕會自動賦予適當的文字 value。

提示:通常您的輸入將反映 model 屬性。然而,他們沒有必要!如果您需要其他資訊,您可以將其包含在表單中,就像使用屬性一樣,並通過 params[:article][:my_nifty_non_attribute_input] 訪問它。

2.1.1 fields_for Helper

您可以建立類似的繫結,而無需實際使用 fields_for helper 建立 <form> 標籤。這對於編輯具有相同形式的其他 model 物件很有用。例如,如果您有一個 Person model 與關聯的 ContactDetail model,您可以建立一個表單來建立兩者,如下所示:

<%= 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="bL13x72pldyDD8bgtkjKQakJCpd4A8JdXGbfksxBDHdf1uC0kCMqe2tvVdUYfidJt0fj3ihC4NxiVHv8GVYxJA==" />
  <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 產生的表單構建器。

2.2 依賴記錄標識

文章 model 可直接提供給應用程式的使用者,因此 - 遵循使用 Rails 進行開發的最佳實踐 - 您應該將其宣告為 資源

resources :articles

提示:宣告一個資源有很多副作用。有關設定和使用資源的更多資訊,請參閱 Rails 從外向內路由 指南。

在處理 RESTful 資源時,如果您依賴記錄識別,則對 form_with 的呼叫會變得更加容易。簡而言之,您可以只傳遞 model 實例並讓 Rails 找出 model 名稱和其餘部分。在這兩個示例中,多頭和空頭樣式具有相同的結果:

## 建立新文章
# 長樣式:
form_with(model: @article, url: articles_path)
# 短樣式:
form_with(model: @article)

## 編輯現有文章
# 長樣式:
form_with(model: @article, url: article_path(@article), method: "patch")
# 短樣式:
form_with(model: @article)

請注意簡短樣式的 form_with 呼叫是如何方便地相同的,無論記錄是新的還是現有的。記錄識別足夠聰明,可以通過詢問 record.persisted? 來確定記錄是否是新的。它還選擇要提交到的正確路徑,以及根據物件類的名稱。

如果您有 單一資源,則需要呼叫 resourceresolve 使其與 form_with 一起使用:

resource :geocoder
resolve('Geocoder') { [:geocoder] }

當您將 STI(單表繼承)與 models 一起使用時,如果僅將其父類宣告為資源,則不能依賴子類的記錄標識。您必須明確指定 :url:scope(模型名稱)。

2.2.1 處理名稱空間

如果您建立了名稱空間路由,form_with 也有一個漂亮的簡寫。如果您的應用程式有一個 admin 名稱空間,那麼

form_with model: [:admin, @article]

將建立一個提交到 admin 名稱空間內的 ArticlesController 的表單(在更新的情況下提交到 admin_article_path(@article))。如果您有多個級別的名稱空間,則語法類似:

form_with model: [:admin, :management, @article]

有關 Rails 路由系統和相關約定的更多資訊,請參閱 Rails 從外向內路由 指南。

2.3 使用 PATCH、PUT 或 DELETE 方法的表單如何工作?

Rails 框架鼓勵您的應用程式的 RESTful 設計,這意味著您將發出大量“PATCH”、“PUT”和“DELETE”請求(除了“GET”和“POST”)。但是,在提交表單時,大多數瀏覽器不支援除“GET”和“POST”之外的其他方法。

Rails 通過使用名為 "_method" 的隱藏輸入模擬 POST 上的其他方法來解決此問題,該輸入設定為反映所需的方法:

form_with(url: search_path, method: "patch")

輸出:

<form accept-charset="UTF-8" action="/search" method="post">
  <input name="_method" type="hidden" value="patch" />
  <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
  <!-- ... -->
</form>

在解析 POSTed 資料時,Rails 將考慮特殊的 _method 引數,並且就像 HTTP 方法是其中指定的方法一樣(在本例中為“PATCH”)。

渲染表單時,提交按鈕可以通過 formmethod: keyword 覆蓋宣告的 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][button-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>

重要提示:在 Rails 6.0 和 5.2 中,所有使用 form_with 的表單預設都實現 remote: true。這些表單將使用 XHR (Ajax) 請求提交資料。要禁用此功能,請包括 local: true。要深入瞭解,請參閱 在 Rails 中使用 JavaScript 指南。

3 輕鬆製作選擇框

HTML 中的選擇框需要大量標記 - 每個選項都有一個 <option> 元素可供選擇。所以 Rails 提供了 helper 方法來減輕這個負擔。

例如,假設我們有一個城市列表供使用者選擇。我們可以像這樣使用 select helper:

<%= form.select :city, ["Berlin", "Chicago", "Madrid"] %>

輸出:

<select name="city" id="city">
  <option value="Berlin">Berlin</option>
  <option value="Chicago">Chicago</option>
  <option value="Madrid">Madrid</option>
</select>

我們還可以指定不同於其標籤的 <option> values:

<%= 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 選擇框和 Model 物件

與其他表單控制元件一樣,選擇框可以繫結到 model 屬性。例如,如果我們有一個 @person model 物件,如:

@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>

請注意,適當的選項被自動標記為 selected="selected"。由於這個選擇框繫結到一個 model,我們不需要指定一個 :selected 引數!

3.3 時區和國家選擇

要利用 Rails 中的時區支援,您必須詢問使用者他們所在的時區。這樣做需要從預定義的 ActiveSupport::TimeZone 物件列表中產生選擇選項,但您可以簡單地使用 time_zone_select helper 已經包裝了這個:

<%= form.time_zone_select :time_zone %>

Rails used 用於選擇國家的 country_select helper,但這已被提取到 country_select plugin

4 使用日期和時間形式 Helpers

如果您不希望使用 HTML5 日期和時間輸入,Rails 提供替代日期和時間形式 helpers 呈現純選擇框。這些 helpers 為每個時間元件(例如年、月、日等)渲染一個選擇框。例如,如果我們有一個 @person model 物件,如:

@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 雜湊中將沒有包含完整日期的單個值。相反,會有幾個 values 具有特殊名稱,如 "birth_date(1i)"。 Active Record 知道如何根據 model 屬性的宣告型別將這些特殊命名的 values 組裝成完整的日期或時間。所以我們可以將 params[:person] 傳遞給例如Person.newPerson#update 就像表單使用單個欄位來表示完整日期一樣。

除了 date_select helper,Rails 還提供了 time_selectdatetime_select

4.1 為單個時間分量選擇框

Rails還提供helpers來呈現選擇框單個時間分量:select_yearselect_monthselect_dayselect_hourselect_minuteselect_second。這些 helpers 是“裸”方法,這意味著它們不會在表單構建器實例上呼叫。例如:

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

輸出一個選擇框,如:

<select name="party[year]" id="party_year">
  <option value="1994">1994</option>
  <option value="1995">1995</option>
  <option value="1996">1996</option>
  <option value="1997">1997</option>
  <option value="1998">1998</option>
  <option value="1999" selected="selected">1999</option>
  <option value="2000">2000</option>
  <option value="2001">2001</option>
  <option value="2002">2002</option>
  <option value="2003">2003</option>
  <option value="2004">2004</option>
</select>

對於這些 helpers 中的每一個,您可以指定日期或時間物件而不是數字作為預設的 value,並且將提取和使用適當的時間分量。

5 從任意物件集合中選擇

通常,我們希望從一組物件中以某種形式產生一組選擇。例如,當我們希望使用者從我們資料庫中的城市中進行選擇時,我們有一個 City model 像:

City.order(:name).to_a
# => [
# #<城市 id: 3, name: "Berlin">,
# #<City id: 1, name: "Chicago">,
# #<城市編號:2,名稱:“馬德里”>
#]

Rails 提供了 helpers,它可以從集合中產生選擇,而無需顯式迭代它。這些 helpers 通過在集合中的每個物件上呼叫指定的方法來確定每個選項的 value 和文字標籤。

5.1 collection_select Helper

要為我們的城市產生一個選擇框,我們可以使用 collection_select

<%= form.collection_select :city_id, City.order(:name), :id, :name %>

輸出:

<select name="city_id" id="city_id">
  <option value="3">Berlin</option>
  <option value="1">Chicago</option>
  <option value="2">Madrid</option>
</select>

使用 collection_select,我們首先指定 value 方法(上例中為 :id),然後指定文字標籤方法(上例中為 :name)。這與為 select helper 指定選項時使用的順序相反,其中文字標籤在前,value 在後。

5.2 collection_radio_buttons Helper

要為我們的城市產生一組單選按鈕,我們可以使用 collection_radio_buttons

<%= form.collection_radio_buttons :city_id, City.order(:name), :id, :name %>

輸出:

<input type="radio" name="city_id" value="3" id="city_id_3">
<label for="city_id_3">Berlin</label>
<input type="radio" name="city_id" value="1" id="city_id_1">
<label for="city_id_1">Chicago</label>
<input type="radio" name="city_id" value="2" id="city_id_2">
<label for="city_id_2">Madrid</label>

5.3 collection_check_boxes Helper

要為我們的城市產生一組複選框(允許使用者選擇多個),我們可以使用 collection_check_boxes

<%= form.collection_check_boxes :city_id, City.order(:name), :id, :name %>

輸出:

<input type="checkbox" name="city_id[]" value="3" id="city_id_3">
<label for="city_id_3">Berlin</label>
<input type="checkbox" name="city_id[]" value="1" id="city_id_1">
<label for="city_id_1">Chicago</label>
<input type="checkbox" name="city_id[]" value="2" id="city_id_2">
<label for="city_id_2">Madrid</label>

6 上傳檔案

一個常見的任務是上傳某種檔案,無論是人的照片還是包含要處理的資料的 CSV 檔案。可以使用 file_field helper 呈現檔案上傳欄位。

<%= form_with model: @person do |form| %>
  <%= form.file_field :picture %>
<% end %>

檔案上傳時要記住的最重要的事情是呈現的表單的 enctype 屬性必須設定為“multipart/form-data”。如果您在 form_with 中使用 file_field,這將自動完成。您還可以手動設定屬性:

<%= form_with url: "/uploads", multipart: true do |form| %>
  <%= file_field_tag :picture %>
<% end %>

需要注意的是,按照form_with約定,上述兩種形式的欄位名稱也會有所不同。也就是說,第一個表單中的欄位名稱將是 person[picture](可通過 params[:person][:picture] 訪問),而第二個表單中的欄位名稱將是 picture(可通過 params[:picture] 訪問)。

6.1 上傳的內容

params 雜湊中的物件是 ActionDispatch::Http::UploadedFile 的一個實例。以下程式碼段將上傳的檔案以與原始檔案相同的名稱儲存在 #{Rails.root}/public/uploads 中。

def upload
  uploaded_file = params[:picture]
  File.open(Rails.root.join('public', 'uploads', uploaded_file.original_filename), 'wb') do |file|
    file.write(uploaded_file.read)
  end
end

上傳檔案後,有許多潛在的任務,包括將檔案儲存在何處(在磁碟、Amazon S3 等上)、將它們與 models 關聯、調整影象檔案大小和產生縮圖等。 Active Storage 的目標在於協助完成這些任務。

7 自定義表單構建器

form_withfields_for 產生的物件是 ActionView::Helpers::FormBuilder 的一個實例。表單構建器封裝了為單個物件顯示錶單元素的概念。雖然您可以用通常的方式為表單編寫 helpers,但您也可以建立 ActionView::Helpers::FormBuilder 的子類,並在那裡新增 helpers。例如,

<%= form_with model: @person do |form| %>
  <%= text_field_with_label form, :first_name %>
<% end %>

可以替換為

<%= form_with model: @person, builder: LabellingFormBuilder do |form| %>
  <%= form.text_field :first_name %>
<% end %>

通過定義類似於以下內容的 LabellingFormBuilder 類:

class LabellingFormBuilder < ActionView::Helpers::FormBuilder
  def text_field(attribute, options={})
    label(attribute) + super
  end
end

如果您經常重用它,您可以定義一個 labeled_form_with helper 自動應用 builder: LabellingFormBuilder 選項:

def labeled_form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
  options.merge! builder: LabellingFormBuilder
  form_with model: model, scope: scope, url: url, format: format, **options, &block
end

使用的表單構建器還決定了當您執行以下操作時會發生什麼:

<%= render partial: f %>

如果 fActionView::Helpers::FormBuilder 的實例,那麼這將呈現 form 部分,將部分的物件設定為表單構建器。如果表單構建器屬於 LabellingFormBuilder 類,則將呈現 labelling_form 部分。

8 瞭解引數命名約定

表單中的 Value 可以位於 params 雜湊的頂層或巢狀在另一個雜湊中。例如,在一個標準的 create action 中,一個人 model,params[:person] 通常是這個人要建立的所有屬性的雜湊。 params 雜湊還可以包含陣列、雜湊陣列等。

從根本上說,HTML 表單不知道任何型別的結構化資料,它們產生的只是名稱-value 對,其中對只是普通字串。您在應用程式中看到的陣列和雜湊是 Rails 使用的一些引數命名約定的結果。

8.1 基本結構

兩種基本結構是陣列和雜湊。雜湊反映了用於訪問 params 中的 value 的語法。例如,如果表單包含:

<input id="person_name" name="person[name]" type="text" value="Henry"/>

params 雜湊將包含

{'person' => {'name' => 'Henry'}}

params[:person][:name] 將在 controller 中檢索提交的 value。

雜湊可以根據需要巢狀多個級別,例如:

<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>

將導致 params 雜湊被

{'person' => {'address' => {'city' => 'New York'}}}

通常 Rails 會忽略重複的引數名稱。如果引數名稱以一組空的正方形 brackets [] 結尾,則它們將累積在陣列中。如果您希望使用者能夠輸入多個電話號碼,您可以將其放在表單中:

<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] 成為包含輸入電話號碼的陣列。

8.2 組合它們

我們可以混合搭配這兩個概念。雜湊的一個元素可能是前一個示例中的陣列,或者您可以擁有一個雜湊陣列。例如,一個表單可以讓您通過重複以下表單片段來建立任意數量的地址

<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] 是一個帶有 keys line1line2city 的雜湊陣列。

然而,有一個限制:雖然雜湊可以任意巢狀,但只允許一級“陣列”。陣列通常可以用雜湊代替;例如,不是擁有 model 物件陣列,而是可以通過其 id、陣列索引或某些其他引數擁有 model 物件的雜湊值 keyed。

陣列引數與 check_box helper 的配合不佳。根據 HTML 規範,未選中的複選框不提交 value。然而,複選框總是提交 value 通常很方便。 check_box helper 通過建立一個同名的輔助隱藏輸入來偽造這一點。如果複選框未選中,則僅提交隱藏輸入,如果選中,則兩者都提交,但複選框提交的 value 優先。

8.3 fields_for Helper

假設我們想要為每個人的地址呈現一個包含一組欄位的表單。 fields_for helper 及其 :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'}}}}

Rails 知道所有這些輸入都應該是個人雜湊的一部分,因為您 在第一個表單產生器上呼叫 fields_for。通過指定 :index 選項 你告訴 Rails 而不是命名輸入 person[address][city] 它應該在地址和城市之間插入由 [] 包圍的索引。 這通常很有用,因為它很容易找到哪個地址記錄 應該修改。你可以傳遞具有其他意義的數字, 字串甚至 nil(這將導致建立一個數組引數)。

要建立更復雜的巢狀,您可以指定輸入的第一部分 顯式命名(上例中的 person[address]):

<%= fields_for 'person[address][primary]', address, index: address.id do |address_form| %>
  <%= address_form.text_field :city %>
<% end %>

將建立輸入,如

<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="Bologna" />

作為一般規則,最終輸入名稱是給 fields_for/form_with 的名稱、索引 value 和屬性名稱的串聯。您還可以將 :index 選項直接傳遞給 helpers,例如 text_field,但在表單構建器級別而不是在單個輸入控制元件上指定它通常不太重複。

作為快捷方式,您可以將 [] 附加到名稱並省略 :index 選項。這與指定 index: address.id 相同,因此

<%= fields_for 'person[address][primary][]', address do |address_form| %>
  <%= address_form.text_field :city %>
<% end %>

產生與前一個示例完全相同的輸出。

9 外部資源的表格

Rails' 表單 helpers 也可用於構建將資料釋出到外部資源的表單。但是,有時可能需要為資源設定 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 可能是不可取的。要不傳送 token,只需將 false 傳遞給 :authenticity_token 選項:

<%= form_with url: 'http://farfar.away/form', authenticity_token: false do %>
  Form contents
<% end %>

10 構建複雜的表單

許多應用程式超越了編輯單個物件的簡單表單。例如,在建立 Person 時,您可能希望允許使用者(在同一個表單上)建立多個地址記錄(家庭、工作等)。稍後編輯該人時,使用者應該能夠根據需要新增、刪除或修改地址。

10.1 設定 Model

Active Record 通過 accepts_nested_attributes_for 方法提供 model 級別的支援:

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= 方法,允許您建立、更新和(可選)銷燬地址。

10.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 %>

當 association 接受巢狀屬性時,fields_for 為 association 的每個元素渲染一次其塊。特別是,如果一個人沒有地址,則它什麼也不呈現。一種常見的模式是 controller 構建一個或多個空子項,以便向用戶顯示至少一組欄位。下面的示例將導致在新人員表單上呈現 2 組地址欄位。

def new
  @person = Person.new
  2.times { @person.addresses.build }
end

fields_for 產生一個表單構建器。引數的名稱將是什麼 accepts_nested_attributes_for 期望。例如,在建立使用者時 2 個地址,提交的引數如下所示:

{
  'person' => {
    'name' => 'John Doe',
    'addresses_attributes' => {
      '0' => {
        'kind' => 'Home',
        'street' => '221b Baker Street'
      },
      '1' => {
        'kind' => 'Office',
        'street' => '31 Spooner Street'
      }
    }
  }
}

:addresses_attributes 雜湊的 keys 並不重要,它們只需要每個地址不同即可。

如果關聯的物件已經儲存,fields_for 會自動產生一個隱藏輸入,其中包含已儲存記錄的 id。您可以通過將 include_id: false 傳遞給 fields_for 來禁用此功能。

10.3 Controller

像往常一樣,你需要 宣告允許的引數中 在將 controller 傳遞給 model 之前:

def create
  @person = Person.new(person_params)
  # ...
end

private
  def person_params
    params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
  end

10.4 移除物件

您可以通過將 allow_destroy: true 傳遞給 accepts_nested_attributes_for 來允許使用者刪除關聯物件

class Person < ApplicationRecord
  has_many :addresses
  accepts_nested_attributes_for :addresses, allow_destroy: true
end

如果物件的屬性雜湊包含 key _destroy 和 value 計算結果為 true(例如 1、'1'、true 或 'true'),則物件將被銷燬。 此表單允許使用者刪除地址:

<%= form_with model: @person do |form| %>
  Addresses:
  <ul>
    <%= form.fields_for :addresses do |addresses_form| %>
      <li>
        <%= addresses_form.check_box :_destroy %>
        <%= addresses_form.label :kind %>
        <%= addresses_form.text_field :kind %>
        ...
      </li>
    <% end %>
  </ul>
<% end %>

不要忘記更新 controller 中允許的引數以包括 _destroy 欄位:

def person_params
  params.require(:person).
    permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy])
end

10.5 防止空記錄

忽略使用者未填寫的欄位集通常很有用。您可以通過將 :reject_if proc 傳遞給 accepts_nested_attributes_for 來控制這一點。將使用表單提交的每個屬性雜湊呼叫此過程。如果 proc 返回 false,則 Active Record 將不會為該雜湊構建關聯物件。如果設定了 kind 屬性,下面的示例僅嘗試構建地址。

class Person < ApplicationRecord
  has_many :addresses
  accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?}
end

為方便起見,您可以改為傳遞 symbol :all_blank,這將建立一個過程,該過程將拒絕所有屬性為空的記錄,不包括 _destroy 的任何 value。

10.6 動態新增欄位

您可能希望僅在使用者單擊“新增新地址”按鈕時才新增它們,而不是提前呈現多組欄位。 Rails 不為此提供任何內建支援。在產生新的欄位集時,您必須確保關聯陣列的 key 是唯一的 - 當前 JavaScript 日期(自 epoch 以來的毫秒數)是一個常見的選擇。

11 在沒有表單產生器的情況下使用標籤 Helpers

如果您需要在表單構建器的上下文之外呈現表單欄位,Rails 為常見表單元素提供標籤 helpers。例如,check_box_tag

<%= check_box_tag "accept" %>

輸出:

<input type="checkbox" name="accept" id="accept" value="1" />

通常,這些 helpers 與它們的表單構建器具有相同的名稱,加上 _tag 字尾。有關完整列表,請參閱 FormTagHelper API 文件

12 使用 form_tagform_for

在 Rails 5.1 中引入 form_with 之前,它的功能曾經被拆分為 form_tagform_for。兩者現在都已被軟棄用。可以在 本指南的舊版本 中找到有關其使用的文件。

回饋

我們鼓勵您幫助提高本指南的品質。

如果您發現任何拼寫錯誤或資訊錯誤,請提供回饋。 要開始回饋,您可以閱讀我們的 回饋 部分。

您還可能會發現不完整的內容或不是最新的內容。 請務必為 main 新增任何遺漏的文件。假設是 非穩定版指南(edge guides) 請先驗證問題是否已經在主分支上解決。 請前往 Ruby on Rails 指南寫作準則 查看寫作風格和慣例。

如果由於某種原因您發現要修復的內容但無法自行修補,請您 提出 issue

關於 Ruby on Rails 的任何類型的討論歡迎提供任何文件至 rubyonrails-docs 討論區