Uh oh!
There was an error while loading. Please reload this page.
fix(fields): a select no longer wipes itself when its value outruns its options (#2968) - #2969
Merged
Merged
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…ts options (#2968) Radix keeps a hidden native `<select>` mirror so a Select's value takes part in native form submission. Assigning a value that mirror has no `<option>` for is a no-op — the element stays on `''` — but Radix still dispatches the synthetic `change`, so `''` comes straight back out through `onValueChange` and lands in react-hook-form on top of the value the caller just set. That window is not theoretical: `SelectContent` registers its native options a commit AFTER the trigger mounts, so a record that lands after first paint (an edit modal whose findOne is still in flight) resets the form into exactly that gap. Every rendered select came back empty while `_defaultValues` still held the right value. When one of the wiped fields is the one a `visibleWhen` predicate reads, the predicate flips back to false, the conditional fields hide again and the form latches broken — Update then fails validation, or submits an empty enum, on a form the user never touched. `SelectItem` rejects `value=""`, so `''` is never a value a user picked — it is always the mirror talking. Drop it at the single `Select` chokepoint, which covers every surface (object form, inline grid editor, action param dialog). Clearing a select still goes through `undefined`, which is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Closes#2968.
Root cause
Not
visibleWhen, and not the cascade-clear path — both of those writeundefined, which does not match the''the reporter measured. The writer isRadix's hidden native
<select>mirror.Radix renders an
aria-hidden<select name=…>alongside every Select so thevalue takes part in native form submission. When the controlled
valuechanges,it assigns the mirror through
HTMLSelectElement.prototype.valueand dispatchesa synthetic
change. If the mirror carries no<option>for that value theassignment is a no-op — the element stays on
''— but thechangefiresanyway, so
''comes straight back out throughonValueChangeand intoreact-hook-form, on top of the value the caller just set.
The window is not theoretical:
SelectContentreturnsnullon its firstrender (its
DocumentFragmentis created in a layout effect), so the nativeoptions register a commit after the trigger mounts. A record landing after
first paint — an edit modal whose
findOneis still in flight — resets the forminto exactly that gap.
That explains every detail of the report: the value is
''and notundefined,only
selectfields are hit,_defaultValuesstays correct while_formValuesis blanked, and it is timing-dependent. And because one wiped field is usually
the one the
visibleWhenpredicate reads, the predicate flips back to false, theconditional fields hide again, and the form latches broken.
Demonstrated directly against the
Selectwrapper (pre-fix):onValueChangereceivedundefined→ value present in options[]undefined→ value not in options[""][""][]Fix
SelectItemrejectsvalue=""outright, so''can never be a value a userpicked — it is always the mirror talking. Drop it at the single
Selectchokepoint (
packages/components/src/ui/select.tsx), which covers every surfacethat renders one: the object form, the inline grid editor,
ActionParamDialog.Clearing a select still goes through
undefined, so thedependsOncascade-clear is untouched.
Tests
Two regression files, red before the fix and green after (3 failed → 6 passed):
packages/components/src/ui/__tests__/select-empty-writeback.test.tsx— thewrapper's invariant, including a positive assertion that a real selection is
still forwarded.
packages/components/src/renderers/form/__tests__/form-select-value-survives-rules.test.tsx— the form-level shape from the issue: a record landing late keeps both
selects and reveals the
visibleWhenfield, and no''is ever recorded as auser edit.
Full DOM suite: 4113 passed / 24 skipped, 0 failed. The 18
unit-projectfailures on this branch are pre-existing (verified by re-running them with the
fix stashed): spec Theme schema parity, plugin-kanban, data-objectstack
onMutation,DATEFORMAT.Browser verification
Framework
examples/app-showcaseon a private port, driven through objectui'sown console dev server (so the running UI is this branch's
src, not thevendored bundle). Fixtures:
showcase_invoice— two selects (region,status) pluspaid_oncarryingvisibleWhen: record.status == 'paid', theexact shape in the issue, and a master-detail line-item subform — and
showcase_cascadefor thedependsOn/ per-optionvisibleWhenpaths.paid_onrendered, and afiber probe of RHF reports
lostKeys: []with_formValuesmatching_defaultValues.paid_onhides and comes back withits value,
readonlyWhenontax_ratefollows, nothing else is touched.batchTransactionpath): renaming the invoice andswitching
regionAPAC → EMEA both persist, withstatus/paid_onintact;the server's
readonlyWhennotice fortax_ratefires as designed.dependsOngate ("Select country first") still holds,changing the parent still clears the child, the option list re-filters, and
cn/zj→us/capersists server-side.A
this.client.data.batchTransaction is not a functionfailure seen mid-run wastraced to a stale local
node_modules(installed@objectstack/clienthaddrifted to 14.6.0 while
pnpm-lock.yamlpins17.0.0-rc.0).pnpm installresolved it; no lockfile or source change was needed, and the master-detail save
above was then verified on the correct client.
🤖 Generated with Claude Code