発端
rails-bootstrap-formsを使っていて、何故かラベルをクリックしてもチェックボックスの操作ができない!となったので、原因を調査してみました。
原因
ラベルのfor属性と、チェックボックスのidが一致していなかった。
問題のerbがこちら。
<%= bootstrap_form_tag method: :get, url: search_path, layout: :horizontal do |f| %>
<%= f.check_box :target_types, { multiple: true, include_hidden: false, checked: params[:target_types].blank? || params[:target_types].include?("Test::Alpha") }, "Test::Alpha" %>
<%= f.check_box :target_types, { multiple: true, include_hidden: false, checked: params[:target_types].blank? || params[:target_types].include?("Test::Beta") }, "Test::Beta" %>
<%= f.submit "Search", class: 'btn btn-primary' %>
<% end %>
生成されるhtml(問題の部分のみ抽出)
<div class="checkbox">
<label for="_target_types_Test::Alpha">
<input checked="checked" id="_target_types_testalpha" name="[target_types][]" type="checkbox" value="Test::Alpha"> Target types
</label>
</div>
labelのfor属性が"_target_types_Test::Alpha"
になっているが、inputのidが"_target_types_testalpha"
となっている。
より詳しい原因を究明するために、rails-bootstrap-formsの実装を追っていく。
def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_value = "0", &block)
options = options.symbolize_keys!
html = check_box_without_bootstrap(name, options.except(:label, :help, :inline), checked_value, unchecked_value)
label_content = block_given? ? capture(&block) : options[:label]
html.concat(" ").concat(label_content || (object && object.class.human_attribute_name(name)) || name.to_s.humanize)
label_name = name
label_name = "#{name}_#{checked_value}" if options[:multiple]
if options[:inline]
label(label_name, html, class: "checkbox-inline")
else
content_tag(:div, class: "checkbox") do
label(label_name, html)
end
end
end
ここらへんにbinding.pryを仕掛けて処理を追っていく。
まずラベルの生成はlabel(label_name, html)
で、ActionView::Helpers::FormBuilder#labelに飛ばしている。
multiple: trueにした場合、
label_name = "#{name}_#{checked_value}" if options[:multiple]
で、ここでは"_target_types_Test::Alpha"
が入っている。
def label(method, text = nil, options = {}, &block)
@template.label(@object_name, method, text, objectify_options(options), &block)
end
method = "_target_types_Test::Alpha"
。
ここでこっちが呼び出される
def label(object_name, method, content_or_options = nil, options = nil, &block)
Tags::Label.new(object_name, method, self, content_or_options, options).render(&block)
end
その後なんやかんやあってmethod = "_target_types_Test::Alpha"
がlabelのfor属性に設定される。(省略)
一方、inputのidについて追っていくと、inputタグの生成はcheck_box_without_bootstrap
でRails側に投げており、ここで、valueの値("Test::Alpha")をsanitizedしたものがidとして設定される。
options["id"] += "_#{sanitized_value(tag_value)}"
def sanitized_value(value)
value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
end
これで"Test::Alpha" -> "testalpha"
に変換され、idが"_target_types_testalpha"
になる。
Railsの仕様により、inputのidはsanitizedされたものが使われるが、rails-bootstrap-forms内でlabelのfor属性に当てる文字列をsanitizedしていなかったのが原因。
解決策
とりあえずの解決策としては、checkboxにidを指定する
<%= bootstrap_form_tag method: :get, url: search_path, layout: :horizontal do |f| %>
- <%= f.check_box :target_types, { multiple: true, include_hidden: false, checked: params[:target_types].blank? || params[:target_types].include?("Test::Alpha") }, "Test::Alpha" %>
+ <%= f.check_box :target_types, { multiple: true, include_hidden: false, checked: params[:target_types].blank? || params[:target_types].include?("Test::Alpha"), id: "_target_types_Test::Alpha" }, "Test::Alpha" %>
- <%= f.check_box :target_types, { multiple: true, include_hidden: false, checked: params[:target_types].blank? || params[:target_types].include?("Test::Beta") }, "Test::Beta" %>
+ <%= f.check_box :target_types, { multiple: true, include_hidden: false, checked: params[:target_types].blank? || params[:target_types].include?("Test::Beta"), id: "_target_types_Test::Beta" }, "Test::Beta" %>
<%= f.submit "Search", class: 'btn btn-primary' %>
<% end %>
が、どう見てもrails-bootstrap-formsのバグなので、Pull Requestを送ろうかなあと思ったら既にあった。
放置されてるっぽい…う〜ん。
いつのまにかMergeされてました。めでたしめでたし。