Skip to content

feat: expose field touched state - #14692

Merged
Rich-Harris merged 16 commits into
sveltejs:version-3from
LeeWxx:feature/add-field-touched-accessor
Jul 1, 2026
Merged

feat: expose field touched state#14692
Rich-Harris merged 16 commits into
sveltejs:version-3from
LeeWxx:feature/add-field-touched-accessor

Conversation

@LeeWxx

@LeeWxx LeeWxx commented Oct 11, 2025

Copy link
Copy Markdown
Contributor

#14665

Remote form fields expose field.touched() so apps can see whether a field or its children were interacted with, and client/server runtimes surface the existing touched map.


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.

@changeset-bot

changeset-bot Bot commented Oct 11, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0d9ce67

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

@LeeWxx
LeeWxx force-pushed the feature/add-field-touched-accessor branch from 01012dd to b503fd7 Compare October 11, 2025 09:53
@hrueger

hrueger commented Oct 19, 2025

Copy link
Copy Markdown
Contributor

Just tested it, works great for me! 👍

@hrueger

hrueger commented Oct 22, 2025

Copy link
Copy Markdown
Contributor

Sorry for asking here about the status again - we're desperately waiting for this feature and this pr has been unreviewed for almost 2 weeks now 🤷‍♂️

Is there anything that can be done to help get this reviewed soon(ish)? Thanks in advance!

@LeeWxx do you think, you can request a review from maintainers?

@LeeWxx
LeeWxx force-pushed the feature/add-field-touched-accessor branch from b503fd7 to 878e77c Compare October 24, 2025 10:42
@LeeWxx

LeeWxx commented Oct 24, 2025

Copy link
Copy Markdown
Contributor Author

@hrueger I don’t think I have permission to request reviewers either.
Do you happen to know if there’s any other way to get this reviewed?

@hrueger

hrueger commented Oct 24, 2025

Copy link
Copy Markdown
Contributor

Very strange... I usually don't tag random people as I've heard it is considered rude. So hopefully Mr Svelte himself (aka @Rich-Harris) sees this as a friendly request for review? 😃🙏 thanks in advance!

@firatciftci

Copy link
Copy Markdown

I think it would be beneficial to differentiate (and individually support) both touched and dirty state for form fields, where the former checks for any modification from the original state (and remains touched once any modification occurs), and the latter keeps track of difference from original and current form state (where dirty would be false if all form fields are the same as original, even if touched is true).

@hrueger

hrueger commented Oct 24, 2025

Copy link
Copy Markdown
Contributor

Ideally, I'd want to implement the following behavior:

  • I have a field, which can be plain, or outlined green or outlined red with a list of issues
  • at the beginning, the field is plain / idle
  • When the user types and the validation passes, it becomes green
  • when the user types more or removes chats so that the field becomes invalid, it should become red and the issue list should display
  • or when the user does not input valid content and the field is blurred, then it should become red

Note that when inputting for the first time, it only can become red after it was already green. So that you don't confuse the user with a min length issue for example while he is still typing.

Does that make sense? Hard to express in writing 😁

Would that be possible without manually keeping track of the validation success of each field? Maybe we need a blurredOnce tracker, too? Not sure.

@Antonio-Bennett

Copy link
Copy Markdown
Contributor

@LeeWxx I think a good place would be the contributors channel in the svelte discord.

@LeeWxx

LeeWxx commented Oct 25, 2025

Copy link
Copy Markdown
Contributor Author

@firatciftci @hrueger I started with adding support for the touched state only in this PR to keep the scope small and focused. Once we agree on the purpose and desired behavior in the issue or discussion, I’m thinking of following up with another PR for dirty and related states. Would that make sense to you?

@jdgamble555

Copy link
Copy Markdown

Hey guys, since touched is agreed, any progress on merging this PR?

@teemingc

teemingc commented Dec 3, 2025

Copy link
Copy Markdown
Member

We'll be reworking the form remote function to reduce the client bundle size for users not using these features. We'll likely have to wait until that's in place

@teemingc teemingc added the forms Stuff relating to forms and form actions label Jan 13, 2026
@Rich-Harris

Copy link
Copy Markdown
Member

Thank you — finally getting round to this PR. I think the big outstanding question here is how we define 'touched'. At the moment a field is touched when the user edits it, or when it is set programmatically, but I don't think that's quite right.

The main reason to care about this is so that sensible things happen on myform.validate(), which by default ignores untouched fields. Reasonable people can and do disagree about exactly when to show validation errors, and so it might be necessary to have more granularity here (with the aforementioned dirty() method, or with more options when you call myform.validate({...})) but I would love to see if we can come up with something that basically works for everyone.

I think the best guide we have here is how browsers apply :user-invalid and :user-valid when HTML form constraints are applied (even though we disregard HTML form constraints in favour of server-side validation with optional preflight validation). Essentially, these pseudo-classes are applied when the user has 'interacted' with a form control, meaning

  1. the control was both edited and blurred
  2. a submission was attempted

Some libraries would remove the 'edited and' part of 1 — if a field is required and you skip over it, it's obviously something you will need to go back and fix. But I don't think it's that uncommon to tab through a form to orient yourself, and it's a little annoying if that results in a wave of validation warnings. Do people have strong opinions here? I'm not sure what a good way to make it configurable would be, if we deemed that necessary.

Another nuance: I think it's probably reasonable to ignore programmatic changes that occur before the form is mounted. In other words:

<script>
  import { myform } from './stuff.remote';

  // this would _not_ mark `message` as touched
  myform.fields.message.set('initial');
</script>

<!-- but this would -->
<button onclick={() => myform.fields.message.set('updated')}>
  update
</button>

<form {...myform}>...</form>

Interested to hear people's thoughts and use cases.

@theetrain

theetrain commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

To me, "touched" implies "interacted with"; while "tainted" implies "initial data has changed".

Other ideas for "touched": changed, focused.
Other ideas for "tainted": modified, mutated, edited, dirty (mentioned).


But I don't think it's that uncommon to tab through a form to orient yourself, and it's a little annoying if that results in a wave of validation warnings.

From the MDN :user-invalid example, on Chrome, Safari, and Firefox, it seems tabbing in and out of the field won't immediately invoke the :user-invalid selector; it only invokes when a change has been made and the field is blurred.

I think at least we have the right tools to display errors and decide when to display errors without SvelteKit needing to do anything extra other than expose errors on submit (aria-invalid), programmatic preflight, or via pattern, :user-invalid and related attributes and selectors. Exposing tainted certainly helps with displaying or hiding "you have unsaved changes"-type messages.

@jdgamble555

Copy link
Copy Markdown

I'm happy with just touched for now, since it is already tracked internally, but if you want to be conscious of all cases, we can look at what Angular has been doing for almost 10 years since Angular 2:

https://angular.dev/guide/forms/signals/field-state-management#field-state-signals

  • touched() - User has focused and blurred the field (if interactive)
  • dirty() - User has modified the field (if interactive), even if value matches initial state

J

@Rich-Harris

Copy link
Copy Markdown
Member

We certainly could expose both. The bigger question for me — and I realise this is tangential to the current PR — is what the behaviour of myform.validate() should be. If I'm validating in an oninput or onfocusout handler, should that mark a touched-but-not-dirty <input> with aria-invalid="true" (assuming it is invalid), or not?

Also, should touched() (or indeed dirty()) always become true upon submission? I think yes, because you would want validity to be updated immediately if you were updating a form post-invalid-submission. But you could argue that that's just something that should be tracked internally, i.e. myform.validate() should behave the same as myform.validate({ includeUntouched: true }) when myform.submitted === true (unless the form was reset).

@Rich-Harris Rich-Harris added this to the 3.0 milestone Jun 26, 2026
@Rich-Harris Rich-Harris added the needs-decision Not sure if we want to do this yet, also design work needed label Jun 26, 2026
@jdgamble555

Copy link
Copy Markdown
  1. (with help from CHAT)

aria-invalid communicates to assistive technologies that the current value is invalid. If a required field starts empty, then:

  • User tabs into it.
  • User tabs out without typing.

Technically the value is invalid, but announcing it immediately is often a poor experience because the user hasn’t attempted to provide a value yet. Many users are simply navigating the form.

The concepts of touched, dirty, and submitted represent different things and are best kept separate.

  • Touched: The user has focused the control and then left it (blurred it).
  • Dirty: The user has changed the value.
  • Submitted: The user attempted to submit the form.

A form submission doesn’t imply either of the first two happened.

Recommended behavior

On submit:

  • ✅ submitted = true
  • ❌ Don’t automatically set dirty = true
  • ❌ Don’t automatically set touched = true

Just thoughts of AI

J

@Rich-Harris

Copy link
Copy Markdown
Member

Yeah, the maintainers spent some time on this earlier today and basically converged on the same conclusions. Basically instead of myform.validate() excluding fields that weren't touched(), it would exclude those that weren't dirty(). (I suppose we need to rename { includeUntouched: true } to { includeClean: true }.)

I'll do that in a follow-up PR once this one passes CI

@Rich-Harris
Rich-Harris changed the base branch from main to version-3 June 26, 2026 19:31
@Rich-Harris
Rich-Harris merged commit 6c1d035 into sveltejs:version-3 Jul 1, 2026
59 of 63 checks passed
Rich-Harris added a commit that referenced this pull request Jul 2, 2026
…g logic (#16208)

Follow-up to #14692. This exposes a `dirty()` method on form fields,
which is separate from `touched()` — it only becomes `true` once a value
is actually edited (whether via user input or programmatically, after
the form has mounted).

It also improves the logic around `myform.validate()` — rather than
validating all fields that have been _touched_ by default, it now only
validates fields that are touched and dirty, meaning the user has edited
a field and then blurred it (or it was set programmatically), which
matches accessibility guidelines.

To validate every field regardless of dirty state, do `myform.validate({
all: true })`.

Draft because I guess we should add a test

---

### Please don't delete this checklist! Before submitting the PR, please
make sure you do the following:
- [x] 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
- [x] This message body should clearly illustrate what problems it
solves.
- [ ] Ideally, include a test that fails without this PR but passes with
it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint` and `pnpm check`

### Changesets
- [x] 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

- [x] Please ensure that 'Allow edits from maintainers' is checked. PRs
without this option may be closed.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Rich-Harris pushed a commit that referenced this pull request Jul 2, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to version-3, this PR
will be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

`version-3` is currently in **pre mode** so this branch has prereleases
rather than normal releases. If you want to exit prereleases, run
`changeset pre exit` on `version-3`.

⚠️⚠️⚠️⚠️⚠️⚠️

# Releases
## @sveltejs/adapter-node@6.0.0-next.2

### Major Changes


- breaking: add `kit.paths.origin` config option, remove
`kit.prerender.origin` and the `adapter-node` `ORIGIN` environment
variable ([#16161](#16161))


### Patch Changes

- Updated dependencies
[[`3c434fb`](3c434fb),
[`a9284e8`](a9284e8),
[`3726a7a`](3726a7a),
[`f9d2240`](f9d2240),
[`a9284e8`](a9284e8),
[`7c040ba`](7c040ba),
[`223eaad`](223eaad),
[`223eaad`](223eaad),
[`3b907d4`](3b907d4),
[`fd628a5`](fd628a5),
[`223eaad`](223eaad),
[`6c1d035`](6c1d035),
[`178eac0`](178eac0),
[`c6562a9`](c6562a9),
[`8eca2ab`](8eca2ab),
[`61cf188`](61cf188)]:
  - @sveltejs/kit@3.0.0-next.6
## @sveltejs/kit@3.0.0-next.6

### Major Changes


- breaking: return no content for 204 responses
([#16200](#16200))


- breaking: form action responses now use the HTTP status code returned
from `fail` ([#16200](#16200))


- breaking: nested server-only directories
([#15685](#15685))


- breaking: add `kit.paths.origin` config option, remove
`kit.prerender.origin` and the `adapter-node` `ORIGIN` environment
variable ([#16161](#16161))


- breaking: don't abort navigation when calling `invalidate(All)` during
navigation ([#16188](#16188))


- breaking: allow `handleError` to influence status code
([#16162](#16162))


- breaking: forbid external redirects by default
([#16198](#16198))


### Minor Changes


- feat: use `type: 'module'` for service worker registrations
([#16169](#16169))


- feat: add `dirty()` property to form fields
([#16208](#16208))


- feat: add `cookies.parse` method
([#16203](#16203))


### Patch Changes


- fix: drain unconsumed request bodies so keep-alive connections don't
hang ([#16170](#16170))


- fix: properly handle Date objects in form.fields.set
([#16168](#16168))


- fix: skip clean fields when programmatically validating forms
([#16208](#16208))


- breaking: experimental remote form `validate({ includeUntouched })`
option is now `all`
([#16208](#16208))


- fix: return `undefined` from `fields.branch.issues()` when only
`fields.branch.leaf` has issues
([#16187](#16187))


- feat: add field.touched() helper to remote form fields
([#14692](#14692))
## @sveltejs/adapter-cloudflare@8.0.0-next.1

### Patch Changes


- fix: avoid overriding user's existing `_headers` rules
([#16183](#16183))

- Updated dependencies
[[`3c434fb`](3c434fb),
[`a9284e8`](a9284e8),
[`3726a7a`](3726a7a),
[`f9d2240`](f9d2240),
[`a9284e8`](a9284e8),
[`7c040ba`](7c040ba),
[`223eaad`](223eaad),
[`223eaad`](223eaad),
[`3b907d4`](3b907d4),
[`fd628a5`](fd628a5),
[`223eaad`](223eaad),
[`6c1d035`](6c1d035),
[`178eac0`](178eac0),
[`c6562a9`](c6562a9),
[`8eca2ab`](8eca2ab),
[`61cf188`](61cf188)]:
  - @sveltejs/kit@3.0.0-next.6

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@teemingc teemingc linked an issue Jul 2, 2026 that may be closed by this pull request
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 needs-decision Not sure if we want to do this yet, also design work needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose touched for remote function form fields

8 participants