Skip to content

feat(combobox): require at least one option on required ComboboxCheckbox#477

Closed
pierry01 wants to merge 2 commits into
ruby-ui:mainfrom
pierry01:feat/combobox-required-validation
Closed

feat(combobox): require at least one option on required ComboboxCheckbox#477
pierry01 wants to merge 2 commits into
ruby-ui:mainfrom
pierry01:feat/combobox-required-validation

Conversation

@pierry01

@pierry01 pierry01 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

A ComboboxCheckbox multi-select marked as required had no client-side validation — the form could submit with nothing checked.

This wires ComboboxCheckbox to the form-field controller (mirroring the existing ComboboxRadio) and adds syncValidity() to the combobox controller: it toggles the native required attribute across the required inputs so the browser blocks submission until at least one option is checked. Runs on connect, on every input change, and on toggle-all.

Registry rebuilt; gem suite green (added test_combobox_checkbox_wires_form_field_validation).

@pierry01 pierry01 requested a review from cirdes as a code owner July 13, 2026 20:21

cirdes commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

@cubic

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 13, 2026

Copy link
Copy Markdown

@cubic

@cirdes I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/app/javascript/controllers/ruby_ui/combobox_controller.js">

<violation number="1" location="docs/app/javascript/controllers/ruby_ui/combobox_controller.js:51">
P2: After a failed submit, using Select all can leave the required error visible even though the selection is now valid, because this validity update does not notify `ruby-ui--form-field#onInput`. Triggering the form-field input update after `toggleAllItems()` would keep `FormFieldError` synchronized with the native validity state.</violation>
</file>

<file name="gem/lib/ruby_ui/combobox/combobox_controller.js">

<violation number="1" location="gem/lib/ruby_ui/combobox/combobox_controller.js:26">
P2: A combobox that gains required options after connect can remain in the wrong native-validation state because `requiredInputs` is a one-time snapshot. Tracking target connect/disconnect or maintaining a stable marker for originally required inputs would keep dynamically morphed options in the group.</violation>

<violation number="2" location="gem/lib/ruby_ui/combobox/combobox_controller.js:27">
P2: After a required error is displayed, selecting or clearing all options leaves the `FormField` error message stale because bulk changes emit no option `input`/`change` event. Reusing the option input event path after the bulk update would refresh both native validity and the form-field error.</violation>

<violation number="3" location="gem/lib/ruby_ui/combobox/combobox_controller.js:52">
P1: After a user selects an option, `syncValidity()` removes `required` from every option; resetting the containing form can then clear the selection without reapplying `required`, allowing submission with nothing selected. Listening for the form's `reset` event and resynchronizing after the reset would keep native validation correct.</violation>

<violation number="4" location="gem/lib/ruby_ui/combobox/combobox_controller.js:55">
P2: When a checked option is disabled, it is treated as satisfying the required group even though it contributes no submitted form value, so all enabled unchecked options become non-required. Excluding disabled controls from `anyChecked` would prevent a form from passing with no usable selection.</violation>

<violation number="5" location="gem/lib/ruby_ui/combobox/combobox_controller.js:57">
P3: The new required-group logic substantially duplicates `CheckboxGroupController#handleRequired`, creating two implementations that can drift as validation edge cases are fixed. Sharing the group-validity helper or centralizing this behavior would keep checkbox and combobox validation consistent.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
}

syncValidity() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: After a user selects an option, syncValidity() removes required from every option; resetting the containing form can then clear the selection without reapplying required, allowing submission with nothing selected. Listening for the form's reset event and resynchronizing after the reset would keep native validation correct.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/lib/ruby_ui/combobox/combobox_controller.js, line 52:

<comment>After a user selects an option, `syncValidity()` removes `required` from every option; resetting the containing form can then clear the selection without reapplying `required`, allowing submission with nothing selected. Listening for the form's `reset` event and resynchronizing after the reset would keep native validation correct.</comment>

<file context>
@@ -46,6 +49,14 @@ export default class extends Controller {
     }
   }
 
+  syncValidity() {
+    if (this.requiredInputs.length === 0) return
+
</file context>

}
}

syncValidity() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: After a failed submit, using Select all can leave the required error visible even though the selection is now valid, because this validity update does not notify ruby-ui--form-field#onInput. Triggering the form-field input update after toggleAllItems() would keep FormFieldError synchronized with the native validity state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/app/javascript/controllers/ruby_ui/combobox_controller.js, line 51:

<comment>After a failed submit, using Select all can leave the required error visible even though the selection is now valid, because this validity update does not notify `ruby-ui--form-field#onInput`. Triggering the form-field input update after `toggleAllItems()` would keep `FormFieldError` synchronized with the native validity state.</comment>

<file context>
@@ -45,6 +48,14 @@ export default class extends Controller {
     }
   }
 
+  syncValidity() {
+    if (this.requiredInputs.length === 0) return
+
</file context>


connect() {
this.updateTriggerContent()
this.requiredInputs = this.inputTargets.filter((input) => input.required)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A combobox that gains required options after connect can remain in the wrong native-validation state because requiredInputs is a one-time snapshot. Tracking target connect/disconnect or maintaining a stable marker for originally required inputs would keep dynamically morphed options in the group.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/lib/ruby_ui/combobox/combobox_controller.js, line 26:

<comment>A combobox that gains required options after connect can remain in the wrong native-validation state because `requiredInputs` is a one-time snapshot. Tracking target connect/disconnect or maintaining a stable marker for originally required inputs would keep dynamically morphed options in the group.</comment>

<file context>
@@ -23,6 +23,8 @@ export default class extends Controller {
 
   connect() {
     this.updateTriggerContent()
+    this.requiredInputs = this.inputTargets.filter((input) => input.required)
+    this.syncValidity()
   }
</file context>

connect() {
this.updateTriggerContent()
this.requiredInputs = this.inputTargets.filter((input) => input.required)
this.syncValidity()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: After a required error is displayed, selecting or clearing all options leaves the FormField error message stale because bulk changes emit no option input/change event. Reusing the option input event path after the bulk update would refresh both native validity and the form-field error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/lib/ruby_ui/combobox/combobox_controller.js, line 27:

<comment>After a required error is displayed, selecting or clearing all options leaves the `FormField` error message stale because bulk changes emit no option `input`/`change` event. Reusing the option input event path after the bulk update would refresh both native validity and the form-field error.</comment>

<file context>
@@ -23,6 +23,8 @@ export default class extends Controller {
   connect() {
     this.updateTriggerContent()
+    this.requiredInputs = this.inputTargets.filter((input) => input.required)
+    this.syncValidity()
   }
 
</file context>

syncValidity() {
if (this.requiredInputs.length === 0) return

const anyChecked = this.requiredInputs.some((input) => input.checked)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a checked option is disabled, it is treated as satisfying the required group even though it contributes no submitted form value, so all enabled unchecked options become non-required. Excluding disabled controls from anyChecked would prevent a form from passing with no usable selection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/lib/ruby_ui/combobox/combobox_controller.js, line 55:

<comment>When a checked option is disabled, it is treated as satisfying the required group even though it contributes no submitted form value, so all enabled unchecked options become non-required. Excluding disabled controls from `anyChecked` would prevent a form from passing with no usable selection.</comment>

<file context>
@@ -46,6 +49,14 @@ export default class extends Controller {
+  syncValidity() {
+    if (this.requiredInputs.length === 0) return
+
+    const anyChecked = this.requiredInputs.some((input) => input.checked)
+
+    this.requiredInputs.forEach((input) => { input.required = !anyChecked })
</file context>
Suggested change
const anyChecked = this.requiredInputs.some((input) => input.checked)
const anyChecked = this.requiredInputs.some((input) => input.checked && !input.matches(":disabled"))


const anyChecked = this.requiredInputs.some((input) => input.checked)

this.requiredInputs.forEach((input) => { input.required = !anyChecked })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new required-group logic substantially duplicates CheckboxGroupController#handleRequired, creating two implementations that can drift as validation edge cases are fixed. Sharing the group-validity helper or centralizing this behavior would keep checkbox and combobox validation consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/lib/ruby_ui/combobox/combobox_controller.js, line 57:

<comment>The new required-group logic substantially duplicates `CheckboxGroupController#handleRequired`, creating two implementations that can drift as validation edge cases are fixed. Sharing the group-validity helper or centralizing this behavior would keep checkbox and combobox validation consistent.</comment>

<file context>
@@ -46,6 +49,14 @@ export default class extends Controller {
+
+    const anyChecked = this.requiredInputs.some((input) => input.checked)
+
+    this.requiredInputs.forEach((input) => { input.required = !anyChecked })
+  }
+
</file context>

@pierry01 pierry01 marked this pull request as draft July 14, 2026 12:09
@pierry01 pierry01 closed this Jul 14, 2026
@pierry01 pierry01 deleted the feat/combobox-required-validation branch July 14, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants