Skip to content

Fix disabled switch/checkbox values being clobbered by hidden fallback (#1279) - #1566

Open
zimudec wants to merge 1 commit into
wintercms:wip/1.3from
zimudec:issue-1279
Open

zimudec wants to merge 1 commit into
wintercms:wip/1.3from
zimudec:issue-1279

Conversation

@zimudec

@zimudec zimudec commented Oct 8, 2026

Copy link
Copy Markdown

Fixes #1279

Problem

A backend form field rendered as switch or checkbox could have its stored value silently overwritten when saving:

  1. The field is checked (value 1 in the database).
  2. At runtime it becomes disabled - via the trigger API, the attributes option, or any third-party JavaScript (el.disabled = true).
  3. The user saves the form.

Result: the field saves as 0 even though it was checked and disabled - nobody toggled it off.

Root cause

The switch/checkbox field partials render a hidden "unchecked" fallback input (<input type="hidden" name="X" value="0">) sharing the name of the visible control - the standard Rails-style checkbox trick to make "toggling off" submittable.

Per WHATWG HTML §4.10.22.4 ("Constructing the entry list"), the browser discards a disabled control from the submitted entry list - but the visible control being disabled does nothing to the hidden fallback, which stayed enabled and kept contributing value="0" with the same name. The server then processed that 0 as the field's submitted value (last value wins), overwriting the stored state.

FormField::filterAttributes() renders config disabled and readOnly attributes only at the field position (the visible control), and the runtime trigger API applies prop('disabled') on the control only - the fallback never knew.

What this PR does

Two coordinated fixes:

A - render inheritance (PHP): the hidden fallback in modules/backend/widgets/form/partials/_field_switch.php and _field_checkbox.php now inherits the disabled state of the visible control when it is known at render time (previewMode, config disabled, or a disabled HTML attribute rendered via the attributes option), so static scenarios no longer leak the fallback into the entry list.

B - runtime deletion (JS): the backend form widget (winter.form.js) now listens for the formdata event on its form and, whenever the entry list is constructed, deletes any entry belonging to a field whose visible checkbox is currently disabled - covering the trigger API, third-party JS, and any future runtime source. The event fires regardless of how serialization happens: form submission and the FormData constructor used by both the Snowboard request API (Request.js) and the framework request API.

formdata is Baseline (widely available) since September 2021; the listener only registers where 'FormDataEvent' in window - older browsers skip it and behave exactly as before, and the render-time fix (A) still protects the static cases in those browsers.

Field semantics unchanged (by design)

  • disabled = the value is not submitted and not saved; the stored value is left as-is. This now holds for runtime disables too - that is the fix.
  • readOnly = the field cannot be edited, but its value is still submitted and saved. This stays as before: readonly is a UI-level promise, not a server-side trust rule - the server must still authorize and validate what it saves (as it does today for every field).
  • An actively unchecked, enabled toggle still saves its off state via the fallback entry (0) - that is the hidden fallback's whole purpose; the listener only removes entries of disabled fields.

Testing

  • Server-side: new unit tests via the existing postForm() pattern in FormFieldSetSaveTest - a config-disabled switch field is omitted from getSaveData() even when a value arrives, and an enabled switch's fallback 0 is still saved.
  • Browser (local test bench), red-to-green repro of the exact issue scenario (checked switch + runtime disabled + save): before, the request payload contained Field=0 and the DB flipped to 0; after, the field is entirely absent from the payload and the stored value is preserved. Same verified through the core trigger API path (data-trigger action="disable"), and non-regression confirmed for enabled-checked (0,1 entry list, last wins), enabled-unchecked (bare 0, saves off) and readOnly (value still saved) fields.
  • Full winter test suite green (870/870 on wip/1.3 tip, no new failures - the two pre-existing PHPUnit test-runner warnings upstream are unchanged).

@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: db420b5e-7943-4b87-9351-622f74a9a77d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant