Skip to content

Commit d499228

Browse files
github-actions[bot]svelte-docs-bot[bot]Rich-Harris
authored
Sync kit docs (#1591)
* sync kit docs * bump kit --------- Co-authored-by: svelte-docs-bot[bot] <196124396+svelte-docs-bot[bot]@users.noreply.github.com> Co-authored-by: Rich Harris <[email protected]>
1 parent de081ef commit d499228

File tree

4 files changed

+250
-134
lines changed

4 files changed

+250
-134
lines changed

apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md

Lines changed: 177 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,6 @@ export const getWeather = query.batch(v.string(), async (cities) => {
230230
231231
The `form` function makes it easy to write data to the server. It takes a callback that receives `data` constructed from the submitted [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)...
232232
233-
234233
```ts
235234
/// file: src/routes/blog/data.remote.js
236235
// @filename: ambient.d.ts
@@ -294,115 +293,188 @@ export const createPost = form(
294293

295294
<h1>Create a new post</h1>
296295

296+
<form {...createPost}>
297+
<!-- form content goes here -->
298+
299+
<button>Publish!</button>
300+
</form>
301+
```
302+
303+
The form object contains `method` and `action` properties that allow it to work without JavaScript (i.e. it submits data and reloads the page). It also has an [attachment](/docs/svelte/@attach) that progressively enhances the form when JavaScript is available, submitting data *without* reloading the entire page.
304+
305+
As with `query`, if the callback uses the submitted `data`, it should be [validated](#query-Query-arguments) by passing a [Standard Schema](https://standardschema.dev) as the first argument to `form`.
306+
307+
### Fields
308+
309+
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:
310+
311+
```svelte
297312
<form {...createPost}>
298313
<label>
299314
<h2>Title</h2>
300-
<input name="title" />
315+
+++<input {...createPost.fields.title.as('text')} />+++
301316
</label>
302317

303318
<label>
304319
<h2>Write your post</h2>
305-
<textarea name="content"></textarea>
320+
+++<textarea {...createPost.fields.content.as('text')}></textarea>+++
306321
</label>
307322

308323
<button>Publish!</button>
309324
</form>
310325
```
311326
312-
As with `query`, if the callback uses the submitted `data`, it should be [validated](#query-Query-arguments) by passing a [Standard Schema](https://standardschema.dev) as the first argument to `form`. The one difference is to `query` is that the schema inputs must all be of type `string` or `File`, since that's all the original `FormData` provides. You can however coerce the value into a different type — how to do that depends on the validation library you use.
327+
These attributes allow SvelteKit to set the correct input type, set a `name` that is used to construct the `data` passed to the handler, populate the `value` of the form (for example following a failed submission, to save the user having to re-enter everything), and set the [`aria-invalid`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-invalid) state.
313328
314-
```ts
315-
/// file: src/routes/count.remote.js
329+
Fields can be nested in objects and arrays, and their values can be strings, numbers, booleans or `File` objects. For example, if your schema looked like this...
330+
331+
```js
332+
/// file: data.remote.js
316333
import * as v from 'valibot';
317334
import { form } from '$app/server';
318-
319-
export const setCount = form(
320-
v.object({
321-
// Valibot:
322-
count: v.pipe(v.string(), v.transform((s) => Number(s)), v.number()),
323-
// Zod:
324-
// count: z.coerce.number<string>()
335+
// ---cut---
336+
const datingProfile = v.object({
337+
name: v.string(),
338+
photo: v.file(),
339+
info: v.object({
340+
height: v.number(),
341+
likesDogs: v.optional(v.boolean(), false)
325342
}),
326-
async ({ count }) => {
327-
// ...
328-
}
329-
);
343+
attributes: v.array(v.string())
344+
});
345+
346+
export const createProfile = form(datingProfile, (data) => { /* ... */ });
330347
```
331348
332-
The `name` attributes on the form controls must correspond to the properties of the schema — `title` and `content` in this case. If you schema contains objects, use object notation:
349+
...your form could look like this:
333350
334351
```svelte
335-
<!--
336-
results in a
337-
{
338-
name: { first: string, last: string },
339-
jobs: Array<{ title: string, company: string }>
340-
}
341-
object
342-
-->
343-
<input name="name.first" />
344-
<input name="name.last" />
345-
{#each jobs as job, idx}
346-
<input name="jobs[{idx}].title">
347-
<input name="jobs[{idx}].company">
348-
{/each}
352+
<script>
353+
import { createProfile } from './data.remote';
354+
355+
const { name, photo, info, attributes } = createProfile.fields;
356+
</script>
357+
358+
<form {...createProfile} enctype="multipart/form-data">
359+
<label>
360+
<input {...name.as('text')} /> Name
361+
</label>
362+
363+
<label>
364+
<input {...photo.as('file')} /> Photo
365+
</label>
366+
367+
<label>
368+
<input {...info.height.as('number')} /> Height (cm)
369+
</label>
370+
371+
<label>
372+
<input {...info.likesDogs.as('checkbox')} /> I like dogs
373+
</label>
374+
375+
<h2>My best attributes</h2>
376+
<input {...attributes[0].as('text')} />
377+
<input {...attributes[1].as('text')} />
378+
<input {...attributes[2].as('text')} />
379+
380+
<button>submit</button>
381+
</form>
349382
```
350383
351-
To indicate a repeated field, use a `[]` suffix:
384+
Because our form contains a `file` input, we've added an `enctype="multipart/form-data"` attribute. The values for `info.height` and `info.likesDogs` are coerced to a number and a boolean respectively.
385+
386+
> [!NOTE] If a `checkbox` input is unchecked, the value is not included in the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object that SvelteKit constructs the data from. As such, we have to make the value optional in our schema. In Valibot that means using `v.optional(v.boolean(), false)` instead of just `v.boolean()`, whereas in Zod it would mean using `z.coerce.boolean<boolean>()`.
387+
388+
In the case of `radio` and `checkbox` inputs that all belong to the same field, the `value` must be specified as a second argument to `.as(...)`:
389+
390+
```js
391+
/// file: data.remote.js
392+
import * as v from 'valibot';
393+
import { form } from '$app/server';
394+
// ---cut---
395+
export const survey = form(
396+
v.object({
397+
operatingSystem: v.picklist(['windows', 'mac', 'linux']),
398+
languages: v.optional(v.array(v.picklist(['html', 'css', 'js'])), [])
399+
}),
400+
(data) => { /* ... */ }
401+
);
402+
```
352403
353404
```svelte
354-
<label><input type="checkbox" name="language[]" value="html" /> HTML</label>
355-
<label><input type="checkbox" name="language[]" value="css" /> CSS</label>
356-
<label><input type="checkbox" name="language[]" value="js" /> JS</label>
405+
<form {...survey}>
406+
<h2>Which operating system do you use?</h2>
407+
408+
{#each ['windows', 'mac', 'linux'] as os}
409+
<label>
410+
<input {...survey.fields.operatingSystem.as('radio', os)}>
411+
{os}
412+
</label>
413+
{/each}
414+
415+
<h2>Which languages do you write code in?</h2>
416+
417+
{#each ['html', 'css', 'js'] as language}
418+
<label>
419+
<input {...survey.fields.languages.as('checkbox', language)}>
420+
{language}
421+
</label>
422+
{/each}
423+
424+
<button>submit</button>
425+
</form>
357426
```
358427
359-
If you'd like type safety and autocomplete when setting `name` attributes, use the form object's `field` method:
428+
Alternatively, you could use `select` and `select multiple`:
360429
361430
```svelte
362-
<label>
363-
<h2>Title</h2>
364-
<input name={+++createPost.field('title')+++} />
365-
</label>
366-
```
431+
<form {...survey}>
432+
<h2>Which operating system do you use?</h2>
367433

368-
This will error during typechecking if `title` does not exist on your schema.
434+
<select {...survey.fields.operatingSystem.as('select')}>
435+
<option>windows</option>
436+
<option>mac</option>
437+
<option>linux</option>
438+
</select>
369439

370-
The form object contains `method` and `action` properties that allow it to work without JavaScript (i.e. it submits data and reloads the page). It also has an [attachment](/docs/svelte/@attach) that progressively enhances the form when JavaScript is available, submitting data *without* reloading the entire page.
440+
<h2>Which languages do you write code in?</h2>
441+
442+
<select {...survey.fields.languages.as('select multiple')}>
443+
<option>html</option>
444+
<option>css</option>
445+
<option>js</option>
446+
</select>
447+
448+
<button>submit</button>
449+
</form>
450+
```
451+
452+
> [!NOTE] As with unchecked `checkbox` inputs, if no selections are made then the data will be `undefined`. For this reason, the `languages` field uses `v.optional(v.array(...), [])` rather than just `v.array(...)`.
371453
372454
### Validation
373455
374-
If the submitted data doesn't pass the schema, the callback will not run. Instead, the form object's `issues` object will be populated:
456+
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`:
375457
376458
```svelte
377459
<form {...createPost}>
378460
<label>
379461
<h2>Title</h2>
380462

381-
+++ {#if createPost.issues.title}
382-
{#each createPost.issues.title as issue}
383-
<p class="issue">{issue.message}</p>
384-
{/each}
385-
{/if}+++
463+
+++ {#each createPost.fields.title.issues() as issue}
464+
<p class="issue">{issue.message}</p>
465+
{/each}+++
386466

387-
<input
388-
name="title"
389-
+++aria-invalid={!!createPost.issues.title}+++
390-
/>
467+
<input {...createPost.fields.title.as('text')} />
391468
</label>
392469

393470
<label>
394471
<h2>Write your post</h2>
395472

396-
+++ {#if createPost.issues.content}
397-
{#each createPost.issues.content as issue}
398-
<p class="issue">{issue.message}</p>
399-
{/each}
400-
{/if}+++
473+
+++ {#each createPost.fields.content.issues() as issue}
474+
<p class="issue">{issue.message}</p>
475+
{/each}+++
401476

402-
<textarea
403-
name="content"
404-
+++aria-invalid={!!createPost.issues.content}+++
405-
></textarea>
477+
<textarea {...createPost.fields.content.as('text')}></textarea>
406478
</label>
407479

408480
<button>Publish!</button>
@@ -419,7 +491,7 @@ You don't need to wait until the form is submitted to validate the data — you
419491
420492
By default, issues will be ignored if they belong to form controls that haven't yet been interacted with. To validate _all_ inputs, call `validate({ includeUntouched: true })`.
421493
422-
For client-side validation, you can specify a _preflight_ schema which will populate `issues` and prevent data being sent to the server if the data doesn't validate:
494+
For client-side validation, you can specify a _preflight_ schema which will populate `issues()` and prevent data being sent to the server if the data doesn't validate:
423495
424496
```svelte
425497
<script>
@@ -441,45 +513,65 @@ For client-side validation, you can specify a _preflight_ schema which will popu
441513
442514
> [!NOTE] The preflight schema can be the same object as your server-side schema, if appropriate, though it won't be able to do server-side checks like 'this value already exists in the database'. Note that you cannot export a schema from a `.remote.ts` or `.remote.js` file, so the schema must either be exported from a shared module, or from a `<script module>` block in the component containing the `<form>`.
443515
444-
### Live inputs
516+
To get a list of _all_ issues, rather than just those belonging to a single field, you can use the `fields.allIssues()` method:
445517
446-
The form object contains a `input` property which reflects its current value. As the user interacts with the form, `input` is automatically updated:
518+
```svelte
519+
{#each createPost.fields.allIssues() as issue}
520+
<p>{issue.message}</p>
521+
{/each}
522+
```
523+
524+
### Getting/setting inputs
525+
526+
Each field has a `value()` method that reflects its current value. As the user interacts with the form, it is automatically updated:
447527
448528
```svelte
449529
<form {...createPost}>
450530
<!-- -->
451531
</form>
452532

453533
<div class="preview">
454-
<h2>{createPost.input.title}</h2>
455-
<div>{@html render(createPost.input.content)}</div>
534+
<h2>{createPost.fields.title.value()}</h2>
535+
<div>{@html render(createPost.fields.content.value())}</div>
456536
</div>
457537
```
458538
539+
Alternatively, `createPost.fields.value()` would return a `{ title, content }` object.
540+
541+
You can update a field (or a collection of fields) via the `set(...)` method:
542+
543+
```svelte
544+
<script>
545+
import { createPost } from '../data.remote';
546+
547+
// this...
548+
createPost.fields.set({
549+
title: 'My new blog post',
550+
content: 'Lorem ipsum dolor sit amet...'
551+
});
552+
553+
// ...is equivalent to this:
554+
createPost.fields.title.set('My new blog post');
555+
createPost.fields.content.set('Lorem ipsum dolor sit amet');
556+
</script>
557+
```
558+
459559
### Handling sensitive data
460560
461-
In the case of a non-progressively-enhanced form submission (i.e. where JavaScript is unavailable, for whatever reason) `input` is also populated if the submitted data is invalid, so that the user does not need to fill the entire form out from scratch.
561+
In the case of a non-progressively-enhanced form submission (i.e. where JavaScript is unavailable, for whatever reason) `value()` is also populated if the submitted data is invalid, so that the user does not need to fill the entire form out from scratch.
462562
463563
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:
464564
465565
```svelte
466566
<form {...register}>
467567
<label>
468568
Username
469-
<input
470-
name="username"
471-
value={register.input.username}
472-
aria-invalid={!!register.issues.username}
473-
/>
569+
<input {...register.fields.username.as('text')} />
474570
</label>
475571

476572
<label>
477573
Password
478-
<input
479-
type="password"
480-
+++name="_password"+++
481-
+++aria-invalid={!!register.issues._password}+++
482-
/>
574+
<input +++{...register.fields._password.as('password')}+++ />
483575
</label>
484576

485577
<button>Sign up!</button>
@@ -681,12 +773,12 @@ This attribute exists on the `buttonProps` property of a form object:
681773
<form {...login}>
682774
<label>
683775
Your username
684-
<input name="username" />
776+
<input {...login.fields.username.as('text')} />
685777
</label>
686778

687779
<label>
688780
Your password
689-
<input name="password" type="password" />
781+
<input {...login.fields._password.as('password')} />
690782
</label>
691783

692784
<button>login</button>
@@ -1003,7 +1095,10 @@ const getUser = query(() => {
10031095
});
10041096
```
10051097
1006-
Note that some properties of `RequestEvent` are different inside remote functions. There are no `params` or `route.id`, and you cannot set headers (other than writing cookies, and then only inside `form` and `command` functions), and `url.pathname` is always `/` (since the path that’s actually being requested by the client is purely an implementation detail).
1098+
Note that some properties of `RequestEvent` are different inside remote functions:
1099+
1100+
- you cannot set headers (other than writing cookies, and then only inside `form` and `command` functions)
1101+
- `route`, `params` and `url` relate to the page the remote function was called from, _not_ the URL of the endpoint SvelteKit creates for the remote function. Queries are not re-run when the user navigates (unless the argument to the query changes as a result of navigation), and so you should be mindful of how you use these values. In particular, never use them to determine whether or not a user is authorized to access certain data.
10071102
10081103
## Redirects
10091104

0 commit comments

Comments
 (0)