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/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 @@ -494,25 +494,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 @@ -522,7 +522,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 @@ -540,11 +540,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 @@ -554,7 +558,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 @@ -564,17 +568,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 @@ -583,14 +587,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 @@ -601,15 +605,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 @@ -668,7 +672,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 @@ -717,11 +721,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 @@ -745,7 +749,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 @@ -798,7 +802,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 @@ -809,14 +813,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 @@ -831,19 +867,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