Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/full-steaks-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: reset form programmatically
40 changes: 40 additions & 0 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<!--- file: +page.svelte --->
<script>
import { login } from './auth.remote';
</script>

<form {...login}>
<input {...login.fields.username.as('text')} />
<input {...login.fields._password.as('password')} />
<button>login</button>
</form>

<button onclick={() => login.reset()}>
Clear form
</button>
```

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
<button onclick={() => login.reset({ values: { username: 'guest', password: 'guest' } })}>
Login as guest
</button>
```

## 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.
Expand Down
16 changes: 16 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '../types/ambient.js';
import {
AdapterEntry,
CspDirectives,
DeepPartial,
HttpMethod,
Logger,
MaybePromise,
Expand Down Expand Up @@ -2052,6 +2053,21 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
/** Perform validation as if the form was submitted by the given button. */
submitter?: HTMLButtonElement | HTMLInputElement;
}): Promise<void>;
/** 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<Input> | 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 */
Expand Down
14 changes: 14 additions & 0 deletions packages/kit/src/runtime/app/server/remote/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,20 @@ export function form(validate_or_fn, maybe_fn) {
}
});

Object.defineProperty(instance, 'reset', {
/** @type {RemoteForm<any, any>['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<any, any>['for']} */
Expand Down
20 changes: 20 additions & 0 deletions packages/kit/src/runtime/client/remote-functions/form.svelte.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,26 @@ export function form(id) {
: merge_with_server_issues(form_data, raw_issues, array);
}
},
reset: {
/** @type {RemoteForm<any, any>['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<any, any>['enhance']} */
value: (callback) => {
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/types/private.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,11 @@ export interface RouteSegment {
}

export type TrailingSlash = 'never' | 'always' | 'ignore';

export type DeepPartial<T> = T extends Record<PropertyKey, unknown> | unknown[]
? {
[K in keyof T]?: T[K] extends Record<PropertyKey, unknown> | unknown[]
? DeepPartial<T[K]>
: T[K];
}
: T | undefined;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { test } from './form.remote.js';
test.reset({ values: { value: 'hi' } });
</script>

<form {...test.enhance(({ submit }) => submit())}>
<input id="input" {...test.fields.value.as('text')} />
<button type="submit" id="submit">Submit</button>
</form>

<button type="button" id="full-reset" onclick={() => test.reset()}>Reset</button>
<button
type="button"
id="partial-reset"
onclick={() => test.reset({ issues: false, result: false })}>Partial Reset</button
>

<pre id="result">{JSON.stringify(test.result)}</pre>
<pre id="value">{JSON.stringify(test.fields.value.value())}</pre>
<pre id="allIssues">{JSON.stringify(test.fields.allIssues())}</pre>
Original file line number Diff line number Diff line change
@@ -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;
}
);
40 changes: 40 additions & 0 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
23 changes: 23 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2028,6 +2028,21 @@ declare module '@sveltejs/kit' {
/** Perform validation as if the form was submitted by the given button. */
submitter?: HTMLButtonElement | HTMLInputElement;
}): Promise<void>;
/** 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<Input> | 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 */
Expand Down Expand Up @@ -2375,6 +2390,14 @@ declare module '@sveltejs/kit' {
}

type TrailingSlash = 'never' | 'always' | 'ignore';

type DeepPartial<T> = T extends Record<PropertyKey, unknown> | unknown[]
? {
[K in keyof T]?: T[K] extends Record<PropertyKey, unknown> | unknown[]
? DeepPartial<T[K]>
: T[K];
}
: T | undefined;
interface Asset {
file: string;
size: number;
Expand Down
Loading