Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
01a4071
feat: remote form factory
sillvva Oct 26, 2025
e9f0fe4
Merge branch 'sveltejs:main' into form-factory
sillvva Oct 26, 2025
de70ffe
simplify types
sillvva Oct 26, 2025
13ebc2c
omit type from instance
sillvva Oct 26, 2025
112b692
fix types
sillvva Oct 26, 2025
49deca3
remove unused type
sillvva Oct 26, 2025
63b2af1
fix form tests
sillvva Oct 26, 2025
32f923e
fix
sillvva Oct 26, 2025
f9c1d46
fix instance caching for multiple factory invocations with the same key
sillvva Oct 26, 2025
aa3f8e1
remove console.log
sillvva Oct 27, 2025
ec5cb13
Merge branch 'main' into form-factory
sillvva Oct 28, 2025
e6ebca5
Add remote form factory options
sillvva Nov 3, 2025
05cca10
remove preflight method in favor of option, simplify factory
sillvva Nov 3, 2025
2a69677
clarify remote form type descriptions
sillvva Nov 3, 2025
dcac882
optimize form instance caching + reactivity fix
sillvva Nov 8, 2025
f37acfe
Merge branch 'main' into form-factory
sillvva Nov 8, 2025
77148c9
reverse server instance caching
sillvva Nov 8, 2025
ddd0f89
improves form instance key uniqueness
sillvva Nov 9, 2025
f4c69c7
Merge branch 'main' into form-factory
sillvva Nov 20, 2025
2b6901b
fix: add type assertion for initialData in form function
sillvva Nov 20, 2025
799be57
format
sillvva Nov 20, 2025
d6d4236
Merge branch 'main' into form-factory
sillvva Nov 20, 2025
3db7f4c
Merge branch 'main' into form-factory
sillvva Nov 20, 2025
6ebd606
fix instance handler
sillvva Nov 20, 2025
b914a92
fix test
sillvva Nov 20, 2025
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/all-symbols-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: remote form factory
118 changes: 77 additions & 41 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export const createPost = form(

<h1>Create a new post</h1>

<form {...createPost}>
<form {...createPost()}>
<!-- form content goes here -->

<button>Publish!</button>
Expand All @@ -308,15 +308,15 @@ As with `query`, if the callback uses the submitted `data`, it should be [valida
A form is composed of a set of _fields_, which are defined by the schema. In the case of `createPost`, we have two fields, `title` and `content`, which are both strings. To get the attributes for a field, call its `.as(...)` method, specifying which [input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#input_types) to use:

```svelte
<form {...createPost}>
<form {...createPost()}>
<label>
<h2>Title</h2>
+++<input {...createPost.fields.title.as('text')} />+++
+++<input {...createPost().fields.title.as('text')} />+++
</label>

<label>
<h2>Write your post</h2>
+++<textarea {...createPost.fields.content.as('text')}></textarea>+++
+++<textarea {...createPost().fields.content.as('text')}></textarea>+++
</label>

<button>Publish!</button>
Expand Down Expand Up @@ -353,10 +353,10 @@ export const createProfile = form(datingProfile, (data) => { /* ... */ });
<script>
import { createProfile } from './data.remote';

const { name, photo, info, attributes } = createProfile.fields;
const { name, photo, info, attributes } = createProfile().fields;
</script>

<form {...createProfile} enctype="multipart/form-data">
<form {...createProfile()} enctype="multipart/form-data">
<label>
<input {...name.as('text')} /> Name
</label>
Expand Down Expand Up @@ -403,12 +403,12 @@ export const survey = form(
```

```svelte
<form {...survey}>
<form {...survey()}>
<h2>Which operating system do you use?</h2>

{#each ['windows', 'mac', 'linux'] as os}
<label>
<input {...survey.fields.operatingSystem.as('radio', os)}>
<input {...survey().fields.operatingSystem.as('radio', os)}>
{os}
</label>
{/each}
Expand All @@ -417,7 +417,7 @@ export const survey = form(

{#each ['html', 'css', 'js'] as language}
<label>
<input {...survey.fields.languages.as('checkbox', language)}>
<input {...survey().fields.languages.as('checkbox', language)}>
{language}
</label>
{/each}
Expand All @@ -429,18 +429,18 @@ export const survey = form(
Alternatively, you could use `select` and `select multiple`:

```svelte
<form {...survey}>
<form {...survey()}>
<h2>Which operating system do you use?</h2>

<select {...survey.fields.operatingSystem.as('select')}>
<select {...survey().fields.operatingSystem.as('select')}>
<option>windows</option>
<option>mac</option>
<option>linux</option>
</select>

<h2>Which languages do you write code in?</h2>

<select {...survey.fields.languages.as('select multiple')}>
<select {...survey().fields.languages.as('select multiple')}>
<option>html</option>
<option>css</option>
<option>js</option>
Expand Down Expand Up @@ -495,25 +495,25 @@ The `invalid` function works as both a function and a proxy:
If the submitted data doesn't pass the schema, the callback will not run. Instead, each invalid field's `issues()` method will return an array of `{ message: string }` objects, and the `aria-invalid` attribute (returned from `as(...)`) will be set to `true`:

```svelte
<form {...createPost}>
<form {...createPost()}>
<label>
<h2>Title</h2>

+++ {#each createPost.fields.title.issues() as issue}
+++ {#each createPost().fields.title.issues() as issue}
<p class="issue">{issue.message}</p>
{/each}+++

<input {...createPost.fields.title.as('text')} />
<input {...createPost().fields.title.as('text')} />
</label>

<label>
<h2>Write your post</h2>

+++ {#each createPost.fields.content.issues() as issue}
+++ {#each createPost().fields.content.issues() as issue}
<p class="issue">{issue.message}</p>
{/each}+++

<textarea {...createPost.fields.content.as('text')}></textarea>
<textarea {...createPost().fields.content.as('text')}></textarea>
</label>

<button>Publish!</button>
Expand All @@ -523,7 +523,7 @@ If the submitted data doesn't pass the schema, the callback will not run. Instea
You don't need to wait until the form is submitted to validate the data — you can call `validate()` programmatically, for example in an `oninput` callback (which will validate the data on every keystroke) or an `onchange` callback:

```svelte
<form {...createPost} oninput={() => createPost.validate()}>
<form {...createPost()} oninput={() => form.validate()}>
<!-- -->
</form>
```
Expand All @@ -541,11 +541,15 @@ For client-side validation, you can specify a _preflight_ schema which will popu
title: v.pipe(v.string(), v.nonEmpty()),
content: v.pipe(v.string(), v.nonEmpty())
});

const form = createPost(+++{
preflight: schema
}+++)
</script>

<h1>Create a new post</h1>

<form {...+++createPost.preflight(schema)+++}>
<form {...form}>
<!-- -->
</form>
```
Expand All @@ -555,7 +559,7 @@ For client-side validation, you can specify a _preflight_ schema which will popu
To get a list of _all_ issues, rather than just those belonging to a single field, you can use the `fields.allIssues()` method:

```svelte
{#each createPost.fields.allIssues() as issue}
{#each createPost().fields.allIssues() as issue}
<p>{issue.message}</p>
{/each}
```
Expand All @@ -565,17 +569,17 @@ To get a list of _all_ issues, rather than just those belonging to a single fiel
Each field has a `value()` method that reflects its current value. As the user interacts with the form, it is automatically updated:

```svelte
<form {...createPost}>
<form {...createPost()}>
<!-- -->
</form>

<div class="preview">
<h2>{createPost.fields.title.value()}</h2>
<div>{@html render(createPost.fields.content.value())}</div>
<h2>{createPost().fields.title.value()}</h2>
<div>{@html render(createPost().fields.content.value())}</div>
</div>
```

Alternatively, `createPost.fields.value()` would return a `{ title, content }` object.
Alternatively, `createPost().fields.value()` would return a `{ title, content }` object.

You can update a field (or a collection of fields) via the `set(...)` method:

Expand All @@ -584,14 +588,14 @@ You can update a field (or a collection of fields) via the `set(...)` method:
import { createPost } from '../data.remote';

// this...
createPost.fields.set({
createPost().fields.set({
title: 'My new blog post',
content: 'Lorem ipsum dolor sit amet...'
});

// ...is equivalent to this:
createPost.fields.title.set('My new blog post');
createPost.fields.content.set('Lorem ipsum dolor sit amet');
createPost().fields.title.set('My new blog post');
createPost().fields.content.set('Lorem ipsum dolor sit amet');
</script>
```

Expand All @@ -602,15 +606,15 @@ In the case of a non-progressively-enhanced form submission (i.e. where JavaScri
You can prevent sensitive data (such as passwords and credit card numbers) from being sent back to the user by using a name with a leading underscore:

```svelte
<form {...register}>
<form {...register()}>
<label>
Username
<input {...register.fields.username.as('text')} />
<input {...register().fields.username.as('text')} />
</label>

<label>
Password
<input +++{...register.fields._password.as('password')}+++ />
<input +++{...register().fields._password.as('password')}+++ />
</label>

<button>Sign up!</button>
Expand Down Expand Up @@ -669,7 +673,7 @@ The second is to drive the single-flight mutation from the client, which we'll s

### Returns and redirects

The example above uses [`redirect(...)`](@sveltejs-kit#redirect), which sends the user to the newly created page. Alternatively, the callback could return data, in which case it would be available as `createPost.result`:
The example above uses [`redirect(...)`](@sveltejs-kit#redirect), which sends the user to the newly created page. Alternatively, the callback could return data, in which case it would be available as `createPost().result`:

```ts
/// file: src/routes/blog/data.remote.js
Expand Down Expand Up @@ -718,11 +722,11 @@ export const createPost = form(

<h1>Create a new post</h1>

<form {...createPost}>
<form {...createPost()}>
<!-- -->
</form>

{#if createPost.result?.success}
{#if createPost().result?.success}
<p>Successfully published!</p>
{/if}
```
Expand All @@ -746,7 +750,7 @@ We can customize what happens when the form is submitted with the `enhance` meth

<h1>Create a new post</h1>

<form {...createPost.enhance(async ({ form, data, submit }) => {
<form {...createPost().enhance(async ({ form, data, submit }) => {
try {
await submit();
form.reset();
Expand Down Expand Up @@ -799,7 +803,7 @@ The override will be applied immediately, and released when the submission compl

### Multiple instances of a form

Some forms may be repeated as part of a list. In this case you can create separate instances of a form function via `for(id)` to achieve isolation.
Some forms may be repeated as part of a list. In this case you can create separate instances of a form function via `form(id)` to achieve isolation.

```svelte
<!--- file: src/routes/todos/+page.svelte --->
Expand All @@ -810,14 +814,46 @@ Some forms may be repeated as part of a list. In this case you can create separa
<h1>Todos</h1>

{#each await getTodos() as todo}
{@const modify = modifyTodo.for(todo.id)}
{@const modify = modifyTodo(todo.id)}
<form {...modify}>
<!-- -->
<button disabled={!!modify.pending}>save changes</button>
<button disabled={!!modify.pending}>Save Changes</button>
</form>
{/each}
```

### Initial form data

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.

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.

Here's an example of how to set initial form data using `initialData`:

```svelte
<!--- file: src/routes/edit-post/[postId]/+page.svelte --->
<script>
import { getPost, editPost } from '../data.remote';

const { params } = $props();

// Fetch the data to pre-fill the form
const data = $derived(await getPost(params.postId));

// Pass initialData when creating the form instance
const form = $derived(editPost({
initialData: data
}));
</script>

<form {...form}>
<!-- Render your form fields here, which will use the initial values from `data` -->
<button disabled={!!form.pending}>Save Changes</button>
</form>
```

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.

### buttonProps

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.
Expand All @@ -832,19 +868,19 @@ This attribute exists on the `buttonProps` property of a form object:
import { login, register } from '$lib/auth';
</script>

<form {...login}>
<form {...login()}>
<label>
Your username
<input {...login.fields.username.as('text')} />
<input {...login().fields.username.as('text')} />
</label>

<label>
Your password
<input {...login.fields._password.as('password')} />
<input {...login().fields._password.as('password')} />
</label>

<button>login</button>
<button {...register.buttonProps}>register</button>
<button {...register().buttonProps}>register</button>
</form>
```

Expand Down
Loading
Loading