更多資訊請參閱 rubyonrails.org:

Action View 表單輔助方法

表單是網頁應用程式中用於使用者輸入的常見介面。然而,表單標記的編寫和維護可能會很繁瑣,因為需要處理表單控制項、命名和屬性。Rails 透過提供視圖輔助方法來簡化此流程,這些方法會輸出 HTML 表單標記。本指南將協助您了解不同的輔助方法以及何時使用每種方法。

閱讀本指南後,您將了解

  • 如何建立基本表單,例如搜尋表單。
  • 如何使用基於模型的表單來建立和編輯特定的資料庫記錄。
  • 如何從多種資料類型產生選取方塊。
  • Rails 提供的日期和時間輔助方法。
  • 檔案上傳表單的不同之處。
  • 如何將表單發佈到外部資源,並指定設定 authenticity_token
  • 如何建立複雜的表單。

本指南並非所有可用表單輔助方法的完整清單。請參閱 Rails API 文件,以取得表單輔助方法及其引數的詳盡清單。

1 使用基本表單

主要的表單輔助方法是 form_with

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

當在沒有引數的情況下呼叫時,它會建立一個 HTML <form> 標籤,其中 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>

請注意,表單包含一個類型為 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 Hash。該名稱會是 params 中使用者針對該欄位輸入的值的索引鍵。

例如,如果表單包含 <%= form.text_field :query %>,則您可以使用 params[:query] 在控制器中取得此欄位的值。

在命名輸入時,Rails 使用某些慣例,這些慣例可讓您提交具有非純量值(例如陣列或 Hash)的參數,這些參數也可以在 params 中存取。您可以在本指南的「表單輸入命名慣例和 Params Hash」一節中閱讀更多相關資訊。如需這些輔助方法的精確使用方式詳細資訊,請參閱 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 Hash 中找到。如果使用者只勾選了「Biography」核取方塊,則 params Hash 將包含

{
  "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::ArticlesController 的表單,因此在更新的情況下會提交到 admin_article_path(@article)

如果您有多個層級的命名空間,則語法類似

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

有關 Rails 路由系統和相關慣例的更多資訊,請參閱Rails 由外而內的路由指南。

2.4 使用 PATCH、PUT 或 DELETE 方法的表單

Rails 框架鼓勵 RESTful 設計,這表示您應用程式中的表單除了 GETPOST 之外,還會發出 methodPATCHPUTDELETE 的請求。但是,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_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 的一個實例。可以擴展這個類別,為您的應用程式新增自訂輔助方法。

例如,如果您想在您的應用程式中顯示一個 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 partial,並將 partial 的物件設定為表單建構器。如果表單建構器是 LabellingFormBuilder 類別的,那麼將改為渲染 labelling_form partial。

表單建構器的自訂(例如 LabellingFormBuilder)會隱藏實作細節(並且對於上面的簡單範例來說可能顯得過於繁瑣)。基於您的表單使用自訂元素的頻率,在不同的自訂、擴展 FormBuilder 類別或建立輔助方法之間進行選擇。

8 表單輸入命名慣例和 params 雜湊

上面描述的所有表單輔助方法都有助於為表單元素產生 HTML,以便使用者可以輸入各種型別的輸入。您如何在 Controller 中存取使用者輸入的值?答案是 params 雜湊。您已經在上面的範例中看到了 params 雜湊。本節將更明確地介紹 params 雜湊中如何建構表單輸入的命名慣例。

params 雜湊可以包含陣列和雜湊陣列。值可以位於 params 雜湊的頂層,也可以巢狀在另一個雜湊中。例如,在 Person 模型的標準 create 動作中,params[:person] 將是 Person 物件的所有屬性的雜湊。

請注意,HTML 表單對使用者輸入資料沒有內在的結構,它們所產生的只是一些名稱-值字串對。您在應用程式中看到的陣列和雜湊是 Rails 使用的參數命名慣例的結果。

params 雜湊中的欄位需要在 Controller 中允許

8.1 基本結構

使用者輸入表單資料的兩個基本結構是陣列和雜湊。

雜湊鏡射了用於存取 params 中值的語法。例如,如果表單包含:

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

params 雜湊將包含:

{ "person" => { "name" => "Henry" } }

params[:person][:name] 將在 Controller 中檢索提交的值。

雜湊可以巢狀任意層級,例如:

<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 會為關聯的每個元素渲染一次其區塊。特別是,如果一個人沒有地址,則不會渲染任何內容。

一種常見的模式是讓 Controller 建構一個或多個空的子項目,以便向使用者顯示至少一組欄位。以下範例將導致在新的人員表單上渲染 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,它會拒絕所有屬性皆為空白的記錄,但 _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,但你仍然可以在某些程式碼庫中找到它們的使用。



返回頂部