Skip to content

[LiveComponent] Fix disabled choices being submitted by live form updates - #3792

Open
Amoifr wants to merge 1 commit into
symfony:3.xfrom
Amoifr:fix-2934-disabled-checkbox-values
Open

[LiveComponent] Fix disabled choices being submitted by live form updates#3792
Amoifr wants to merge 1 commit into
symfony:3.xfrom
Amoifr:fix-2934-disabled-checkbox-values

Conversation

@Amoifr

@Amoifr Amoifr commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
Q A
Bug fix? yes
New feature? no
Docs? no
Issues Fix #2934
License MIT

Browsers never submit disabled controls, but extractFormValues() (whose contract is precisely "the raw POST data that would be sent if the form were submitted") included them. For an expanded ChoiceType with a choice disabled through choice_attr, the aggregated parent value kept the disabled choice, so every live update re-submitted it: validation failed on live updates while a native submit of the very same form passed (the exact asymmetry reported).

The fix makes extractFormValues() apply browser parity: it drops the values of disabled choices (expanded multiple, expanded single) and treats a checked but disabled standalone checkbox as unsubmitted, whether the field is disabled at the form level (the disabled option) or only in HTML (a disabled attribute, e.g. set through choice_attr).

No JS change: SetValueOntoModelFieldsPlugin only syncs elements carrying data-model themselves (Symfony forms only put it on <form>), and the two morphdom syncs only run for modified or focused elements, which a disabled control cannot be.

Covered by a unit test on the extracted values, an end-to-end functional test (initially checked disabled choice, then a live update: the disabled value never reaches the submitted data), and a JS test pinning the server-side sync of a checkbox the server disables and clears (unchecked + disabled).

Note: disabled <option> elements of a non-expanded <select> have the same theoretical issue through a different code path; left out to keep this focused, happy to follow up if wanted.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

📊 Packages dist files size difference

Thanks for the PR! Here is the difference in size of the packages dist files between the base branch and the PR.
Please review the changes and make sure they are expected.

FileBefore (Size / Gzip)After (Size / Gzip)
LiveComponent
live_controller.js 82.9 kB / 18.43 kB 82.99 kB0% / 18.45 kB0%

@Amoifr
Amoifr force-pushed the fix-2934-disabled-checkbox-values branch from 4677a89 to c64f4f8 Compare August 18, 2026 16:35
@Kocal

Kocal commented Aug 20, 2026

Copy link
Copy Markdown
Member

Please target 3.x branch

@Amoifr
Amoifr force-pushed the fix-2934-disabled-checkbox-values branch from c64f4f8 to 5e6f268 Compare August 20, 2026 13:36
@Amoifr
Amoifr requested a review from Kocal as a code owner August 20, 2026 13:36
@Amoifr
Amoifr changed the base branch from 2.x to 3.x August 20, 2026 13:37
@Amoifr

Amoifr commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Done! Rebased onto 3.x, dist rebuilt from the 3.x sources, PHP suite green (391 tests) and JS tests too, base switched.

@smnandre smnandre left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Guard Breaks Backend State Sync ?

When the backend disables a previously-checked checkbox and sets its data to null/empty, extractFormValues() correctly returns null (line 272).

But setValueOnElement() guard prevents applying this null value because the element is disabled.

Result: DOM shows checked + disabled (contradictory/broken UI), when it should show unchecked + disabled.

Maybe the guard should NOT apply to SetValueOntoModelFieldsPlugin syncs, but only during morphdom to preserve user interactions during DOM updates.

wdyt ?

Comment thread src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php Outdated
Comment thread src/LiveComponent/assets/dist/live_controller.js Outdated
Comment thread src/LiveComponent/assets/dist/live_controller.js Outdated
Comment thread src/LiveComponent/assets/dist/live_controller.js Outdated
@Amoifr

Amoifr commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review! Applied: return types dropped from the test methods (Fabbot agrees with you), dist rebuilt on top of 3.x so it only carries the one-line change, and the guard is now a single check for radios and checkboxes.

On the backend sync question, I think the scenario works as expected, because the guard never competes with the server: when the backend disables a previously checked checkbox and clears its data, the re-rendered HTML no longer carries checked, and idiomorph syncs the checked property during the morph, so the DOM shows unchecked and disabled before SetValueOntoModelFieldsPlugin runs. That plugin is the only place the guard actually kicks in, and it is exactly where the bug was: Symfony keeps the submitted data of a disabled field, so the server renders the disabled choice checked, while the value store (built from what a browser would submit, which never includes disabled controls) does not contain its value and would uncheck it. The two setValueOnElement() calls in morphdom.ts only run for elements the user modified or focused, which a disabled control cannot be. So the server-rendered state stays the source of truth for disabled controls in every path, which is the intent of the guard. Happy to add a test for your scenario if you want it pinned.

@Amoifr
Amoifr force-pushed the fix-2934-disabled-checkbox-values branch 2 times, most recently from 77ae5d6 to fc597e3 Compare August 22, 2026 07:39
@smnandre

Copy link
Copy Markdown
Member

@Amoifr to be honest I'd be happy with one :)

@Amoifr
Amoifr force-pushed the fix-2934-disabled-checkbox-values branch from fc597e3 to 14fd667 Compare August 23, 2026 06:42
@Amoifr

Amoifr commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Here it is, and writing it was instructive: to make the test meaningful the checkbox has to carry data-model itself, because SetValueOntoModelFieldsPlugin only syncs [data-model] elements and Symfony forms only put data-model on the <form>. So for the #2934 case the plugin never touches the inputs, and the two setValueOnElement() calls in morphdom.ts only run for modified or focused elements, which a disabled control cannot be. In other words the guard was dead code for this bug and only covered a contrived markup, so I removed it altogether: the PR is now PHP-only (dist identical to 3.x), plus the test for your scenario (server disables and clears: unchecked + disabled, synced by idiomorph). Rebased on current 3.x as well.

@smnandre smnandre left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A great PR, that will remove/handle a lot of small bugs / weird situations we had no idea about (and that were happening, not a single doubt about that)

Thank you very much @Amoifr

@carsonbot carsonbot added Status: Reviewed Has been reviewed by a maintainer and removed Status: Needs Review Needs to be reviewed labels Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Bug Fix LiveComponent Status: Reviewed Has been reviewed by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[LiveComponent] Disabled option in a multi-value checkbox handled incorrectly

4 participants