Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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/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
105 changes: 65 additions & 40 deletions documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,12 @@ export const createPost = form(
<!--- file: src/routes/blog/new/+page.svelte --->
<script>
import { createPost } from '../data.remote';
const form = createPost();
</script>

<h1>Create a new post</h1>

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

<button>Publish!</button>
Expand All @@ -308,15 +309,20 @@ 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}>
<script>
import { createPost } from '../data.remote';
const form = createPost();
</script>

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

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

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

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

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

```svelte
<form {...survey}>
<script>
import { survey } from '../data.remote';
const form = survey();
</script>

<form {...form}>
<h2>Which operating system do you use?</h2>

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

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

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

<select {...survey.fields.operatingSystem.as('select')}>
<select {...form.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 {...form.fields.languages.as('select multiple')}>
<option>html</option>
<option>css</option>
<option>js</option>
Expand Down Expand Up @@ -492,25 +504,30 @@ 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}>
<script>
import { createPost } from '../data.remote';
const form = createPost();
</script>

<form {...form}>
<label>
<h2>Title</h2>

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

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

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

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

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

<button>Publish!</button>
Expand All @@ -520,7 +537,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 {...form} oninput={() => form.validate()}>
<!-- -->
</form>
```
Expand All @@ -534,6 +551,8 @@ For client-side validation, you can specify a _preflight_ schema which will popu
import * as v from 'valibot';
import { createPost } from '../data.remote';

const form = createPost();

const schema = v.object({
title: v.pipe(v.string(), v.nonEmpty()),
content: v.pipe(v.string(), v.nonEmpty())
Expand All @@ -542,7 +561,7 @@ For client-side validation, you can specify a _preflight_ schema which will popu

<h1>Create a new post</h1>

<form {...+++createPost.preflight(schema)+++}>
<form {...+++form.preflight(schema)+++}>
<!-- -->
</form>
```
Expand All @@ -552,7 +571,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 form.fields.allIssues() as issue}
<p>{issue.message}</p>
{/each}
```
Expand All @@ -562,33 +581,35 @@ 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 {...form}>
<!-- -->
</form>

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

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

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

```svelte
<script>
import { createPost } from '../data.remote';

const form = createPost();

// this...
createPost.fields.set({
form.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');
form.fields.title.set('My new blog post');
form.fields.content.set('Lorem ipsum dolor sit amet');
</script>
```

Expand All @@ -599,15 +620,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 {...form}>
<label>
Username
<input {...register.fields.username.as('text')} />
<input {...form.fields.username.as('text')} />
</label>

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

<button>Sign up!</button>
Expand Down Expand Up @@ -666,7 +687,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 `form.result`:

```ts
/// file: src/routes/blog/data.remote.js
Expand Down Expand Up @@ -711,15 +732,16 @@ export const createPost = form(
<!--- file: src/routes/blog/new/+page.svelte --->
<script>
import { createPost } from '../data.remote';
const form = createPost();
</script>

<h1>Create a new post</h1>

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

{#if createPost.result?.success}
{#if form.result?.success}
<p>Successfully published!</p>
{/if}
```
Expand All @@ -739,11 +761,12 @@ We can customize what happens when the form is submitted with the `enhance` meth
<script>
import { createPost } from '../data.remote';
import { showToast } from '$lib/toast';
const form = createPost();
</script>

<h1>Create a new post</h1>

<form {...createPost.enhance(async ({ form, data, submit }) => {
<form {...form.enhance(async ({ form, data, submit }) => {
try {
await submit();
form.reset();
Expand Down Expand Up @@ -796,7 +819,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 @@ -807,7 +830,7 @@ 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>
Expand All @@ -827,21 +850,23 @@ This attribute exists on the `buttonProps` property of a form object:
<!--- file: src/routes/login/+page.svelte --->
<script>
import { login, register } from '$lib/auth';
const loginForm = login();
const registerForm = register();
</script>

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

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

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

Expand Down
19 changes: 4 additions & 15 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2003,6 +2003,10 @@ type InvalidField<T> =
export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
InvalidField<Input>;

export type RemoteFormFactory<Input extends RemoteFormInput | void, Output> = (
key?: ExtractId<Input>
) => RemoteForm<Input, Output>;

/**
* The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
*/
Expand All @@ -2026,21 +2030,6 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
action: string;
[attachment: symbol]: (node: HTMLFormElement) => void;
};
/**
* Create an instance of the form for the given `id`.
* The `id` is stringified and used for deduplication to potentially reuse existing instances.
* Useful when you have multiple forms that use the same remote form action, for example in a loop.
* ```svelte
* {#each todos as todo}
* {@const todoForm = updateTodo.for(todo.id)}
* <form {...todoForm}>
* {#if todoForm.result?.invalid}<p>Invalid data</p>{/if}
* ...
* </form>
* {/each}
* ```
*/
for(id: ExtractId<Input>): Omit<RemoteForm<Input, Output>, 'for'>;
/** Preflight checks */
preflight(schema: StandardSchemaV1<Input, any>): RemoteForm<Input, Output>;
/** Validate the form contents programmatically */
Expand Down
Loading
Loading