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..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, @@ -2052,6 +2053,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?: DeepPartial | 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/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 7bc7259185e9..090bfdb80d1a 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -267,6 +267,20 @@ 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 02bbf9239720..1b524933d885 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/src/types/private.d.ts b/packages/kit/src/types/private.d.ts index da512ed777c5..9345bdc0e522 100644 --- a/packages/kit/src/types/private.d.ts +++ b/packages/kit/src/types/private.d.ts @@ -241,3 +241,11 @@ export interface RouteSegment { } export type TrailingSlash = 'never' | 'always' | 'ignore'; + +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/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..2150033fed55 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/form/reset/+page.svelte @@ -0,0 +1,20 @@ + + +
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..62c44c3045a6 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -2018,6 +2018,46 @@ test.describe('remote functions', () => { await page.fill('input', 'hello'); await expect(page.locator('select')).toHaveValue('one'); }); + + test('form resets programmatically', async ({ page, javaScriptEnabled }) => { + await page.goto('/remote/form/reset'); + + // SSR case + await expect(page.locator('#value')).toHaveText('"hi"'); + + if (!javaScriptEnabled) return; + + // Error case + 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..f4f9d1f2b862 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?: DeepPartial | 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 */ @@ -2375,6 +2390,14 @@ declare module '@sveltejs/kit' { } type TrailingSlash = 'never' | 'always' | 'ignore'; + + 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;