本指南並非旨在成為可用的表單輔助工具及其參數的完整文件。請瀏覽 Rails API 文件 以取得所有可用輔助工具的完整參考。
1 處理基本表單
主要的表單輔助工具是 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 包含一個類型為 hidden
的 input
元素。這個 input
很重要,因為沒有它的非 GET 表單無法成功提交。名稱為 authenticity_token
的隱藏輸入元素是 Rails 的一項安全功能,稱為跨網站請求偽造保護,而且表單輔助程式會為每個非 GET 表單產生它(前提是已啟用此安全功能)。您可以在 保護 Rails 應用程式 指南中進一步了解這項功能。
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
會告訴表單在哪裡提出請求。不過,如下所述,您也可以將 Active Record 物件傳遞給表單。
對於每個表單輸入,都會從其名稱(以上面的範例為 "query"
)產生一個 ID 屬性。這些 ID 對於 CSS 造型或使用 JavaScript 操作表單控制項非常有用。
將「GET」用作搜尋表單的方法。這允許使用者將特定搜尋加入書籤並回到該搜尋。更一般來說,Rails 鼓勵您對動作使用正確的 HTTP 動詞。
1.2 產生表單元素的輔助程式
form_with
所產生的表單建構器物件提供許多用於產生表單元素(例如文字欄位、核取方塊和無線按鈕)的輔助方法。這些方法的第一個參數永遠是輸入的名稱。當表單提交時,名稱會與表單資料一起傳遞,並會傳遞到控制器中的 params
,其中包含使用者為該欄位輸入的值。例如,如果表單包含 <%= form.text_field :query %>
,那麼你就可以使用 params[:query]
在控制器中取得此欄位的值。
Rails 在命名輸入時會使用某些慣例,讓你可以提交具有非純量值(例如陣列或雜湊)的參數,這些值也可以在 params
中存取。你可以在本指南的 了解參數命名慣例 部分中進一步了解這些慣例。如需這些輔助工具的詳細使用方式,請參閱 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
的第一個參數是輸入的名稱。核取方塊的值(會顯示在 params
中的值)可以使用第三個和第四個參數選擇性指定。詳細資訊請參閱 API 文件。
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>
傳遞給 radio_button
的第二個參數是輸入的值。由於這兩個單選按鈕共用同一個名稱 (age
),使用者只能選取其中一個,而 params[:age]
將包含 "child"
或 "adult"
。
務必為核取方塊和單選按鈕加上標籤。它們會將文字與特定選項關聯起來,並透過擴充可點擊區域,讓使用者更容易按一下輸入。
1.3 其他值得注意的輔助工具
其他值得一提的表單控制項包括文字區域、隱藏欄位、密碼欄位、數字欄位、日期和時間欄位,以及更多。
<%= 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" />
隱藏輸入不會顯示給使用者,而是像任何文字輸入一樣儲存資料。它們裡面的值可以用 JavaScript 變更。
搜尋、電話、日期、時間、顏色、日期時間、日期時間-本地、月份、星期、URL、電子郵件、數字和範圍輸入是 HTML5 控制項。如果你需要讓應用程式在舊版瀏覽器中擁有相容的體驗,你將需要一個 HTML5 polyfill(由 CSS 和/或 JavaScript 提供)。對於這個問題,絕對 不乏解決方案,儘管目前流行的工具是 Modernizr,它提供了一個基於已偵測到的 HTML5 功能來新增功能的簡單方法。
如果你正在使用密碼輸入欄位(無論出於何種目的),你可能想要設定應用程式來防止記錄這些參數。你可以在 保護 Rails 應用程式 指南中了解這一點。
2 處理模型物件
2.1 將表單繫結到物件
form_with
的 :model
參數允許我們將表單建立器物件繫結到模型物件。這表示表單將限定在該模型物件中,而表單的欄位將填入該模型物件中的值。
例如,如果我們有一個像這樣的 @article
模型物件
@article = Article.find(42)
# => #<Article 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
的適當值。 - 表單欄位會自動填入
@article
中對應的值。 - 表單欄位名稱的範圍是
article[...]
。這表示params[:article]
會是一個包含所有這些欄位值的雜湊。您可以在本指南的 了解參數命名慣例 章節中進一步了解輸入名稱的重要性。 - 提交按鈕會自動給予適當的文字值。
慣例上,您的輸入會反映模型屬性。不過,它們不必如此!如果您需要其他資訊,您可以將其包含在表單中,就像使用屬性一樣,並透過 params[:article][:my_nifty_non_attribute_input]
存取它。
2.1.1 複合主鍵表單
表單也可以使用複合主鍵模型建立。在這種情況下,表單建立語法相同,但輸出略有不同。
假設有 @book
模型物件,其複合鍵為 [:author_id, :id]
@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 %>
輸出
<form action="/books/2_25" method="post" accept-charset="UTF-8" >
<input name="authenticity_token" type="hidden" value="..." />
<input type="text" name="book[title]" id="book_title" value="My book" />
<input type="submit" name="commit" value="Update Book" data-disable-with="Update Book">
</form>
請注意,產生的 URL 包含以底線分隔的 author_id
和 id
。提交後,控制器可以從參數中 萃取每個主鍵值,並像使用單一主鍵一樣更新記錄。
2.1.2 fields_for
輔助工具
fields_for
輔助程式會建立類似的繫結,但不會呈現 <form>
標籤。這可以用於在同一個表單中呈現其他模型物件的欄位。例如,如果您有具有關聯 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="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 依賴記錄識別
Article 模型可直接供應用程式使用者使用,因此 - 遵循使用 Rails 開發的最佳實務 - 您應該宣告它為資源
resources :articles
宣告資源有許多副作用。請參閱 Rails 路由從外而內 指南,以取得更多關於設定和使用資源的資訊。
在處理 RESTful 資源時,如果您依賴記錄識別,則對 form_with
的呼叫可能會變得容易許多。簡而言之,您只要傳遞模型實例,讓 Rails 找出模型名稱和其他部分即可。在以下這兩個範例中,長式和短式的結果相同
## Creating a new article
# long-style:
form_with(model: @article, url: articles_path)
# short-style:
form_with(model: @article)
## Editing an existing article
# long-style:
form_with(model: @article, url: article_path(@article), method: "patch")
# short-style:
form_with(model: @article)
請注意,不論記錄是新的或現有的,短式的 form_with
呼叫都非常方便,而且相同。記錄識別足夠聰明,可以透過詢問 record.persisted?
來找出記錄是否為新的。它也會選擇正確的路徑來提交,以及根據物件的類別來選擇名稱。
如果您有 單數資源,您需要呼叫 resource
和 resolve
,才能讓它與 form_with
搭配使用
resource :geocoder
resolve('Geocoder') { [:geocoder] }
當您在模型中使用 STI (單一表格繼承) 時,如果僅宣告其父類別為資源,您無法依賴子類別的記錄識別。您必須明確指定 :url
和 :scope
(模型名稱)。
2.2.1 處理命名空間
如果您已建立命名空間路由,form_with
也有個簡潔的速記法。如果您的應用程式有管理命名空間,則
form_with model: [:admin, @article]
將建立一個表單,提交至管理命名空間內的 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"
的隱藏輸入,設定為反映所需的方法,來解決此問題
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:
關鍵字覆寫已宣告的 method
屬性
<%= form_with url: "/posts/1", method: :patch do |form| %>
<%= form.button "Delete", formmethod: :delete, data: { confirm: "Are you sure?" } %>
<%= form.button "Update" %>
<% end %>
類似於 <form>
元素,大多數瀏覽器不支援覆寫透過 formmethod 宣告的表單方法,除了「GET」和「POST」。
Rails 透過結合 formmethod、value 和 name 屬性,模擬 POST 上的其他方法,來解決此問題。
<form accept-charset="UTF-8" action="/posts/1" method="post">
<input name="_method" type="hidden" value="patch" />
<input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
<!-- ... -->
<button type="submit" formmethod="post" name="_method" value="delete" data-confirm="Are you sure?">Delete</button>
<button type="submit" name="button">Update</button>
</form>
3 輕鬆建立選取方塊
HTML 中的選取方塊需要大量的標記 - 對於每個可選取的選項,都需要一個 <option>
元素。因此,Rails 提供了輔助方法來減輕此負擔。
例如,假設我們有一個供使用者選擇的城市清單。我們可以使用 select
輔助方法,如下所示
<%= 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>
值
<%= 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>
請注意,適當的選項已自動標記為 selected="selected"
。由於此選取方塊已繫結到模型,因此我們不需要指定 :selected
參數!
3.3 時區和國家選擇
若要利用 Rails 中的時區支援,您必須詢問使用者他們在哪個時區。這樣做需要從預先定義的 ActiveSupport::TimeZone
物件清單中產生選項,但您可以直接使用 time_zone_select
輔助程式,它已經包含此功能
<%= form.time_zone_select :time_zone %>
Rails 曾經 有個 country_select
輔助程式用於選擇國家,但這已被擷取到 country_select 外掛程式 中。
4 使用日期和時間表單輔助程式
如果您不希望使用 HTML5 日期和時間輸入,Rails 提供替代的日期和時間表單輔助程式,用於呈現純粹的選取方塊。這些輔助程式會為每個時間元件(例如年、月、日等)呈現一個選取方塊。例如,如果我們有一個 @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
hash 中不會有一個單一值包含完整的日期。相反地,會有幾個具有特殊名稱的值,例如 "birth_date(1i)"
。Active Record 知道如何根據模型屬性的宣告類型,將這些特殊命名的值組裝成完整的日期或時間。因此,我們可以將 params[:person]
傳遞給例如 Person.new
或 Person#update
,就像表單使用單一欄位來表示完整日期一樣。
除了 date_select
輔助程式外,Rails 還提供 time_select
和 datetime_select
。
4.1 個別時間元件的選取方塊
Rails 也提供幫手來呈現個別時間元件的選取方塊:select_year
、select_month
、select_day
、select_hour
、select_minute
,以及 select_second
。這些幫手是「單純」的方法,表示它們並非在表單產生器實例上呼叫。例如
<%= 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>
對於這些幫手中的每一個,您可以指定一個日期或時間物件,而不是數字作為預設值,然後會萃取並使用適當的時間元件。
5 從任意物件的集合中選擇
有時,我們想要從任意物件的集合中產生一組選項。例如,如果我們有一個 City
模型和對應的 belongs_to :city
關聯
class City < ApplicationRecord
end
class Person < ApplicationRecord
belongs_to :city
end
City.order(:name).map { |city| [city.name, city.id] }
# => [["Berlin", 3], ["Chicago", 1], ["Madrid", 2]]
然後,我們可以使用下列表單允許使用者從資料庫中選擇一個城市
<%= form_with model: @person do |form| %>
<%= form.select :city_id, City.order(:name).map { |city| [city.name, city.id] } %>
<% end %>
在為 belongs_to
關聯呈現欄位時,您必須指定外來金鑰的名稱(在上述範例中為 city_id
),而不是關聯本身的名稱。
不過,Rails 提供了幫手,可以在不需明確地反覆運算集合的情況下,從集合中產生選項。這些幫手透過呼叫集合中每個物件上指定的函式,來決定每個選項的值和文字標籤。
5.1 collection_select
幫手
若要產生一個選取方塊,我們可以使用 collection_select
<%= form.collection_select :city_id, City.order(:name), :id, :name %>
輸出
<select name="person[city_id]" id="person_city_id">
<option value="3">Berlin</option>
<option value="1">Chicago</option>
<option value="2">Madrid</option>
</select>
使用 collection_select
時,我們首先指定值方法(如上例中的 :id
),然後再指定文字標籤方法(如上例中的 :name
)。這與指定 select
輔助函式的選項時使用的順序相反,後者先指定文字標籤,再指定值。
5.2 collection_radio_buttons
輔助函式
若要產生一組無線按鈕,我們可以使用 collection_radio_buttons
<%= form.collection_radio_buttons :city_id, City.order(:name), :id, :name %>
輸出
<input type="radio" name="person[city_id]" value="3" id="person_city_id_3">
<label for="person_city_id_3">Berlin</label>
<input type="radio" name="person[city_id]" value="1" id="person_city_id_1">
<label for="person_city_id_1">Chicago</label>
<input type="radio" name="person[city_id]" value="2" id="person_city_id_2">
<label for="person_city_id_2">Madrid</label>
5.3 collection_check_boxes
輔助函式
若要產生一組核取方塊,例如支援 has_and_belongs_to_many
關聯,我們可以使用 collection_check_boxes
<%= form.collection_check_boxes :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 :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 等)、將檔案與模型關聯、調整影像檔案大小、產生縮圖等。Active Storage 旨在協助執行這些任務。
7 自訂表單產生器
form_with
和 fields_for
產生的物件是 ActionView::Helpers::FormBuilder
的執行個體。表單產生器封裝了為單一物件顯示表單元素的概念。雖然你可以用一般的方式為表單撰寫輔助程式,但你也可以建立 ActionView::Helpers::FormBuilder
的子類別,並將輔助程式新增到其中。例如,假設你在應用程式中定義了一個名為 text_field_with_label
的輔助程式方法,如下所示
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 %>
可以用下列方式取代
<%= 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
輔助程式,自動套用 builder: LabellingFormBuilder
選項
module ApplicationHelper
def labeled_form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
options[:builder] = LabellingFormBuilder
form_with model: model, scope: scope, url: url, format: format, **options, &block
end
end
使用的表單產生器也會決定在你執行下列動作時會發生什麼事
<%= render partial: f %>
如果 f
是 ActionView::Helpers::FormBuilder
的執行個體,這將會呈現 form
部分,將部分的物件設定為表單產生器。如果表單產生器是 LabellingFormBuilder
類別,則會呈現 labelling_form
部分。
8 了解參數命名慣例
表單中的值可以位於 params
hash 的頂層,或巢狀在另一個 hash 中。例如,在 Person 模型的標準 create
動作中,params[:person]
通常會是所有要建立的人員屬性的 hash。params
hash 也包含陣列、hash 陣列等。
基本上 HTML 表單不知道任何結構化資料,它們產生的只是名稱-值對,其中對只是純文字字串。在應用程式中看到的陣列和雜湊是 Rails 使用的一些參數命名慣例的結果。
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]
成為包含輸入電話號碼的陣列。
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]
成為一個雜湊陣列,其鍵為 line1
、line2
和 city
。
但有一個限制:雜湊可以任意巢狀,但只允許一層「陣列性」。陣列通常可以用雜湊取代;例如,與其擁有模型物件陣列,可以擁有以其 ID、陣列索引或其他參數為鍵的模型物件雜湊。
陣列參數與 check_box
輔助程式不搭配。根據 HTML 規範,未勾選的核取方塊不會提交任何值。然而,核取方塊經常方便地提交一個值。check_box
輔助程式透過建立一個具有相同名稱的輔助隱藏輸入來偽造此行為。如果核取方塊未勾選,則只會提交隱藏輸入,如果已勾選,則兩個都會提交,但核取方塊提交的值優先。
8.3 fields_for
輔助程式的 :index
選項
假設我們想要為每個人的地址組呈現一個包含一組欄位的表單。具有 :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>
的輸出,這將產生看起來像
{
"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 記錄。
您可以透過 :index
選項傳遞其他有意義的數字或字串。您甚至可以傳遞 nil
,這將產生一個陣列參數。
若要建立更複雜的巢狀結構,您可以明確指定輸入名稱的前導部分。例如
<%= fields_for 'person[address][primary]', address, index: address.id do |address_form| %>
<%= address_form.text_field :city %>
<% end %>
將建立類似下列的輸入
<input id="person_address_primary_23_city" name="person[address][primary][23][city]" type="text" value="Paris" />
您也可以將 :index
選項直接傳遞給輔助程式,例如 text_field
,但通常在表單建立器層級指定此選項比在個別輸入欄位中指定更不重複。
一般來說,最終的輸入名稱會是提供給 fields_for
/ form_with
的名稱、:index
選項值和屬性名稱的串接。
最後,作為捷徑,您可以將 "[]"
附加到給定的名稱,而不是為 :index
指定 ID(例如 index: address.id
)。例如
<%= fields_for 'person[address][primary][]', address do |address_form| %>
<%= address_form.text_field :city %>
<% end %>
產生的輸出與我們的原始範例完全相同。
9 表單傳送至外部資源
Rails 的表單輔助程式也可以用來建立表單,以便將資料傳送到外部資源。不過,有時可能需要為資源設定 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 %>
10 建立複雜表單
許多應用程式會擴充到編輯單一物件的簡單表單之外。例如,在建立 Person
時,您可能希望允許使用者(在同一個表單中)建立多筆地址記錄(家、公司等)。在稍後編輯該人時,使用者應該能夠視需要新增、移除或修正地址。
10.1 設定模型
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=
方法,讓你可以建立、更新,以及(選擇性地)刪除地址。
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 %>
當一個關聯接受巢狀屬性時,fields_for
會針對關聯的每個元素渲染一次區塊。特別是,如果一個人沒有地址,它就不會渲染任何東西。一個常見的模式是控制器會建立一個或多個空的子項,以便至少向使用者顯示一組欄位。以下範例會在新的個人表單上渲染 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
hash 中鍵的實際值並不重要;但是它們必須是整數字串,且每個地址都不同。
如果關聯的物件已經儲存,fields_for
會自動產生一個隱藏輸入,其中包含儲存記錄的 id
。你可以透過傳遞 include_id: false
給 fields_for
來停用這項功能。
10.3 控制器
和往常一樣,你需要在將允許的參數傳遞給模型之前,在控制器中 宣告允許的參數
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
如果物件的屬性雜湊包含鍵值 _destroy
,且其值評估為 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 %>
別忘了更新控制器中允許的參數,以包含 _destroy
欄位
def person_params
params.require(:person).
permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy])
end
10.5 防止產生空記錄
通常會忽略使用者未填寫的欄位組。你可以透過將 :reject_if
程序傳遞給 accepts_nested_attributes_for
來控制這項功能。此程序將會呼叫表單提交的每個屬性雜湊。如果程序傳回 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
作為一種便利的方式,它將會建立一個程序,用於拒絕所有屬性皆為空白(不包括 _destroy
的任何值)的記錄。
10.6 動態新增欄位
與事先呈現多組欄位不同,你可能希望僅在使用者按一下「新增地址」按鈕時才新增欄位。Rails 沒有提供任何內建支援。在產生新欄位組時,你必須確保關聯陣列的鍵值是唯一的 - 目前的 JavaScript 日期(自 Unix 時間 起的毫秒數)是一個常見的選擇。
11 在沒有表單產生器的情況下使用標籤輔助程式
如果你需要在表單產生器的內容之外呈現表單欄位,Rails 會針對常見的表單元素提供標籤輔助程式。例如,check_box_tag
<%= check_box_tag "accept" %>
輸出
<input type="checkbox" name="accept" id="accept" value="1" />
一般來說,這些輔助程式的名稱與其表單產生器對應項相同,並加上 _tag
字尾。如需完整清單,請參閱 FormTagHelper
API 文件。
12 使用 form_tag
和 form_for
在 Rails 5.1 中引入 form_with
之前,它的功能會分割成 form_tag
和 form_for
。現在這兩個都已逐漸淘汰。可以在 本指南的舊版本 中找到它們的使用說明。
回饋
我們鼓勵您協助提升本指南的品質。
如果您發現任何錯字或事實錯誤,請協助我們修正。首先,您可以閱讀我們的 文件貢獻 部分。
您也可能會發現內容不完整或已過時。請務必為 main 新增任何遺漏的文件。請先查看 Edge Guides,確認問題是否已在 main 分支中修正。請查看 Ruby on Rails 指南指南,了解風格和慣例。
如果您發現需要修正的地方,但無法自行修改,請 開啟問題。
最後,我們非常歡迎在 Ruby on Rails 官方論壇 上針對 Ruby on Rails 文件進行任何討論。