From e0a9ed4a1e54475649071077fd835dd85aa4852c Mon Sep 17 00:00:00 2001 From: Matt DeKok Date: Mon, 20 Oct 2025 23:11:35 -0500 Subject: [PATCH 1/7] feat: reset form programmatically --- .changeset/full-steaks-count.md | 5 +++ .../20-core-concepts/60-remote-functions.md | 40 +++++++++++++++++++ packages/kit/src/exports/public.d.ts | 15 +++++++ .../client/remote-functions/form.svelte.js | 20 ++++++++++ .../src/routes/remote/form/reset/+page.svelte | 19 +++++++++ .../routes/remote/form/reset/form.remote.ts | 12 ++++++ packages/kit/test/apps/basics/test/test.js | 38 ++++++++++++++++++ packages/kit/types/index.d.ts | 15 +++++++ 8 files changed, 164 insertions(+) create mode 100644 .changeset/full-steaks-count.md create mode 100644 packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/remote/form/reset/form.remote.ts diff --git a/.changeset/full-steaks-count.md b/.changeset/full-steaks-count.md new file mode 100644 index 000000000000..df4c781b47fa --- /dev/null +++ b/.changeset/full-steaks-count.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: reset form programmatically diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index 969b90d17121..aaa72faea9d5 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -847,6 +847,46 @@ This attribute exists on the `buttonProps` property of a form object: Like the form object itself, `buttonProps` has an `enhance` method for customizing submission behaviour. +### Resetting the form programmatically + +You can programmatically reset a form using the `reset()` method. When called with no arguments, `reset()` will: + +- Reset the HTML form element (clearing all input values) +- Clear all validation issues +- Clear the `result` value +- Clear all touched field tracking +- Set `submitted` to `false` + +```svelte + + + +
+ + + +
+ + +``` + +The available options are: + +- `values` — Set to `true` (default) to reset the HTML form element, `false` to keep current values, or pass an object with partial values to reset to specific values +- `issues` — Set to `false` to preserve validation issues (default is `true`) +- `result` — Set to `false` to preserve the result value (default is `true`) +- `touched` — Set to `false` to preserve touched field tracking (default is `true`) + +```svelte + +``` + ## command The `command` function, like `form`, allows you to write data to the server. Unlike `form`, it's not specific to an element and can be called from anywhere. diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index cb42dc6cc1c0..0886541c8c24 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -2052,6 +2052,21 @@ export type RemoteForm = { /** Perform validation as if the form was submitted by the given button. */ submitter?: HTMLButtonElement | HTMLInputElement; }): Promise; + /** Reset the form to its initial state */ + reset(options?: { + /** + * Set this to the new values to reset the form to. + * Set this to `false` to not reset the values. + * @default true + */ + values?: Partial | boolean; + /** Set this to `false` to not reset the issues. */ + issues?: boolean; + /** Set this to `false` to not reset the result. */ + result?: boolean; + /** Set this to `false` to not reset the touched fields. */ + touched?: boolean; + }): void; /** The result of the form submission */ get result(): Output | undefined; /** The number of pending submissions */ diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index 02bbf9239720..6f7c666ee224 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -572,6 +572,26 @@ export function form(id) { : merge_with_server_issues(form_data, raw_issues, array); } }, + reset: { + /** @type {RemoteForm['reset']} */ + value: ({ + values = true, + issues: resetIssues = true, + result: resetResult = true, + touched: resetTouched = true + } = {}) => { + submitted = false; + + if (values === true) { + if (element) element.reset(); + else input = {}; + } else if (values) input = values; + + if (resetIssues) raw_issues = []; + if (resetResult) result = undefined; + if (resetTouched) touched = {}; + } + }, enhance: { /** @type {RemoteForm['enhance']} */ value: (callback) => { diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte new file mode 100644 index 000000000000..8bc48e4fc7da --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte @@ -0,0 +1,19 @@ + + +
submit())}> + + +
+ + + + +
{JSON.stringify(test.result)}
+
{JSON.stringify(test.fields.value.value())}
+
{JSON.stringify(test.fields.allIssues())}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/reset/form.remote.ts b/packages/kit/test/apps/basics/src/routes/remote/form/reset/form.remote.ts new file mode 100644 index 000000000000..fbf5f9b050ff --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/form/reset/form.remote.ts @@ -0,0 +1,12 @@ +import { form } from '$app/server'; +import * as v from 'valibot'; + +export const test = form( + v.object({ + value: v.pipe(v.string(), v.minLength(3)) + }), + async (data) => { + console.log(data); + return data; + } +); diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 9b57a59b79d7..f6a7eef10ef5 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -2018,6 +2018,44 @@ test.describe('remote functions', () => { await page.fill('input', 'hello'); await expect(page.locator('select')).toHaveValue('one'); }); + + test('form resets programmatically', async ({ page, javaScriptEnabled }) => { + if (!javaScriptEnabled) return; + + await page.goto('/remote/form/reset'); + + // Error case + await page.locator('#input').fill('hi'); + await page.locator('#submit').click(); + await expect(page.locator('#value')).toHaveText('"hi"'); + await expect(page.locator('#result')).toHaveText(''); + await expect(page.locator('#allIssues')).toHaveText( + '[{"message":"Invalid length: Expected >=3 but received 2"}]' + ); + + await page.locator('#partial-reset').click(); + await expect(page.locator('#value')).toHaveText('""'); + await expect(page.locator('#allIssues')).toHaveText( + '[{"message":"Invalid length: Expected >=3 but received 2"}]' + ); + + await page.locator('#full-reset').click(); + await expect(page.locator('#allIssues')).toHaveText(''); + + // Result case + await page.locator('#input').fill('hello'); + await page.locator('#submit').click(); + await expect(page.locator('#value')).toHaveText('"hello"'); + await expect(page.locator('#result')).toHaveText('{"value":"hello"}'); + await expect(page.locator('#allIssues')).toHaveText(''); + + await page.locator('#partial-reset').click(); + await expect(page.locator('#value')).toHaveText('""'); + await expect(page.locator('#result')).toHaveText('{"value":"hello"}'); + + await page.locator('#full-reset').click(); + await expect(page.locator('#result')).toHaveText(''); + }); }); test.describe('params prop', () => { diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 9fde95c8dd4c..f58381b71cb3 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2028,6 +2028,21 @@ declare module '@sveltejs/kit' { /** Perform validation as if the form was submitted by the given button. */ submitter?: HTMLButtonElement | HTMLInputElement; }): Promise; + /** Reset the form to its initial state */ + reset(options?: { + /** + * Set this to the new values to reset the form to. + * Set this to `false` to not reset the values. + * @default true + */ + values?: Partial | boolean; + /** Set this to `false` to not reset the issues. */ + issues?: boolean; + /** Set this to `false` to not reset the result. */ + result?: boolean; + /** Set this to `false` to not reset the touched fields. */ + touched?: boolean; + }): void; /** The result of the form submission */ get result(): Output | undefined; /** The number of pending submissions */ From 22a25b93ab167650113dda2e49c31133a3836fc3 Mon Sep 17 00:00:00 2001 From: Matt DeKok Date: Mon, 20 Oct 2025 23:35:43 -0500 Subject: [PATCH 2/7] SSR support + test --- .../kit/src/runtime/app/server/remote/form.js | 18 ++++++++++++++++++ .../client/remote-functions/form.svelte.js | 1 + .../src/routes/remote/form/reset/+page.svelte | 1 + packages/kit/test/apps/basics/test/test.js | 8 +++++--- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 7bc7259185e9..7e013c84ae0b 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -267,6 +267,24 @@ export function form(validate_or_fn, maybe_fn) { } }); + Object.defineProperty(instance, 'reset', { + /** @type {RemoteForm['reset']} */ + value: ({ + values = true, + issues = true, + result = true + } = {}) => { + const cache = (get_cache(__)[''] ??= {}); + + if (values === true) { + cache.input = {}; + } else if (values) cache.input = values; + + if (issues) cache.issues = []; + if (result) cache.result = undefined; + } + }); + if (key == undefined) { Object.defineProperty(instance, 'for', { /** @type {RemoteForm['for']} */ diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index 6f7c666ee224..68eab5ad1b59 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -581,6 +581,7 @@ export function form(id) { touched: resetTouched = true } = {}) => { submitted = false; + console.log('reset', values, resetIssues, resetResult, resetTouched); if (values === true) { if (element) element.reset(); diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte index 8bc48e4fc7da..2150033fed55 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte @@ -1,5 +1,6 @@
submit())}> diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index f6a7eef10ef5..62c44c3045a6 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -2020,12 +2020,14 @@ test.describe('remote functions', () => { }); test('form resets programmatically', async ({ page, javaScriptEnabled }) => { - if (!javaScriptEnabled) return; - await page.goto('/remote/form/reset'); + // SSR case + await expect(page.locator('#value')).toHaveText('"hi"'); + + if (!javaScriptEnabled) return; + // Error case - await page.locator('#input').fill('hi'); await page.locator('#submit').click(); await expect(page.locator('#value')).toHaveText('"hi"'); await expect(page.locator('#result')).toHaveText(''); From 4a3f9da8b4713ff96564c6993ca2dee3edf4015a Mon Sep 17 00:00:00 2001 From: Matt DeKok Date: Tue, 21 Oct 2025 07:41:25 -0500 Subject: [PATCH 3/7] format --- packages/kit/src/runtime/app/server/remote/form.js | 8 ++------ .../src/runtime/client/remote-functions/form.svelte.js | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 7e013c84ae0b..090bfdb80d1a 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -269,13 +269,9 @@ export function form(validate_or_fn, maybe_fn) { Object.defineProperty(instance, 'reset', { /** @type {RemoteForm['reset']} */ - value: ({ - values = true, - issues = true, - result = true - } = {}) => { + value: ({ values = true, issues = true, result = true } = {}) => { const cache = (get_cache(__)[''] ??= {}); - + if (values === true) { cache.input = {}; } else if (values) cache.input = values; diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index 68eab5ad1b59..82c9a7c0fcce 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -590,7 +590,7 @@ export function form(id) { if (resetIssues) raw_issues = []; if (resetResult) result = undefined; - if (resetTouched) touched = {}; + if (resetTouched) touched = {}; } }, enhance: { From 16f8998855741389c1013994ae6ca290b66b1057 Mon Sep 17 00:00:00 2001 From: Matt DeKok Date: Tue, 21 Oct 2025 07:41:33 -0500 Subject: [PATCH 4/7] deep partial --- packages/kit/src/exports/public.d.ts | 6 +++++- packages/kit/types/index.d.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 0886541c8c24..0852f98d7fda 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -2003,6 +2003,10 @@ type InvalidField = export type Invalid = ((...issues: Array) => never) & InvalidField; +type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; +}; + /** * The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation. */ @@ -2059,7 +2063,7 @@ export type RemoteForm = { * Set this to `false` to not reset the values. * @default true */ - values?: Partial | boolean; + values?: DeepPartial | boolean; /** Set this to `false` to not reset the issues. */ issues?: boolean; /** Set this to `false` to not reset the result. */ diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index f58381b71cb3..d5d08f846763 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1979,6 +1979,10 @@ declare module '@sveltejs/kit' { export type Invalid = ((...issues: Array) => never) & InvalidField; + type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; + }; + /** * The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation. */ @@ -2030,12 +2034,12 @@ declare module '@sveltejs/kit' { }): Promise; /** Reset the form to its initial state */ reset(options?: { - /** + /** * Set this to the new values to reset the form to. * Set this to `false` to not reset the values. * @default true */ - values?: Partial | boolean; + values?: DeepPartial | boolean; /** Set this to `false` to not reset the issues. */ issues?: boolean; /** Set this to `false` to not reset the result. */ From 786dbd555dcc9c8a1fb93e1ad3053de009a3f0fe Mon Sep 17 00:00:00 2001 From: Matt DeKok Date: Tue, 21 Oct 2025 14:32:51 -0500 Subject: [PATCH 5/7] Remove console log --- packages/kit/src/runtime/client/remote-functions/form.svelte.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index 82c9a7c0fcce..1b524933d885 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -581,7 +581,6 @@ export function form(id) { touched: resetTouched = true } = {}) => { submitted = false; - console.log('reset', values, resetIssues, resetResult, resetTouched); if (values === true) { if (element) element.reset(); From a4dd2ec9f2374e1b7b6063de7f36743b4711c90b Mon Sep 17 00:00:00 2001 From: Matt DeKok Date: Mon, 27 Oct 2025 21:53:26 -0500 Subject: [PATCH 6/7] move DeepPartial type definition to private.d.ts --- packages/kit/src/exports/public.d.ts | 5 +---- packages/kit/src/types/private.d.ts | 4 ++++ packages/kit/types/index.d.ts | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 0852f98d7fda..840eddaed7ed 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -5,6 +5,7 @@ import '../types/ambient.js'; import { AdapterEntry, CspDirectives, + DeepPartial, HttpMethod, Logger, MaybePromise, @@ -2003,10 +2004,6 @@ type InvalidField = export type Invalid = ((...issues: Array) => never) & InvalidField; -type DeepPartial = { - [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; -}; - /** * The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation. */ diff --git a/packages/kit/src/types/private.d.ts b/packages/kit/src/types/private.d.ts index da512ed777c5..746058787a6d 100644 --- a/packages/kit/src/types/private.d.ts +++ b/packages/kit/src/types/private.d.ts @@ -241,3 +241,7 @@ export interface RouteSegment { } export type TrailingSlash = 'never' | 'always' | 'ignore'; + +export type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; +}; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index d5d08f846763..b6e6cffed5a5 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1979,10 +1979,6 @@ declare module '@sveltejs/kit' { export type Invalid = ((...issues: Array) => never) & InvalidField; - type DeepPartial = { - [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; - }; - /** * The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation. */ @@ -2394,6 +2390,10 @@ declare module '@sveltejs/kit' { } type TrailingSlash = 'never' | 'always' | 'ignore'; + + type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; + }; interface Asset { file: string; size: number; From 8562ffdd1292caac32e02545af82e46156712744 Mon Sep 17 00:00:00 2001 From: Matt DeKok Date: Tue, 28 Oct 2025 10:34:26 -0500 Subject: [PATCH 7/7] fix `DeepPartial` type --- packages/kit/src/types/private.d.ts | 10 +++++++--- packages/kit/types/index.d.ts | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/kit/src/types/private.d.ts b/packages/kit/src/types/private.d.ts index 746058787a6d..9345bdc0e522 100644 --- a/packages/kit/src/types/private.d.ts +++ b/packages/kit/src/types/private.d.ts @@ -242,6 +242,10 @@ export interface RouteSegment { export type TrailingSlash = 'never' | 'always' | 'ignore'; -export type DeepPartial = { - [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; -}; +export type DeepPartial = T extends Record | unknown[] + ? { + [K in keyof T]?: T[K] extends Record | unknown[] + ? DeepPartial + : T[K]; + } + : T | undefined; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b6e6cffed5a5..f4f9d1f2b862 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2391,9 +2391,13 @@ declare module '@sveltejs/kit' { type TrailingSlash = 'never' | 'always' | 'ignore'; - type DeepPartial = { - [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; - }; + type DeepPartial = T extends Record | unknown[] + ? { + [K in keyof T]?: T[K] extends Record | unknown[] + ? DeepPartial + : T[K]; + } + : T | undefined; interface Asset { file: string; size: number;