近日在解票時,遇到一張關於 simple_form 表單問題的 ticket。
(註:該專案使用的 SimpleForm 是 v2.1.1 版本)
# 先聲明小弟是前端工程師,完全的 Rails 生手,有說明錯誤的地方請多多包涵哦 :$
情況是這樣子的,在這個專案裡,使用者個人資料的表單中有個這樣的欄位:
它是以群組核取框(checkboxes)呈現,使用者同時可以核取多個項目。
但奇怪的是,用 SimpleForm 產生的這個欄位,在點選「科技」、「網路」、「手機」的文字(label)時,預期行為應是「讓點選的 label 文字左方的核取框作用(勾選或取消勾選)」,但無論按哪個文字,都是第一項「科技」的核取框有反應而已。
這是因為產生的 HTML 語法有誤導致:
<div class="input check_boxes optional">
<label class="check_boxes optional" for="user_profile_attributes_preferred_info">喜歡哪類資訊</label>
<span>
<input name="user[profile_attributes][preferred_info][]" type="hidden" value=">
<input class="check_boxes optional" id="user_profile_attributes_preferred_info_" name="user[profile_attributes][preferred_info][]" type="checkbox" value="科技">
<label class="collection_check_boxes" for="user_profile_attributes_preferred_info_">科技
</label>
</span>
<span>
<input name="user[profile_attributes][preferred_info][]" type="hidden" value=">
<input class="check_boxes optional" id="user_profile_attributes_preferred_info_" name="user[profile_attributes][preferred_info][]" type="checkbox" value="網路">
<label class="collection_check_boxes" for="user_profile_attributes_preferred_info_">網路
</label>
</span>
<span>
<input name="user[profile_attributes][preferred_info][]" type="hidden" value=">
<input class="check_boxes optional" id="user_profile_attributes_preferred_info_" name="user[profile_attributes][preferred_info][]" type="checkbox" value="手機">
<label class="collection_check_boxes" for="user_profile_attributes_preferred_info_">手機
</label>
</span>
</div>
不大正常,每個 input 的 id 以及 label 的 for 屬性值都一模一樣,當然選不到預期對應的項目囉。
嗯,這樣看來就是 SimpleForm 產生 checkboxes 的時候出了點問題。
看看產生 checkboxes 的那段程式碼:
<%= profile.input :preferred_info, :as => :check_boxes, :collection => ['科技', '網路', '手機', '攝影', '遊戲', '比價'] %>
試著把 :collection 陣列的值改為英文,卻可以產生預期的結果。如果是中文 + 英文呢?居然只有中文的部份會消失。
在不更動這些值的前提下,要根治看來只能從這個 gem 本身下手了。
在 lib/simple_form/action_view_extensions/builder.rb 找到一點線索,
產生過程用到 add_default_name_and_id_for_value 及 sanitize_attribute_name 來產生 id、for 的 value,這兩個 method 內容為:
sanitize_attribute_name:
def sanitize_attribute_name(attribute, value) #:nodoc:
"#{attribute}_#{value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase}"
end
add_default_name_and_id_for_value: (Rails 內建 method)
def add_default_name_and_id_for_value(tag_value, options)
unless tag_value.nil?
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
specified_id = options["id"]
add_default_name_and_id(options)
options["id"] += "_#{pretty_tag_value}" if specified_id.blank? && options["id"].present?
else
add_default_name_and_id(options)
end
end
可以注意到其中 gsub regular expression 的規則是 /[^-\w]/,用意應該是只想保留「-」及「[a-zA-Z0-9_] 」,避免在 id 及 for 中產生規定之外的字元。
在 Rails Console 下驗證一下(特別輸入「中日韓」字元看看):
1.9.3p327 :025 > "!$%^&test-攝影り요".gsub(/[^-\w]/, "")
=> "test-"
果然都被濾掉了。
不過根據 W3C 說明( http://www.w3.org/TR/html-markup/syntax.html#syntax-text ),attribute 的 value 字元規定:
4.5. Text and character data
Text in element contents (including in comments) and attribute values must consist of Unicode characters, with the following restrictions:
- must not contain U+0000 characters
- must not contain permanently undefined Unicode characters
- must not contain control characters other than space characters
其實是可以包含「-」及「[a-zA-Z0-9_] 」之外的更多字元的,「中日韓」字元可以不必被排除在外。
另外在 http://www.regular-expressions.info/unicode.html#prop 查到:
\p{L} or \p{Letter}: any kind of letter from any language.
所以我將 gsub 的 regular expression 規則改了下,並在 Rails Console 中驗證看看:
1.9.3p327 :038 > "!$%^&test攝adsf__dsf-影り요".to_s.gsub(/\s/, "").gsub(/[^-\p{L}]/, "")
=> "test攝adsfdsf-影り요"
OK,這樣的結果可達到預期的需求,那要怎麼在專案中補上修正呢?
我們可以在 config/initializers/ 中新增一支 rb 檔,並 override 掉 simple_form 的這兩個 method。
# -*- encoding : utf-8 -*-
module SimpleForm
module ActionViewExtensions
module Builder
private
def sanitize_attribute_name(attribute, value) #:nodoc:
"#{attribute}_#{value.to_s.gsub(/\s/, "_").gsub(/[^-\p{L}]/, "").downcase}"
end
end
end
end
module ActionView::Helpers
class InstanceTag
private
def add_default_name_and_id_for_value(tag_value, options)
unless tag_value.nil?
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/[^-\p{L}]/, "").downcase
specified_id = options["id"]
add_default_name_and_id(options)
options["id"] += "_#{pretty_tag_value}" if specified_id.blank? && options["id"].present?
else
add_default_name_and_id(options)
end
end
end
end
作了以上修改之後,SimpleForm 便能成功產生正確的 HTML,讓 label for 與 input id 能夠成功配對,有情人終成眷屬,可喜可樂!
FYI.
http://www.w3.org/TR/1999/REC-html401-19991224/types.html#type-name
ID and NAME tokens must begin with a letter ([A-Za-z]) and may be followed by any number of letters, digits ([0-9]), hyphens (“-“), underscores (“_”), colons (“:”), and periods (“.”).
感謝您的回應,不過我有些疑問,關於這點是否在 HTML5 中限制就放寬了呢?
http://www.w3.org/TR/html-markup/global-attributes.html#common.attrs.id
>關於這點是否在 HTML5 中限制就放寬了呢?
嗯,的確是放寬了,謝謝回應。