Skip to content

fix: keep an untouched remote form <select> selected when validation runs - #17133

Open
say8425 wants to merge 11 commits into
sveltejs:mainfrom
say8425:fix/select-keep-selection-without-value
Open

say8425 wants to merge 11 commits into
sveltejs:mainfrom
say8425:fix/select-keep-selection-without-value

Conversation

@say8425

@say8425 say8425 commented Sep 18, 2026 •

Copy link
Copy Markdown

Fixes #17131

What happens

An untouched <select {...field.as('select')}> goes blank (selectedIndex -1) as soon as validate() runs, for instance from an onchange handler on a neighbouring <input>. Once blank, the control no longer contributes to FormData, so a required schema then reports Invalid key: Expected "picked" but received undefined instead of the intended message.

Root cause

  1. field.as('select') always spreads a value prop, and that value is undefined until the field has a value, because input starts out empty.
  2. Whenever any spread prop changes, Svelte re-applies value to the <select>, and a post-mount undefined that matches no option deselects everything.

aria-invalid reads the whole issues object, so every validate()/submit() re-runs the spread. That is how the issue's scenario triggers it. The same happens when the <select> gets or loses its own issue, because aria-invalid really changes then.

The deselect in (2) is intended Svelte behaviour (see binding-select-unmatched in the Svelte test suite, and the discussion in this PR), so the fix belongs in Kit.

Fix

Leave value out of the spread while the field has no value, so the browser keeps the current selection. As soon as the field gets a value (user picks an option, field.set(...), fallback, reset), value appears in the spread and Svelte applies it as before.

This uses a small proxy (omit_while_undefined) that hides the key from spreads. Only the select's value getter is made configurable, which the proxy needs in order to hide it; add_props is unchanged.

Scope: this only covers the never-set state. If a value is set and later cleared with field.set(undefined), the select is deselected, exactly as before this PR. Use field.set('') to go back to an <option value=""> placeholder.

#17132 took a different approach (making issues granular so unrelated validation doesn't re-run the spread). It was merged and then reverted in #17184, because the select still went blank when its own aria-invalid changed.

Tests

  • New route remote/form/select-untouched-validate and two Playwright tests in the async app: an unrelated field validated (its issue appearing and clearing again), and the select's own issues changing. Both fail on version-3 (selectedIndex is -1) and pass with this PR, in dev and build mode. The existing regression test for fix: granular updates of field.value() #14621 is kept.
  • Unit test in form-utils.spec.js: as('select') omits value while the field has none and includes it once it has one.
  • Checked the exact reproduction from Untouched remote form <select> renders blank when validate() runs on onchange #17131 (.for(uid), optional(string(), ''), onchange={() => validate()}), including a select without an empty-value option: selection is kept through repeated validation, user selection and reset.
  • form-utils.spec.js: 76 passed. Async app remote functions (dev): 67 passed, 8 skipped. pnpm lint, pnpm -F @sveltejs/kit check.

Screenshots

Untouched <select> after typing in the text field, blurring it (validate()), and then validate({ all: true }).

Before: the select is blank and the schema can't even see the field.

before-untouched-select-blank.png

After: -- pick -- stays selected and the field's own issue is reported.

after-untouched-select-kept.png


Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PcqPEahv26ddHVqhPZHTJw

@changeset-bot

changeset-bot Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 02af8c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-svelte-dev

pkg-svelte-dev Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Install the latest version of @sveltejs/kit from 02af8c7:

pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/02af8c735560a8fd4785aa8359c23ab6780f9b80

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/kit/pr/17133

Note

This PR is from a fork. A maintainer must approve approve each commit before it can be built and installed.

@hyunbinseo

This comment was marked as off-topic.

@CheHyeonYeong

This comment was marked as off-topic.

Comment thread packages/kit/src/runtime/form-utils.js Outdated

@teemingc teemingc 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.

Thank you for the PR. Sorry I saw #17131 before this one and prematurely merged it.

I don't think this is the right fix. I feel that it should be fixed in Svelte instead. With this example it's a little surprising for Svelte to unselect the currently selected option.

Comment thread .editorconfig Outdated
@say8425

say8425 commented Sep 23, 2026 •

Copy link
Copy Markdown
Author

Thank you for the PR. Sorry I saw #17131 before this one and prematurely merged it.

I don't think this is the right fix. I feel that it should be fixed in Svelte instead. With this example it's a little surprising for Svelte to unselect the currently selected option.

@teemingc Thank you for reviewing the PR and I understand what your meaning that it should be fixed in Svelte instead.

Looking at the history, this is the third time the same pattern has come up: <select> gets an undefined value re-applied and that is implemented as "deselect everything".

So I'll open issue and PR to sveltejs/svelte about on spread re-run: this issue.

And when merged and released these PRs in sveltejs/svelte, then I'll bump the svelte version and drop the omit_while_undefined changes but just keep only the regression tests and the select-untouched-validate route in this PR.

By the way, I'm not AI 😆

@say8425
say8425 marked this pull request as draft September 23, 2026 06:17
@teemingc

Copy link
Copy Markdown
Member

I raised the issue with a few other maintainers and it seems that the majority believe Svelte is behaving correctly here.

I’m wondering if there’s a way to merge this but without setting every function property as configurable

@say8425

say8425 commented Sep 23, 2026

Copy link
Copy Markdown
Author

@teemingc yes configurable is removal.

I added configurable: true because the proxy needs to hide value while it’s undefined, and hiding a non configurable property from a proxy throws a TypeError. But it doesn’t need to apply to every getter, I can remove the changes from add_props and define the select’s value getter separately.

I'll push that changes, thanks

say8425 and others added 10 commits September 27, 2026 14:43
… yet

`field.as('select')` always spread `value` onto the `<select>`, even while the
field has no value (the control was never touched). Svelte re-applies a spread
`value` to a `<select>` whenever any spread prop changes — for example
`aria-invalid` after `validate()` — and an `undefined` value clears the
selection, so an untouched `<select>` ended up blank with `selectedIndex -1`.

Leave `value` out of the spread until the field has a value so the browser
keeps the current selection, in both the "unrelated field validated" case and
the "this field's own issues changed" case.

Fixes sveltejs#17131

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcqPEahv26ddHVqhPZHTJw
…ssion test

Give the text field a rule the test violates so `validate()` has a visible
signal, and cover the issue both appearing and clearing. Also make the unit
assertion self-contained and document why `add_props` getters are configurable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcqPEahv26ddHVqhPZHTJw
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcqPEahv26ddHVqhPZHTJw
@say8425
say8425 force-pushed the fix/select-keep-selection-without-value branch from 93f996d to da6ab70 Compare September 27, 2026 05:45
@say8425 say8425 changed the title fix: keep the selection of a remote form <select> that has no value yet fix: keep an untouched remote form <select> selected when validation runs Sep 27, 2026
@say8425
say8425 marked this pull request as ready for review September 27, 2026 05:46
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcqPEahv26ddHVqhPZHTJw
@say8425
say8425 requested a review from teemingc September 27, 2026 06:15
@Nic-Polumeyv Nic-Polumeyv added the forms Stuff relating to forms and form actions label Sep 27, 2026
@say8425

say8425 commented Oct 1, 2026

Copy link
Copy Markdown
Author

@teemingc Sorry to ping you. Can you review again?

@teemingc teemingc reopened this Oct 1, 2026
@teemingc
teemingc changed the base branch from version-3 to main October 1, 2026 20:48

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

forms Stuff relating to forms and form actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Untouched remote form <select> renders blank when validate() runs on onchange

6 participants