Skip to content

Commit e6ebca5

Browse files
committed
Add remote form factory options
1 parent ec5cb13 commit e6ebca5

File tree

8 files changed

+163
-50
lines changed

8 files changed

+163
-50
lines changed

documentation/docs/20-core-concepts/60-remote-functions.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -540,11 +540,15 @@ For client-side validation, you can specify a _preflight_ schema which will popu
540540
title: v.pipe(v.string(), v.nonEmpty()),
541541
content: v.pipe(v.string(), v.nonEmpty())
542542
});
543+
544+
const form = createPost(+++{
545+
preflight: schema
546+
}+++)
543547
</script>
544548

545549
<h1>Create a new post</h1>
546550

547-
<form {...+++createPost().preflight(schema)+++}>
551+
<form {...form}>
548552
<!-- -->
549553
</form>
550554
```
@@ -812,11 +816,43 @@ Some forms may be repeated as part of a list. In this case you can create separa
812816
{@const modify = modifyTodo(todo.id)}
813817
<form {...modify}>
814818
<!-- -->
815-
<button disabled={!!modify.pending}>save changes</button>
819+
<button disabled={!!modify.pending}>Save Changes</button>
816820
</form>
817821
{/each}
818822
```
819823
824+
### Initial form data
825+
826+
There are times when you want a form to be pre-filled with certain values when it first renders. For example, you might want to populate a form with values fetched from the server or set default values for a new data entry.
827+
828+
You can do this by passing the `initialData` option when creating a form instance. This will set the initial state of the form fields, both for their values and for client-side validation.
829+
830+
Here's an example of how to set initial form data using `initialData`:
831+
832+
```svelte
833+
<!--- file: src/routes/edit-post/[postId]/+page.svelte --->
834+
<script>
835+
import { getPost, editPost } from '../data.remote';
836+
837+
const { params } = $props();
838+
839+
// Fetch the data to pre-fill the form
840+
const data = $derived(await getPost(params.postId));
841+
842+
// Pass initialData when creating the form instance
843+
const form = $derived(editPost({
844+
initialData: data
845+
}));
846+
</script>
847+
848+
<form {...form}>
849+
<!-- Render your form fields here, which will use the initial values from `data` -->
850+
<button disabled={!!form.pending}>Save Changes</button>
851+
</form>
852+
```
853+
854+
You can also pass a partial object to `initialData` if you only want to set values for some fields. If `initialData` is omitted, the fields will be empty by default.
855+
820856
### buttonProps
821857
822858
By default, submitting a form will send a request to the URL indicated by the `<form>` element's [`action`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#attributes_for_form_submission) attribute, which in the case of a remote function is a property on the form object generated by SvelteKit.

packages/kit/src/exports/public.d.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
PrerenderUnseenRoutesHandlerValue,
1616
PrerenderOption,
1717
RequestOptions,
18-
RouteSegment
18+
RouteSegment,
19+
DeepPartial,
20+
ExtractId
1921
} from '../types/private.js';
2022
import { BuildData, SSRNodeLoader, SSRRoute, ValidatedConfig } from 'types';
2123
import { SvelteConfig } from '@sveltejs/vite-plugin-svelte';
@@ -1969,14 +1971,6 @@ export interface RemoteFormAllIssue extends RemoteFormIssue {
19691971
path: Array<string | number>;
19701972
}
19711973

1972-
// If the schema specifies `id` as a string or number, ensure that `for(...)`
1973-
// only accepts that type. Otherwise, accept `string | number`
1974-
type ExtractId<Input> = Input extends { id: infer Id }
1975-
? Id extends string | number
1976-
? Id
1977-
: string | number
1978-
: string | number;
1979-
19801974
/**
19811975
* Recursively maps an input type to a structure where each field can create a validation issue.
19821976
* This mirrors the runtime behavior of the `invalid` proxy passed to form handlers.
@@ -2007,8 +2001,19 @@ type InvalidField<T> =
20072001
export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
20082002
InvalidField<Input>;
20092003

2004+
export type RemoteFormFactoryOptions<Input extends RemoteFormInput | void> = {
2005+
/** Optional key to create a scoped instance */
2006+
key?: ExtractId<Input>;
2007+
/** Client-side preflight schema for validation before submit */
2008+
preflight?: StandardSchemaV1<Input, any>;
2009+
/** Initial input values for the form fields */
2010+
initialData?: DeepPartial<Input>;
2011+
/** Reset the form values after successful submission (default: true) */
2012+
resetAfterSuccess?: boolean;
2013+
};
2014+
20102015
export type RemoteFormFactory<Input extends RemoteFormInput | void, Output> = (
2011-
key?: ExtractId<Input>
2016+
keyOrOptions?: ExtractId<Input> | RemoteFormFactoryOptions<Input>
20122017
) => RemoteForm<Input, Output>;
20132018

20142019
/**

packages/kit/src/runtime/app/server/remote/form.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
/** @import { RemoteFormInput, RemoteForm, RemoteFormFactory } from '@sveltejs/kit' */
2-
/** @import { InternalRemoteFormIssue, MaybePromise, RemoteInfo } from 'types' */
1+
/** @import { RemoteFormInput, RemoteForm, RemoteFormFactory, RemoteFormFactoryOptions } from '@sveltejs/kit' */
2+
/** @import { ExtractId, InternalRemoteFormIssue, MaybePromise, RemoteInfo } from 'types' */
33
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
44
import { get_request_store } from '@sveltejs/kit/internal/server';
55
import { DEV } from 'esm-env';
@@ -165,7 +165,8 @@ export function form(validate_or_fn, maybe_fn) {
165165
};
166166

167167
/**
168-
* @param {string | number} [key]
168+
* @param {ExtractId<Input>} [key]
169+
* @returns {RemoteForm<Input, Output>}
169170
*/
170171
function create_instance(key) {
171172
const { state } = get_request_store();
@@ -294,7 +295,22 @@ export function form(validate_or_fn, maybe_fn) {
294295
}
295296

296297
/** @type {RemoteFormFactory<Input, Output>} */
297-
const factory = (key) => create_instance(key);
298+
const factory = (arg) => {
299+
/** @type {RemoteFormFactoryOptions<Input> | undefined } */
300+
const options = arg && typeof arg === 'object' ? arg : undefined;
301+
const key = options ? options.key : /** @type {ExtractId<Input> | undefined} */ (arg);
302+
303+
const instance = create_instance(key);
304+
305+
if (options?.initialData) {
306+
// seed initial input into cache for SSR
307+
const { state } = get_request_store();
308+
const cache = get_cache(/** @type any */ (instance).__, state);
309+
(cache[''] ??= {}).input = options.initialData;
310+
}
311+
312+
return instance;
313+
};
298314

299315
Object.defineProperty(factory, '__', { value: __ });
300316

packages/kit/src/runtime/client/remote-functions/form.svelte.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
2-
/** @import { RemoteFormInput, RemoteForm, RemoteQueryOverride, RemoteFormFactory } from '@sveltejs/kit' */
3-
/** @import { InternalRemoteFormIssue, RemoteFunctionResponse } from 'types' */
2+
/** @import { RemoteFormInput, RemoteForm, RemoteQueryOverride, RemoteFormFactory, RemoteFormFactoryOptions } from '@sveltejs/kit' */
3+
/** @import { ExtractId, InternalRemoteFormIssue, RemoteFunctionResponse } from 'types' */
44
/** @import { Query } from './query.svelte.js' */
55
import { app_dir, base } from '$app/paths/internal/client';
66
import * as devalue from 'devalue';
@@ -53,8 +53,9 @@ export function form(id) {
5353
/** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
5454
const instances = new Map();
5555

56-
/** @param {string | number | boolean} [key] */
57-
function create_instance(key) {
56+
/** @param {RemoteFormFactoryOptions<T>} options */
57+
function create_instance(options) {
58+
const { key, resetAfterSuccess = true } = options;
5859
const action_id = id + (key !== undefined ? `/${JSON.stringify(key)}` : '');
5960
const action = '?/remote=' + encodeURIComponent(action_id);
6061

@@ -411,7 +412,7 @@ export function form(id) {
411412
instance[createAttachmentKey()] = create_attachment(
412413
form_onsubmit(({ submit, form }) =>
413414
submit().then(() => {
414-
if (!issues.$) {
415+
if (!issues.$ && resetAfterSuccess) {
415416
form.reset();
416417
}
417418
})
@@ -452,7 +453,7 @@ export function form(id) {
452453
formaction: action,
453454
onclick: form_action_onclick(({ submit, form }) =>
454455
submit().then(() => {
455-
if (!issues.$) {
456+
if (!issues.$ && resetAfterSuccess) {
456457
form.reset();
457458
}
458459
})
@@ -588,8 +589,26 @@ export function form(id) {
588589
}
589590

590591
/** @type {RemoteFormFactory<T, U>} */
591-
const factory = (key) => {
592-
const entry = instances.get(key) ?? { count: 0, instance: create_instance(key) };
592+
const factory = (arg) => {
593+
/** @type {RemoteFormFactoryOptions<T> | undefined } */
594+
const options = arg && typeof arg === 'object' ? arg : undefined;
595+
const key = options ? options.key : /** @type {ExtractId<T> | undefined} */ (arg);
596+
597+
let entry = instances.get(key);
598+
if (!entry) {
599+
const instance = create_instance(options ?? { key });
600+
601+
if (options?.preflight) {
602+
instance.preflight(options.preflight);
603+
}
604+
// seed optional initial input data
605+
if (options?.initialData) {
606+
instance.fields.set(options.initialData);
607+
}
608+
609+
entry = { count: 0, instance };
610+
instances.set(key, entry);
611+
}
593612

594613
try {
595614
$effect.pre(() => {

packages/kit/src/types/private.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,19 @@ export interface RouteSegment {
241241
}
242242

243243
export type TrailingSlash = 'never' | 'always' | 'ignore';
244+
245+
export type DeepPartial<T> = T extends Record<PropertyKey, unknown> | unknown[]
246+
? {
247+
[K in keyof T]?: T[K] extends Record<PropertyKey, unknown> | unknown[]
248+
? DeepPartial<T[K]>
249+
: T[K];
250+
}
251+
: T | undefined;
252+
253+
// If the schema specifies `id` as a string or number, ensure that `for(...)`
254+
// only accepts that type. Otherwise, accept `string | number`
255+
export type ExtractId<Input> = Input extends { id: infer Id }
256+
? Id extends string | number
257+
? Id
258+
: string | number
259+
: string | number;

packages/kit/test/apps/basics/src/routes/remote/form/+page.svelte

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
99
const message = get_message();
1010
11-
const scoped = set_message('scoped');
11+
const scoped = set_message({
12+
key: 'scoped',
13+
initialData: {
14+
message: 'hello'
15+
},
16+
resetAfterSuccess: false
17+
});
1218
const enhanced = set_message('enhanced');
13-
const message_form = set_message();
14-
const reverse_message_form = set_reverse_message();
15-
const deferreds = resolve_deferreds();
1619
</script>
1720

1821
<p>message.current: {message.current}</p>
@@ -24,20 +27,20 @@
2427

2528
<hr />
2629

27-
<form data-unscoped {...message_form}>
28-
{#if message_form.fields.message.issues()}
29-
<p>{message_form.fields.message.issues()[0].message}</p>
30+
<form data-unscoped {...set_message()}>
31+
{#if set_message().fields.message.issues()}
32+
<p>{set_message().fields.message.issues()[0].message}</p>
3033
{/if}
3134

32-
<input {...message_form.fields.message.as('text')} />
35+
<input {...set_message().fields.message.as('text')} />
3336
<button>set message</button>
34-
<button {...reverse_message_form.buttonProps}>set reverse message</button>
37+
<button {...set_reverse_message().buttonProps}>set reverse message</button>
3538
</form>
3639

37-
<p>set_message.input.message: {message_form.fields.message.value()}</p>
38-
<p>set_message.pending: {message_form.pending}</p>
39-
<p>set_message.result: {message_form.result}</p>
40-
<p>set_reverse_message.result: {reverse_message_form.result}</p>
40+
<p>set_message.input.message: {set_message().fields.message.value()}</p>
41+
<p>set_message.pending: {set_message().pending}</p>
42+
<p>set_message.result: {set_message().result}</p>
43+
<p>set_reverse_message.result: {set_reverse_message().result}</p>
4144

4245
<hr />
4346

@@ -76,6 +79,6 @@
7679

7780
<hr />
7881

79-
<form {...deferreds}>
82+
<form {...resolve_deferreds()}>
8083
<button>resolve deferreds</button>
8184
</form>

packages/kit/test/apps/basics/test/test.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,10 +1772,9 @@ test.describe('remote functions', () => {
17721772
);
17731773
});
17741774

1775-
test('form scoping with for(...) works', async ({ page, javaScriptEnabled }) => {
1775+
test('form scoping works', async ({ page, javaScriptEnabled }) => {
17761776
await page.goto('/remote/form');
17771777

1778-
await page.fill('[data-scoped] input', 'hello');
17791778
await page.getByText('set scoped message').click();
17801779

17811780
if (javaScriptEnabled) {
@@ -1788,7 +1787,7 @@ test.describe('remote functions', () => {
17881787
}
17891788

17901789
await expect(page.getByText('scoped.result')).toHaveText('scoped.result: hello (from: scoped)');
1791-
await expect(page.locator('[data-scoped] input')).toHaveValue('');
1790+
await expect(page.locator('[data-scoped] input')).toHaveValue('hello');
17921791
});
17931792

17941793
test('form enhance(...) works', async ({ page, javaScriptEnabled }) => {

packages/kit/types/index.d.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1945,14 +1945,6 @@ declare module '@sveltejs/kit' {
19451945
path: Array<string | number>;
19461946
}
19471947

1948-
// If the schema specifies `id` as a string or number, ensure that `for(...)`
1949-
// only accepts that type. Otherwise, accept `string | number`
1950-
type ExtractId<Input> = Input extends { id: infer Id }
1951-
? Id extends string | number
1952-
? Id
1953-
: string | number
1954-
: string | number;
1955-
19561948
/**
19571949
* Recursively maps an input type to a structure where each field can create a validation issue.
19581950
* This mirrors the runtime behavior of the `invalid` proxy passed to form handlers.
@@ -1983,8 +1975,19 @@ declare module '@sveltejs/kit' {
19831975
export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
19841976
InvalidField<Input>;
19851977

1978+
export type RemoteFormFactoryOptions<Input extends RemoteFormInput | void> = {
1979+
/** Optional key to create a scoped instance */
1980+
key?: ExtractId<Input>;
1981+
/** Client-side preflight schema for validation before submit */
1982+
preflight?: StandardSchemaV1<Input, any>;
1983+
/** Initial input values for the form fields */
1984+
initialData?: DeepPartial<Input>;
1985+
/** Reset the form values after successful submission (default: true) */
1986+
resetAfterSuccess?: boolean;
1987+
};
1988+
19861989
export type RemoteFormFactory<Input extends RemoteFormInput | void, Output> = (
1987-
key?: ExtractId<Input>
1990+
keyOrOptions?: ExtractId<Input> | RemoteFormFactoryOptions<Input>
19881991
) => RemoteForm<Input, Output>;
19891992

19901993
/**
@@ -2368,6 +2371,22 @@ declare module '@sveltejs/kit' {
23682371
}
23692372

23702373
type TrailingSlash = 'never' | 'always' | 'ignore';
2374+
2375+
type DeepPartial<T> = T extends Record<PropertyKey, unknown> | unknown[]
2376+
? {
2377+
[K in keyof T]?: T[K] extends Record<PropertyKey, unknown> | unknown[]
2378+
? DeepPartial<T[K]>
2379+
: T[K];
2380+
}
2381+
: T | undefined;
2382+
2383+
// If the schema specifies `id` as a string or number, ensure that `for(...)`
2384+
// only accepts that type. Otherwise, accept `string | number`
2385+
type ExtractId<Input> = Input extends { id: infer Id }
2386+
? Id extends string | number
2387+
? Id
2388+
: string | number
2389+
: string | number;
23712390
interface Asset {
23722391
file: string;
23732392
size: number;

0 commit comments

Comments
 (0)