Uh oh!
There was an error while loading. Please reload this page.
fix(fields): a gated option list keeps the field's stored value on mount - #4253
Merged
Conversation
The four fixed-option widgets and the form renderer each end their cascade resolution with a "drop what is no longer offered" clear. Both read `resolveCascadingOptions`, which returns an EMPTY offered set whenever the list is gated (a declared `dependsOn` parent is still empty), so nothing the field held could be "still offered" and both paths wrote the field empty on MOUNT, with no interaction — while the control rendered its "select the parent first" hint beside it. Gated means UNKNOWN, not invalid. ADR-0058's cascade prunes on a USER-DRIVEN parent change; a withheld list on mount is missing information (a later-cleared parent, an import, a partially-migrated row), and that is not a reason to destroy stored data. Both clears now skip while gated, reading the resolver's own `gated` flag rather than re-deriving it from an empty offered set — which would collide with the distinct never-configured case guarded by #4220. Convergence is unchanged: once the parent IS chosen and the resolved set genuinely excludes the stored value the prune applies, including at the moment the gate lifts. The three states (never-configured / gated / resolved-and-excludes) are pinned apart across all four widgets and the form host. Fixes#4247 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 11, 2026 09:14
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#4247
A dependency-gated option list deleted the field's stored value on mount, before any interaction, while the control was rendering "Select Country first" beside it — telling the user it could not offer anything, and deleting what they had.
Ruling implemented (from the card)
Gating signal
The guard reads
resolveCascadingOptions's owngatedflag — already returned onCascadingOptionsand already destructured by all four widgets. It is deliberately not re-derived fromoptions.length === 0: that expression is the never-configured case guarded by #4244/#4220, a different state with a different guard, and collapsing the two would undo that fix's distinction.Two clear sites, not one
The card notes the FORM path is affected too. Measured: it is affected by an independent second clear, not by the widgets' effect.
packages/components/src/renderers/form/form.tsxruns its own cascade clear over every option field (form.setValue(name, undefined)), and the components test suite never loads@object-ui/fields— yet the gated value was still wiped there. So the guard lands in both places; the widget fix alone does not reach the form host. Both read the same resolver, so the two guards cannot drift.The three states, pinned apart
optionsat alloptionWidgets.unconfiguredOptions.test.tsx— control, unchanged, still greendependsOnoptionWidgets.gatedOptions.test.tsxx4 widgets +form-gated-options-keep-value.test.tsxoptionWidgets.gatedOptions.test.tsxx4 widgets + form controlThe transition between rows 2 and 3 is pinned as its own case: a gated field holding a value converges the moment the parent is chosen — the resolved set excluding it clears it then; a resolved set including it keeps it. Pinned on the widget path (
MultiSelectField,SelectField) and on the form path (typing a country whose list excludes the stored value prunes it on that transition).Evidence
Premise reproduced red-first on the branch tip, both hosts.
Widget path, before the fix — 8 red / 4 green:
The 4 passes are the resolved-and-excludes controls — the ADR-0058 contract, green before and after.
Form path, before the fix — 1 red / 3 green (nothing but
form.tsx's own effect can move the value in this suite):After the fix, all three files green — 24 passed (24), the #4220 control file included.
Reverse verification
Fix taken out with
git checkout origin/main --over the five source files (nevergit stash— shared stack). Predicted direction: RED, with the mount-clear signature returning per widget (onChange([])for the array widgets,onChange(undefined)for the scalar ones), and the resolved-and-excludes controls staying green.Confirmed exactly — 9 failed / 15 passed:
The 15 green are the resolved-and-excludes controls (4 widget + 3 form) and the whole 8-case #4220 never-configured control file — i.e. taking the fix out moves the gated state only, leaving both neighbouring states exactly where they were. Restored, and re-confirmed green (24/24) before pushing.
Gates
pnpm exec vitest run packages/fields/— 78 files, 1220 passedpnpm exec vitest run packages/components/— 116 files, 1047 passedpnpm exec vitest run packages/core/ packages/plugin-form/ packages/react/— 150 files, 2533 passed (the resolver's other consumers)turbo run type-check --filter=@object-ui/fields --filter=@object-ui/components— 11 tasks green (build closure built first)turbo run linton both packages — 0 errorsnode scripts/check-control-bytes.mjs— OK (3965 files); touched files additionally self-scanned with a ripgrep class covering the C0 range (NUL through unit-separator) plus DEL — cleangated-options-keep-stored-value-4247.md(@object-ui/fields+@object-ui/components, both patch);check-changeset-fixed/check-changeset-no-majorgreenNo public API or type surface changed — the guard reads a flag the returned type already carried, so there is no consumer-facing signature to sweep.
Generated by Claude Code