更多內容 rubyonrails.org: 更多內容

Action View 表單輔助方法

表單是 Web 應用程式裡,供使用者輸入的基本介面。然而表單的各種繁雜的名稱與屬性,使得撰寫表單很快便變得繁瑣與難以維護。Rails 透過 Action View 提供輔助方法,來簡化表單的撰寫。但各種輔助方法的應用場景不盡相同,開發者需要知道輔助方法之間的差異,才能完善的使用這些輔助方法。

讀完本篇,您將了解:

本篇不是表單輔助方法完整的文件,完整文件請參考 Rails API 文件

1 處理簡單的表單

最基本的表單輔助方法是 form_tag

<%= form_tag do %>
  Form contents
<% end %>

像這樣不傳參數呼叫時,會建立出 <form> 標籤。按下送出時,會對目前的頁面做 POST。舉例來說,假設目前的頁面是 /home/index,上例產生的 HTML 會像是(加了某些斷行提高可讀性):

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

注意到 HTML 裡有個隱藏的 input,這個隱藏的 input 很重要,沒有這個 input 表單便無法順利送出。第一個 name 屬性為 utf8input,強制瀏覽器正確採用表單指定的編碼,所有 HTTP 動詞為 GET 或 POST 表單,Rails 都會產生這個 input。第二個 name 屬性為 authenticity_tokeninput,是 Rails 內建用來防止 CSRF (cross-site request forgery protection) 攻擊的安全機制,任何非 GET 的表單,Rails 都會產生一個這樣的 input(安全機制有啟用的話)。詳情請閱讀安全指南

1.1 通用搜索表單

Web 世界最基本的表單之一是「搜索表單」。通常由以下元素組成:

  • 一個有 GET 動詞的表單
  • 供輸入的文字欄位
  • 輸入有標籤
  • 送出元素

要建立這樣的搜索表單,可以使用 form_taglabel_tagtext_field_tag 以及 submit_tag

<%= form_tag("/search", method: "get") do %>
  <%= label_tag(:q, "Search for:") %>
  <%= text_field_tag(:q) %>
  <%= submit_tag("Search") %>
<% end %>

會產生出如下 HTML:

<form accept-charset="UTF-8" action="/search" method="get">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <label for="q">Search for:</label>
  <input id="q" name="q" type="text" />
  <input name="commit" type="submit" value="Search" />
</form>

每個表單的輸入 input,都會根據 name 屬性來產生 ID 屬性(上例為 q)。有了 ID,CSS 要新增樣式、或 JavaScript 要操作表單都很方便。

除了 text_field_tagsubmit_tag 之外,每個表單元素都有對應的輔助方法。

搜索表單永遠使用 GET 動詞。這允許使用者可以把搜索結果加入書籤,之後便能透過書籤瀏覽。Rails 普遍鼓勵使用正確的 HTTP 動詞。

1.2 呼叫表單輔助方法同時傳多個 Hash

form_tag 輔助方法接受 2 個參數:表單送出的目標路徑和 Hash 選項。Hash 選項用來指定表單所使用的方法,以及其它 HTML 選項,如指定表單的 class

link_to 輔助方法類似,路徑不需要是字串。可以是 Rails Router 看的懂的 URL Hash,Rails 的路由機制會把 Hash 轉換為有效的 URL。但由於傳給 form_tag 的兩個參數都是 Hash 時,同時指定會碰到下例所演示的問題:

form_tag(controller: "people", action: "search", method: "get", class: "nifty_form")
# => '<form accept-charset="UTF-8" action="/people/search?class=nifty_form&amp;method=get" method="post">'

這裡 methodclass 變成了 URL 的查詢字串,因為 Rails 將這四個參數認成了一個 Hash。需要把第一組 Hash 放在大括號裡(或明確使用大括號亦可),才會產生出正確的 HTML:

form_tag({ controller: "people", action: "search" }, method: "get", class: "nifty_form")
# => '<form accept-charset="UTF-8" action="/people/search" class="nifty_form" method="get">'

1.3 產生表單元素的輔助方法

Rails 提供一系列的輔助方法,用來產生表單元素,像是多選方框(checkboxes)、文字欄位(text fields)以及單選按鈕(radio button)。名字以 _tag 結尾的輔助方法(譬如 text_field_tagcheck_box_tag)只會產生一個 <input> 元素。這些輔助方法的第一個參數都是 input 的名稱(name)。表單送出時,name 會與表單資料一起送出,使用者輸入的資料會存在 params Hash 裡,可在 Controller 取用。舉個例子,若表單的 input<%= text_field_tag(:query) %>,則可在 Controller 用 params[:query] 來獲得使用者的輸入。

Rails 使用特定的慣例來命名 input,使得送出像是陣列與 Hash 的值,也可以在 params 裡取用。了解更多可閱讀本文第七章:理解參數命名慣例。這些輔助方法更精確的用途,請參考 API 文件

1.3.1 多選方框

多選方框是一種表單控件,給使用者一組可啟用停用的選項:

<%= check_box_tag(:pet_dog) %>
<%= label_tag(:pet_dog, "I own a dog") %>
<%= check_box_tag(:pet_cat) %>
<%= label_tag(:pet_cat, "I own a cat") %>

會產生出如下 HTML:

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

checkbox_box_tag 第一個參數是 inputname,第二個參數通常是 inputvalue,當該多選方框被選中時,value 會被包含在表單資料一併送出,便可在 params 取用。

1.3.2 Radio Buttons

單選按鈕與多選方框類似,但每個選項是互斥的(也就是只能選一個):

<%= radio_button_tag(:age, "child") %>
<%= label_tag(:age_child, "I am younger than 21") %>
<%= radio_button_tag(:age, "adult") %>
<%= label_tag(:age_adult, "I'm over 21") %>

會產生出如下 HTML:

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

check_box_tag 類似,radio_button_tag 的第二個參數同樣是 inputvalue。因為這兩個單選按鈕的 name 都是 age,使用者只能選一個, params[:age] 的值會是 "child""adult"

永遠記得幫多選方框與單選按鈕加上 labellabel 可以為特定的輸入新增說明文字,也會加大可按範圍,讓使用者更容易選中。

1.4 其它相關輔助方法

其它值得一提的表單控件有:textareas、password fields、hidden fields、search fields、telephone fields、date fields、time fields、color fields、datetime fields、datetime-local fields、month fields、week fields、url fields、email fields、number fields 以及 range fields:

<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
<%= password_field_tag(:password) %>
<%= hidden_field_tag(:parent_id, "5") %>
<%= search_field(:user, :name) %>
<%= telephone_field(:user, :phone) %>
<%= date_field(:user, :born_on) %>
<%= datetime_field(:user, :meeting_time) %>
<%= datetime_local_field(:user, :graduation_day) %>
<%= month_field(:user, :birthday_month) %>
<%= week_field(:user, :birthday_week) %>
<%= url_field(:user, :homepage) %>
<%= email_field(:user, :address) %>
<%= color_field(:user, :favorite_color) %>
<%= time_field(:task, :started_at) %>
<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>
<%= range_field(:product, :discount, in: 1..100) %>

產生的 HTML:

<textarea id="message" name="message" cols="24" rows="6">Hi, nice site</textarea>
<input id="password" name="password" type="password" />
<input id="parent_id" name="parent_id" type="hidden" value="5" />
<input id="user_name" name="user[name]" type="search" />
<input id="user_phone" name="user[phone]" type="tel" />
<input id="user_born_on" name="user[born_on]" type="date" />
<input id="user_meeting_time" name="user[meeting_time]" type="datetime" />
<input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" />
<input id="user_birthday_month" name="user[birthday_month]" type="month" />
<input id="user_birthday_week" name="user[birthday_week]" type="week" />
<input id="user_homepage" name="user[homepage]" type="url" />
<input id="user_address" name="user[address]" type="email" />
<input id="user_favorite_color" name="user[favorite_color]" type="color" value="#000000" />
<input id="task_started_at" name="task[started_at]" type="time" />
<input id="product_price" max="20.0" min="1.0" name="product[price]" step="0.5" type="number" />
<input id="product_discount" max="100" min="1" name="product[discount]" type="range" />

隱藏的 input 不會顯示給使用者,但和其它文字輸入一樣可以存放資料。隱藏的 input 的值可以使用 JavaScript 來修改。

search、telephone、date、time、color、datetime、datetime-local、month、week、URL、email、number 以及 range inputs 是 HTML5 控件。若需要應用程式在舊版的瀏覽器也有一致的瀏覽體驗,需要使用 HTML5 polyfill(由 CSS 或 JavaScript 提供)。雖然 polyfill 很好,但目前主流工具是 Modernizr 以及 yepnope,這兩個工具提供一種簡單的方式,用來新增 HTML5 的新功能。

若使用了 password input fields(不論用途),輸入的值可能不要記錄在 Log。詳細做法請參考安全指南:logging 一節

2 處理 Model 物件

2.1 Model 物件輔助方法

表單通常拿來新建或編輯 Model 物件。可以使用 *_tag 這些輔助方法來處理,但太繁瑣了,參數名稱和預設值都得正確才行。Rails 提供更多方便的輔助方法(沒有 _tag 字尾),像是 text_fieldtext_area 等,專門用來處理 Model 物件。

這些輔助方法的第一個參數是實體變數的名字,第二個參數是要對實體變數呼叫的方法名稱(通常是屬性)。Rails 會將呼叫的結果存成 inputvalue,並幫你給 inputname 取個好名字。假設 Controller 已經定義了 @person@person.nameHenry,則:

<%= text_field(:person, :name) %>

會產生

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

送出表單時,使用者的輸入會存在 params[:person][:name]params[:person] 可傳給 Person.new;若 @personPerson 的實體,則可傳給 Person#update。通常第二個參數是屬性名稱,實在是太常用了,通常可省略不寫,只要該物件有實作 namename= 方法即可。

第一個參數必須是實體變數的“名稱”,如::person"person",而不是傳實際的實體物件進去。

Rails 還提供了用來顯示與 Model 物件驗證錯誤訊息的輔助方法。這些方法在 Active Record 驗證一文裡詳細說明。

2.2 將表單綁定到物件

雖然這些去掉 _tag 的輔助方法很方便,但還不夠好。若 Person 有很多屬性時,得一直重複傳入要編輯的物件名稱,來生成對應的表單。Rails 提供了 form_for,用來將表單綁定至 Model 的物件。

假設有處理文章的 Controller app/controllers/articles_controller.rb

def new
  @article = Article.new
end

對應的 View app/views/articles/new.html.erb,使用了 form_for 看起來會像是這樣:

<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
  <%= f.text_field :title %>
  <%= f.text_area :body, size: "60x12" %>
  <%= f.submit "Create" %>
<% end %>

有幾件要說明的事情:

  • @article 是實際被編輯的物件。 & form_for 接受一個 Hash 選項。路由相關選項放在 :url 傳入,HTML 相關選項放在 html: 選項傳入。還可以提供 :namespace 選項,用來確保 ID 的唯一性。namespace 的值會自動成為 HTML ID 的前綴。

  • form_for 方法會產生一個 表單構造器(Form Builder) 物件(f 變數)。

  • 輔助方法皆在 f,表單構造器上呼叫。

產生的 HTML 為:

<form accept-charset="UTF-8" action="/articles/create" method="post" class="nifty_form">
  <input id="article_title" name="article[title]" type="text" />
  <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
  <input name="commit" type="submit" value="Create" />
</form>

傳給 form_for 的名稱會成為在 params 取用表單數值的鍵。上例名稱為 article,因此所有的 name 都是 article[attribute_name]。在 create 動作裡的 params[:article] 會是有著 :title:body 鍵的 Hash。輸入名稱的重要性,可參閱理解參數命名慣例一節。

對表單構造器呼叫輔助方法,和對 Model 物件上呼叫的效果相同。但不需要指定編輯的物件,因為編輯的物件即表單構造器。

使用 fields_for 輔助方法也可以達到上面的效果,但不會產生出 <form> 標籤。同個表單用來編輯多個 Model 物件時很有用。譬如 Person Model 有個關聯的 ContactDetail Model,下面的表單可以同時建立初兩個 Model 的物件:

<%= form_for @person, url: {action: "create"} do |person_form| %>
  <%= person_form.text_field :name %>
  <%= fields_for @person.contact_detail do |contact_details_form| %>
    <%= contact_details_form.text_field :phone_number %>
  <% end %>
<% end %>

會產生出以下輸出:

<form accept-charset="UTF-8" action="/people/create" class="new_person" id="new_person" method="post">
  <input id="person_name" name="person[name]" type="text" />
  <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" />
</form>

fields_for 給出的物件也是個表單構造器,和 form_for 一樣(實際上 form_for 內部呼叫的是 fields_for)。

2.3 記錄自動識別技術

如使用者可以直接操作 Article Model,則依據 Rails 開發的最佳實踐,應將 Article 視為一個資源

resources :articles

宣告成資源有許多副作用。見 Rails 路由:深入淺出〈資源式路由:Rails 的預設路由〉來瞭解更多關於設定與使用資源的資訊。

處理 RESTful 資源時,若用了記錄自動識別技術,則呼叫 form_for 便很容易使用。簡單的說,可以只把 Model 實體傳進去,Rails 會自己處理好 Model 名稱與其它內容:

## Creating a new article
# long-style:
form_for(@article, url: articles_path)
# same thing, short-style (record identification gets used):
form_for(@article)

## Editing an existing article
# long-style:
form_for(@article, url: article_path(@article), html: {method: "patch"})
# short-style:
form_for(@article)

無論記錄是否存在,使用簡短風格的 form_for 呼叫都長得一樣。記錄自動識別技術很聰明,會對紀錄呼叫 record.new_record? 來檢查是否是新紀錄。也能根據物件的類別,選出正確的送出路徑與名稱。

Rails 也會自動幫表單設定適當的 classid。新增文章的表單 idclass 可能是 new_article。若編輯 ID 為 23 的文章,class 則會設為 edit_articleid 設為 edit_article_23。為求行文簡潔,這些屬性後文忽略不計。

使用 STI(單表繼承)時,如父類宣告為資源,則子類便不能依賴記錄自動識別技術。必須要明確指定 Model 的名稱、:url 以及 :method

2.3.1 處理命名空間

若建立的路由有命名空間,form_for 也有對應的簡寫形式。假設應用程式有 admin 命名空間:

form_for [:admin, @article]

會在 admin 命名空間裡,建立出對 ArticlesController 提交的表單,送出結果到 admin_article_path(@article)(假設是更新文章的情況)。若有多層命名空間,語法類推:

form_for [:admin, :management, @article]

關於 Rails 路由系統的更多資訊以及有關的慣例,請參見:[Rails 路由:深入淺出]。

2.4 PATCH、PUT、DELETE 表單的工作原理

Rails 框架鼓勵用 RESTful 風格來設計應用程式,這表示會用到許多 “PATCH” 與 “DELETE” 請求(而不只是 GET 與 POST)。但多數瀏覽器 只支援 用 GET 或 POST 來送出表單。

Rails 透過使用 POST 請求模擬出其它 HTTP 方法來解決這個問題。在表單裡新增一個 name_methodvalue 為真正希望使用的方法名稱的隱藏輸入:

form_tag(search_path, method: "patch")

輸出:

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

解析 POST 過來的資料時,Rails 會將特殊的 _method 參數考慮進去,以 value 的值作為 HTTP 方法(上例為 “PATCH”)。

3 輕鬆製作下拉式選單

HTML 的下拉選單需要大量的 Markup(一個選項就要一個 OPTION 元素),非常適合動態產生這些選項。

以下是可能的 Markup:

<select name="city_id" id="city_id">
  <option value="1">Lisbon</option>
  <option value="2">Madrid</option>
  ...
  <option value="12">Berlin</option>
</select>

這裡有一組給使用者選擇的城市清單。應用程式內部只需要處理各選項的 ID,因此把 optionvalue 設為 ID。接著看 Rails 如何化繁為簡。

3.1 Select 與 Option 標籤

最通用的輔助方法是 select_tag,從名字就可以看出來,是用來產生封裝了選項字串的 select 標籤:

<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>

這只是剛開始而已,上面把字串封裝在 select_tag 裡面,無法動態生成 option 標籤,於是有了 options_for_select

<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %>

輸出:

<option value="1">Lisbon</option>
<option value="2">Madrid</option>
...

options_for_select 的第一個參數是選項組成的嵌套陣列,每個選項有兩個元素,選項文字(城市名稱)與選項數值(城市 ID)。選項數值會送給 Controller 處理。通常會是資料庫對應物件的 ID,但也不強迫一定要用 ID。

瞭解之後,可以結合 select_tagoptions_for_select 來實作完整的 Markup:

<%= select_tag(:city_id, options_for_select(...)) %>

options_for_select 的第二個參數可以設定預設選項。

<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %>

輸出:

<option value="1">Lisbon</option>
<option value="2" selected="selected">Madrid</option>
...

Rails 在發現屬性值與 options_for_select 第二個參數的值相同時,便會給該選項新增 selected 屬性。

options_for_select 的第二個參數,必須與需要選中選項的值完全相等。特別注意若該選項的值是整數 2options_for_select 第二個參數的值便不可以是 "2",必須是 2。需要注意的是從 params 取出的數值都是字串。

可以用 Hash 給每個選項加上任意的屬性:

<%= options_for_select(
  [
    ['Lisbon', 1, { 'data-size' => '2.8 million' }],
    ['Madrid', 2, { 'data-size' => '3.2 million' }]
  ], 2
) %>

輸出:

<option value="1" data-size="2.8 million">Lisbon</option>
<option value="2" selected="selected" data-size="3.2 million">Madrid</option>
...

3.2 處理 Models 的下拉選單

多數情況下表單控件與特定的資料庫模型綁在一起,可能會好奇 Rails 有沒有針對 Model 提供 的輔助方法可用呢?答案是有。針對 Model 的輔助方法和其它的表單輔助方法相同,名稱去掉 select_tag_tag 即可:

# controller:
@person = Person.new(city_id: 2)

# view:
<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>

注意 select 的第三個參數,由選項組成的陣列,跟傳給 options_for_select 的參數一樣。好處是無需煩惱預選的城市是那個,Rails 會自己去讀取 @person.city_id 來決定預選城市是那個。

和其它輔助方法一樣,對表單構造器也可以使用,語法是:

# select on a form builder
<%= f.select(:city_id, ...) %>

select 也接受區塊:

<%= f.select(:city_id) do %>
  <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
    <%= content_tag(:option, c.first, value: c.last) %>
  <% end %>
<% end %>

上例 Person 與 City Model 存在 belongs_to 關係,在使用 select 時必須傳入 foreign key,否則會報這個錯誤:ActiveRecord::AssociationTypeMismatch

若使用 select (或其它類似的輔助方法,像是 collection_selectselect_tag)來設定 belongs_to 關聯,則必須傳入外鍵的名稱(上例須傳入 city_id),而不是關聯名稱。若指定的是 city 而不是 city_id,把 params 傳給 Person.newPerson.update 時,Active Record 會拋出錯誤: ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)。換句話說也就是表單輔助方法只能編輯屬性。應該要注意讓使用者直接編輯外鍵,所存在的安全性風險。

3.3 從任何物件集合產生選項

options_for_select 來產生選項,需要先建立陣列,陣列裡有選項文字與數值。但要是已經有了 City Model(假設是個繼承自 Active Record 的 Model),想要直接從 Model 的實體產生出這些選項該怎麼做?解法之一是迭代這些物件,產生出嵌套的陣列:

<% cities_array = City.all.map { |city| [city.name, city.id] } %>
<%= options_for_select(cities_array) %>

這個方法完美可行,但 Rails 提供更簡潔的解法:options_from_collection_for_select。這個輔助方法接受一組任意物件的集合和兩個額外的參數:用來讀取選項 數值文字 的方法名稱。

<%= options_from_collection_for_select(City.all, :id, :name) %>

從名字可以看出來,options_from_collection_for_select 只會產生出 option 標籤。要產生出會動的 select,需要與 select_tag 一起使用。就跟 options_for_select 需要與 select_tag 同時使用的情況相同。在處理 Model 物件時,select 結合了 select_tagoptions_for_selectcollection_select 則結合了 select_tagoptions_from_collection_for_select

<%= collection_select(:person, :city_id, City.all, :id, :name) %>

和其他輔助方法一樣,若想在 form builder 的作用在 @person 物件裡使用 collection_select,應當這麼寫:

<%= f.collection_select(:city_id, City.all, :id, :name) %>

複習一下,options_from_collection_for_selectcollection_select 的關係,和 options_for_selectselect 之間的關係一樣。

傳給 options_for_select 的陣列需要先傳 name,再傳 id;而 options_from_collection_for_select 則是先傳 id,再傳 name

3.4 時區與國家選單

要完善利用 Rails 支援的時區功能,首先要詢問使用者所在的時區為何。要詢問時區得先產生所有的時區選項,再傳給 collection_select 來產生選單,但可以直接使用 time_zone_select 輔助方法,已經包裝好了:

<%= time_zone_select(:person, :time_zone) %>

還有一個 time_zone_options_for_select 輔助方法,這個的客製性更高。關於這個方法的使用方法,請查閱 API 文件,來了解 time_zone_selecttime_zone_options_for_select 可用的參數有那些。

Rails 曾有過 country_select 輔助方法,用來選擇國家。但已經抽出來變成 country_select 套件。使用這個套件時,請注意清單裡的國家名稱,有些國家有列在清單裡、有些沒有、有些有爭議。這也是為什麼 Rails 不內建這個功能的原因。

4 日期與時間的表單輔助方法

可選擇不用會產生出 HTML5 日期與時間輸入欄位的輔助方法,而使用替代的日期與時間輔助方法。這些日期與時間方法和其它的表單輔助方法主要有以下兩點不同:

  • 日期與時間不代表單一的 input 元素,而是多個 input,每個有每個的用途(年份、月份、日等)。所以 params 裡的日期與時間不會是個單獨的數值。
  • 其它的表單輔助方法用 _tag 來區分,這個方法是個準方法,或是針對 Model 物件的輔助方法。而日期與時間的輔助方法有:select_dateselect_time 以及 select_datetime 是準方法;而 date_selecttime_select 以及 datetime_select 則是針對 Model 物件的輔助方法。

準方法和針對 Model 物件的方法,都會針對不同的時間單位(年、月、日等)來建出選單。

4.1 準方法

select_* 家族的輔助方法,第一個參數接受的是日期的實體,DateTimeDateTime,用來作為目前選中的日期。第一個參數可以忽略,預設會選擇當下日期。舉個例子:

<%= select_date Date.today, prefix: :start_date %>

輸出(省略選項數值,保持簡單):

<select id="start_date_year" name="start_date[year]"> ... </select>
<select id="start_date_month" name="start_date[month]"> ... </select>
<select id="start_date_day" name="start_date[day]"> ... </select>

以上的輸入送出時會存在 params[:start_date],以散列表的形式儲存,鍵有 :year:month 以及 day。要獲得實際的 TimeDate 物件,可以將時間各個單位取出來,傳給適當的建構子,參考下例:

Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)

上例的 :prefix 選項為 :start_date,是時間單位存在 params 的鍵名。沒給的話預設值是 date

4.2 給 Model 物件用的方法

select_date 與 Active Record 配合的不好,因為 Active Record 期望每個 params 的元素,都對應到一個屬性。而 Model 物件的日期與時間輔助方法,會採用特殊的名稱來送出參數。Active Record 看到這些特殊名稱的參數時,便知道要將這些參數結合起來,傳給欄位類型的建構子。譬如:

<%= date_select :person, :birth_date %>

輸出(省略選項數值,保持簡單):

<select id="person_birth_date_1i" name="person[birth_date(1i)]"> ... </select>
<select id="person_birth_date_2i" name="person[birth_date(2i)]"> ... </select>
<select id="person_birth_date_3i" name="person[birth_date(3i)]"> ... </select>

產生出來的 params

{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}

params 傳給 Person.newPerson.update 時,Active Record 會注意到這些參數名稱,要一起傳進來,來產生 birth_date 屬性,並根據字尾的資訊(ni),來決定傳給 Date.civil 的順序。

4.3 通用選項

這兩個家族的輔助方法,內部使用同一組核心功能,來產生 select 標籤,因此接受的選項大致相同。特別要提 Rails 預設會產生前後五年的年份。若這個範圍不夠用,:start_year 以及 :end_year 選項可以修改。可用選項更詳細的清單,請參考 API 文件

經驗法則表示,處理 Model 物件使用 date_select、其它情況用 select_date,像是用來過濾日期的搜尋表單。

內建的日期選單不太好用,無法幫助使用者處理日期與星期幾這個問題。

4.4 單一時間單位

有時只需顯示日期的某個部分,像年或月。Rails 提供一系列的輔助方法:select_yearselect_monthselect_dayselect_hourselect_minute 以及 select_second。這些輔助方法的使用方式非常直觀,產生出來的 inputname 屬性預設會產生以時間單位命名的(譬如 select_year 產生出來的 selectnameyear,以此類推)。這可以透過 :field_name 選項修改。:prefix 選項和 select_dateselect_time 裡的用途相同,預設值也相同。

這些輔助方法的第一個參數指定要選中的數值,可以是 DateTimeDateTime 的實體,或是數值也可以,對應的時間單位會被選中,譬如:

<%= select_year(2009) %>
<%= select_year(Time.now) %>

若今年是 2009 年,上面兩種用法的輸出相同,使用者選的數值可以在 params[:date][:year] 取出。

5 檔案上傳

常見的任務是上傳檔案,舉凡使用者的圖片或需要處理的 CSV。檔案上傳最重要要記住的一點是,表單的編碼必須是 "multipart/form-data"。若使用 form_for,已經自動設定好了。若使用 form_tag,則必須自己設定,以下是表單上傳檔案的兩個例子:

<%= form_tag({action: :upload}, multipart: true) do %>
  <%= file_field_tag 'picture' %>
<% end %>

<%= form_for @person do |f| %>
  <%= f.file_field :picture %>
<% end %>

Rails 提供成對的輔助方法:準方法 file_field_tag 以及供 Model 物件使用的 file_field。這兩個輔助方法與其它表單輔助方法的差別在於無法設定預設值,因為預設值在這沒有意義。第一個例子,使用 file_field_tag 上傳的檔案會存在 params[:picture],而 file_field 上傳的檔案則放在 params[:person][:picture]

5.1 究竟上傳了什麼

params Hash 裡的物件,是 IO 子類別的實體。取決於上傳的檔案大小,會是 StringIO 或存在臨時檔案的 File 實體。兩種都會有 original_filename 屬性,記錄使用者電腦裡的檔案名稱;以及 content_type 屬性,記錄了上傳檔案的 MIME 類型。以下程式碼片段將上傳的內容存在 #{Rails.root}/public/uploads,使用原始上傳的檔名存放(假設使用前例 form_for 的表單來上傳)。

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

一旦檔案上傳成功,有許多事情可以做。譬如把檔案存到別的地方(硬碟、Amazon S3 等);或把檔案與 Model 關聯起來;縮放圖片檔案、產生縮圖等。這些事情超出了本文的範疇,但有許多專門設計的函式庫來協助完成這些任務。其中兩個不錯也比較多人知道的是 CarrierWave 以及 Paperclip

若使用者沒有選擇檔案,對應的參數會是空字串。

5.2 處理 Ajax

要非同步的上傳檔案,不像其它的方法那麼簡單,像 form_for 只要加個 remote: true 即可。Ajax 表單的序列化由跑在瀏覽器的 JavaScript 處理,由於 JavaScript 無法從硬碟讀取檔案,檔案則無法上傳。最常見的解法是使用隱藏的 iframe,作為表單送出的目的地。

6 客製化表單構造器

如前所述,由 form_forfields_for 給出的物件,是 FormBuilder (或子類)的實體。表單構造器封裝了單一物件的顯示。當然可以如往常一樣使用輔助方法,也可以繼承 FormBuilder,再往裡面新增輔助方法。譬如:

<%= form_for @person do |f| %>
  <%= text_field_with_label f, :first_name %>
<% end %>

可以替換成

<%= form_for @person, builder: LabellingFormBuilder do |f| %>
  <%= f.text_field :first_name %>
<% end %>

藉由定義 LabellingFormBuilder 類別:

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

若很常需要使用這個功能,可以定義一個 labeled_form_for 輔助方法,來自動代入 builder: LabellingFormBuilder 選項:

def labeled_form_for(record, options = {}, &block)
  options.merge! builder: LabellingFormBuilder
  form_for record, options, &block
end

表單構造器也決定了下面這行程式碼的行為:

<%= render partial: f %>

fFormBuilder 的實體,則會算繪(render)form 這個部分頁面(partial),並把傳入的 f 設定成表單構造器。若表單構造器是 LabellingFormBuilder 的實體,則會算繪 labelling_form 這個部分頁面。

7 理解參數命名慣例

如前一節所見,表單的數值可以在 params 的第一層,或是嵌套在 Hash 裡。舉例來說,Person Model 對應的 Controller create 動作裡,params[:person] 這個 Hash,會存放建立 person 所需的屬性。params Hash 也可以包含陣列、陣列裡有 Hash 等都可以。

HTML 表單基本上不知道資料的結構,只是產生出純字串組成的 name-value 對。應用程式裡的陣列與 Hash,是透過 Rails 參數的命名慣例所產生。

可能會發現在 Console 裡試試這些例子,可以瞭解得比較快。直接像下例這樣呼叫 Rack 的參數即可:

Rack::Utils.parse_query "name=fred&phone=0123456789"
# => {"name"=>"fred", "phone"=>"0123456789"}

7.1 基本結構

兩個基本結構是陣列與 Hash。Hash 取值的方法和 params 相同。假設表單的內容為:

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

params 的內容為:

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

在 Controller 可以用 params[:person][:name] 來取出表單送出的數值。

Hash 可以多層嵌套,如:

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

產生的 params Hash:

{'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] 會是個陣列。

7.2 結合起來

陣列與 Hash 可以混合使用。舉個例子,Hash 的一個元素可能像前面的例子一樣,是個陣列;或是可以有一個陣列,裡面存 Hash。下例是用來新建多筆地址的表單:

<input name="addresses[][line1]" type="text"/>
<input name="addresses[][line2]" type="text"/>
<input name="addresses[][city]" type="text"/>

params[:addresses] 會是裡面有 Hash 的陣列,每個 Hash 的鍵有 line1, line2 以及 city。Rails 在目前的 Hash 發現有同樣的輸入時,會新建 Hash 來存放。

但有個限制,Hash 可以隨意嵌套,但陣列只能嵌套一次。陣列通常可以用 Hash 取代,譬如可以用 Hash 組成的 Model 物件來取代陣列組成的 Model 物件,Hash 的鍵是 id、陣列的索引、以及其它的參數。

陣列參數與 check_box 輔助方法配合的不好。根據 HTML 規範,沒選中的多選方框不會送出值。但多選方框總是送出值會比較方便。check_box 透過建立一個同名的隱藏輸入來處理。若多選方框沒有被勾選,則只會送出隱藏輸入;若勾選了多選方框,則會將隱藏輸入與勾選的值一起送出,但勾選的值優先權比較高。處理陣列參數時,重複的送出會使 Rails 困惑,因為 Rails 見到重複的輸入,就會建立一個新的陣列。使用 check_box_tag 或用 Hash 取代陣列是推薦的做法。

7.3 使用表單輔助方法

前一節完全沒用到 Rails 的表單輔助方法。自己手寫 input 再直接傳給 text_field_tag 沒有問題。但 Rails 提供了更抽象的方法。這裡介紹 form_forfields_for,以及 :index 選項。

可能會想要有地址表單,裡面有一組可編輯的欄位,分別編輯地址的各個部分。

<%= form_for @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 %>

假設 person 有兩個地址(id 分別是 23 與 45),輸出會像是:

<form accept-charset="UTF-8" action="/people/1" class="edit_person" id="edit_person_1" method="post">
  <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 知道所有的 input 皆屬於 person Hash,因為對 person_form 呼叫了 fields_for。透過指定 :index 選項 index: address.id,可以告訴 Rails,inputname 不要命名為 person[address][city],而是在 addresscity 之間插入索引值(放在中括號內)。通常這很有用,因為可以簡單的找出要修改的地址記錄是那個。:index 的值可以是其它有意義的屬性,字串,甚至是 nil 也可以(nil 會建立一個陣列參數出來)。

要產生更複雜的嵌套,可以明確指定 input name 的第一個部分(person[address]):

<%= fields_for 'person[address][primary]', address, index: address 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" />

一個通用的規則是,最後的 input name 是傳給 fields_forform_for 的名字,加上索引值,再加上屬性名稱。也可以直接將 :index 選項傳給像是 text_field 的輔助方法,但這樣比較繁瑣,在表單構造器一起指定來減少重複。

忽略 :index 選項的簡寫是,在傳給 form_forfields_for 的名稱後面加上一個中括號。這與指定 index: address 的效果相同:

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

會產生與前例相同的輸出。

8 送出至外部資源的表單

Rails 的表單輔助方法,也可以用來打造送出資料到外部資源的表單。但需要給資源指定一個 authenticity_token,可以使用 :authenticity_token 選項來指定:

<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %>
  Form contents
<% end %>

某些時候在送出資料到外部資源時,像是付款閘到。可以使用的欄位受外部 API 限制,還有可能不需要 authenticity_token,此時將 :authenticity_token 設為 false 即可:

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

同樣的技術 form_for 也適用:

<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
  Form contents
<% end %>

不需要 authenticity_token 的情況:

<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
  Form contents
<% end %>

9 打造複雜表單

許多應用程式表單不僅是編輯單一物件這麼簡單。例如建立 person 時,可能想讓使用者(在同一個表單)建立出多筆地址記錄(住家地址、工作地址等)。之後在編輯 person 時,使用者應該要能夠新增、刪除或修改地址。

9.1 Model 部分

Active Record 在 Model 層級提供這樣的支援,請用 accepts_nested_attributes_for 方法:

class Person < ActiveRecord::Base
  has_many :addresses
  accepts_nested_attributes_for :addresses
end

class Address < ActiveRecord::Base
  belongs_to :person
end

會建出一個 Person#addresses_attributes= 方法,用來新建、更新與刪除地址。

9.2 嵌套表單

下面的表單允許使用者用 Person 的實體來建立地址。

<%= form_for @person do |f| %>
  Addresses:
  <ul>
    <%= f.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 會對關聯的每個元素,執行 fields_for 的區塊。若 person 沒有地址,便不執行 fields_for 區塊。常見的做法是在 Controller 建一個或多個空的子元素,這樣只少有一組欄位會顯示給使用者。下例會在新建 person 的表單產生兩組地址欄位,

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

fields_for 給出一個表單構造器。參數的名稱要與 accepts_nested_attributes_for 指定的相同。舉個例子,建立有兩組地址的使用者,送出的參數看起來會像是:

{
  '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 是該記錄的 id。可以傳入 include_id: falsefields_for 來禁用這個行為。可能會想要禁止產生隱藏輸入,因為自動產生的輸入擺放的位置不對,導致 HTML 不合法;或者是使用的 ORM,子物件沒有 id

9.3 Controller 部分

通常需要在傳給 Model 之前,先在 Controller 過濾參數

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

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

9.4 移除物件

可以透過傳入 allow_destroy: trueaccepts_nested_attributes_for,來允許使用者刪除關聯物件。

class Person < ActiveRecord::Base
  has_many :addresses
  accepts_nested_attributes_for :addresses, allow_destroy: true
end

若屬性組成的 Hash 的鍵有 _destroy,值是 1true,則物件會被刪除。下面這個表單允許使用者刪除地址:

<%= form_for @person do |f| %>
  Addresses:
  <ul>
    <%= f.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

9.5 避免空的紀錄

忽略使用者沒有填的欄位通常很有用。可以透過傳給 accepts_nested_attributes_for 一個 :reject_if proc 來辦到。這個 proc 會在每個屬性送出時呼叫。若 proc 回傳 flase,則 Active Record 不會為這組 Hash 建立關聯物件。下面這個例子只有在有給出 kind 屬性時,才會建立地址:

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

有一個方便的符號可以用::all_blank,會建立一個 proc,會拒絕為有任何屬性為空( _destroy 屬性除外)的 Hash 建立物件。

9.6 動態添加欄位

與其一開始就算繪多組地址,不如加入一個按鈕 Add new address,讓使用者自己決定什麼時候要新增一組地址。但 Rails 不支援這個功能。建立一組新的欄位時,要確保關聯陣列的鍵是獨一無二的。在 JavaScript 使用目前的日期是常見的做法。

反饋

歡迎幫忙改善指南的品質。

如發現任何錯誤之處,歡迎修正。開始貢獻前,可以先閱讀貢獻指南:文件

翻譯如有錯誤,深感抱歉,歡迎 Fork 修正,或至此處回報

文章可能有未完成或過時的內容。請先檢查 Edge Guides 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 Ruby on Rails 指南準則來了解行文風格。

最後,任何關於 Ruby on Rails 文件的討論,歡迎至 rubyonrails-docs 郵件論壇