`collection_radio_buttons` / `collection_check_boxes` でnil値のlabel `for`とinput `id`が一致しないバグを修正
collection_radio_buttons や collection_check_boxes のコレクションに nil が含まれる場合、ラベルの for 属性がインプットの id と一致しないバグが修正されました。これにより、nil 値を持つ選択肢のラベルをクリックしても対応するラジオボタンやチェックボックスが選択されなかった問題が解消されます。
背景
nil を含むコレクションを collection_radio_buttons に渡した際、生成されるHTMLに不整合が生じていました。具体的には、for 属性に末尾アンダースコアが付いた user_active_ が出力される一方、id 属性には user_active が出力されるため、両者が一致しない状態になっていました。
collection_radio_buttons(:user, :active, [["Yes", true], ["Undefined", nil]], :last, :first)
上記のコードが生成するHTMLは次のようになっていました:
<!-- ✅ true — for と id が一致 -->
<input id="user_active_true" type="radio" value="true" />
<label for="user_active_true">Yes</label>
<!-- ❌ nil — 末尾アンダースコアの不一致 -->
<input id="user_active" type="radio" value="" />
<label for="user_active_">Undefined</label>
この挙動は、sanitize_attribute_name が 36cb715 で導入された2012年から継続していたものです。
技術的な変更
根本原因は、id と for を生成する2つのメソッドが nil を異なる方法で処理していたことにあります。
Tags::Base#add_default_name_and_id_for_value(inputの id 生成に使用)は、tag_value が nil の場合にサフィックスを付与しないため id="user_active" を正しく生成していました。一方、CollectionHelpers#sanitize_attribute_name(labelの for 生成に使用)は常に _ を連結するため、sanitized_value(nil) が空文字列を返しても for="user_active_" という末尾アンダースコア付きの値を生成していました。
修正は actionview/lib/action_view/helpers/tags/collection_helpers.rb の sanitize_attribute_name メソッドに加えられました。
変更前:
def sanitize_attribute_name(value)
"#{sanitized_method_name}_#{sanitized_value(value)}"
end
変更後:
def sanitize_attribute_name(value)
sanitized = sanitized_value(value)
if sanitized.empty?
sanitized_method_name.dup
else
"#{sanitized_method_name}_#{sanitized}"
end
end
サニタイズ後の値が空文字列(nil を含む)の場合はアンダースコアセパレータを省略し、メソッド名のみを返すよう変更されました。これにより add_default_name_and_id_for_value と同じ挙動になります。
リグレッションテストとして actionview/test/template/form_collections_helper_test.rb に2件のテストが追加されています。それぞれ collection_radio_buttons と collection_check_boxes に対して nil 値を含むコレクションを渡した際に for と id が一致することを検証し、また label[for=user_active_] が生成されないことも assert_no_select で明示的に確認しています。
設計判断
add_default_name_and_id_for_value の挙動に sanitize_attribute_name を合わせる という方針が採られました。
nil の特別扱いを sanitize_attribute_name 内に閉じ込めることで、呼び出し側のロジックを変更せずに修正が完結しています。sanitized_value(nil) が空文字列を返す事実を活用し、nil を特別ケースとして明示的にチェックするのではなく empty? を条件とすることで、nil 以外で空文字列になる値(例:空文字列のコレクション値)も同様に正しく処理されます。また sanitized_method_name.dup として新しいStringオブジェクトを返している点も、参照の安全性を考慮した実装です。
まとめ
2012年の導入以来残っていた sanitize_attribute_name の nil 処理の不整合が、最小限の変更で修正されました。nil 値を含むコレクションでラベルクリックが機能しないというアクセシビリティ上の問題が解消され、collection_radio_buttons と collection_check_boxes の両方で一貫した動作が保証されます。