From 9fdded05c562bfbaea6bf91199c716cb61b9a91d Mon Sep 17 00:00:00 2001 From: Kenta Ishizaki Date: Mon, 20 Apr 2026 08:13:05 +0900 Subject: [PATCH] Fix label `for` not matching input `id` when collection value is nil When a collection contains a nil value (e.g. `[['Undefined', nil]]`), the label's `for` attribute gets a trailing underscore (`for="model_method_"`) while the input's `id` does not (`id="model_method"`), making the label non-functional. The root cause is in Rails' `sanitize_attribute_name`, which always concatenates an underscore separator before the sanitized value (`"#{sanitized_method_name}_#{sanitized_value(value)}"`), even when `sanitized_value(nil)` returns an empty string. Override `sanitize_attribute_name` in `CollectionExtensions` to skip the trailing underscore when the sanitized value is empty, aligning the label's `for` with the input's `id`. Fixes #1840 --- CHANGELOG.md | 1 + lib/simple_form/tags.rb | 14 +++++++++++++ .../collection_check_boxes_input_test.rb | 20 +++++++++++++++++++ .../collection_radio_buttons_input_test.rb | 20 +++++++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc675d2e9..9bd5be39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Ruby 4.0 support (no changes required) * Support procs on validators for minlength/maxlength, and improve validators logic across the board to match Rails [#1859](https://github.com/heartcombo/simple_form/pull/1859) +* Fix label `for` attribute not matching input `id` when collection contains a nil value [#1840](https://github.com/heartcombo/simple_form/issues/1840) ## 5.4.0 diff --git a/lib/simple_form/tags.rb b/lib/simple_form/tags.rb index 2d779b0dd..fed27ba06 100644 --- a/lib/simple_form/tags.rb +++ b/lib/simple_form/tags.rb @@ -26,6 +26,20 @@ def render_collection end.join.html_safe end + # Override Rails' sanitize_attribute_name to avoid generating a trailing + # underscore when the collection value is nil (e.g. "method_" instead of + # "method"), which causes the label's `for` attribute to not match the + # input's `id`. See https://github.com/heartcombo/simple_form/issues/1840 + def sanitize_attribute_name(value) + sanitized = sanitized_value(value) + + if sanitized.empty? + sanitized_method_name.dup + else + "#{sanitized_method_name}_#{sanitized}" + end + end + def wrap_rendered_collection(collection) wrapper_tag = @options[:collection_wrapper_tag] diff --git a/test/inputs/collection_check_boxes_input_test.rb b/test/inputs/collection_check_boxes_input_test.rb index 139ac33fa..03d0e274e 100644 --- a/test/inputs/collection_check_boxes_input_test.rb +++ b/test/inputs/collection_check_boxes_input_test.rb @@ -314,4 +314,24 @@ class CollectionCheckBoxesInputTest < ActionView::TestCase assert_select 'label.beautiful-label', count: 2 end end + + test 'input check boxes label for attribute matches input id when collection contains nil value' do + with_input_for @user, :name, :check_boxes, collection: [['Yes', true], ['Undefined', nil]] + + assert_select 'input[type=checkbox][value=true]#user_name_true' + assert_select 'label.collection_check_boxes[for=user_name_true]', 'Yes' + assert_select 'input[type=checkbox]#user_name' + assert_select 'label.collection_check_boxes[for=user_name]', 'Undefined' + assert_no_select 'label[for=user_name_]' + end + + test 'input check boxes with nested style label for attribute matches input id when collection contains nil value' do + swap SimpleForm, boolean_style: :nested do + with_input_for @user, :name, :check_boxes, collection: [['Yes', true], ['Undefined', nil]] + + assert_select 'label[for=user_name_true]' + assert_select 'label[for=user_name]' + assert_no_select 'label[for=user_name_]' + end + end end diff --git a/test/inputs/collection_radio_buttons_input_test.rb b/test/inputs/collection_radio_buttons_input_test.rb index 94c5aaaa5..2b75fcb6a 100644 --- a/test/inputs/collection_radio_buttons_input_test.rb +++ b/test/inputs/collection_radio_buttons_input_test.rb @@ -437,4 +437,24 @@ class CollectionRadioButtonsInputTest < ActionView::TestCase assert_select 'label.beautiful-label', count: 2 end end + + test 'input radio label for attribute matches input id when collection contains nil value' do + with_input_for @user, :name, :radio_buttons, collection: [['Yes', true], ['Undefined', nil]] + + assert_select 'input[type=radio][value=true]#user_name_true' + assert_select 'label.collection_radio_buttons[for=user_name_true]', 'Yes' + assert_select 'input[type=radio]#user_name' + assert_select 'label.collection_radio_buttons[for=user_name]', 'Undefined' + assert_no_select 'label[for=user_name_]' + end + + test 'input radio with nested style label for attribute matches input id when collection contains nil value' do + swap SimpleForm, boolean_style: :nested do + with_input_for @user, :name, :radio_buttons, collection: [['Yes', true], ['Undefined', nil]] + + assert_select 'label[for=user_name_true]' + assert_select 'label[for=user_name]' + assert_no_select 'label[for=user_name_]' + end + end end