Skip to content

Commit b875e36

Browse files
Rich-Harrisgithub-actions[bot]svelte-docs-bot[bot]
authored
chore: bump kit (#1540)
* chore: bump kit * Sync `kit` docs (#1539) sync kit docs Co-authored-by: svelte-docs-bot[bot] <196124396+svelte-docs-bot[bot]@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: svelte-docs-bot[bot] <196124396+svelte-docs-bot[bot]@users.noreply.github.com>
1 parent f465a0e commit b875e36

File tree

5 files changed

+376
-77
lines changed

5 files changed

+376
-77
lines changed

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

Lines changed: 233 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export const getWeather = query.batch(v.string(), async (cities) => {
228228
229229
## form
230230
231-
The `form` function makes it easy to write data to the server. It takes a callback that receives the current [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)...
231+
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
233233
234234
```ts
@@ -260,30 +260,28 @@ export const getPosts = query(async () => { /* ... */ });
260260

261261
export const getPost = query(v.string(), async (slug) => { /* ... */ });
262262

263-
export const createPost = form(async (data) => {
264-
// Check the user is logged in
265-
const user = await auth.getUser();
266-
if (!user) error(401, 'Unauthorized');
267-
268-
const title = data.get('title');
269-
const content = data.get('content');
270-
271-
// Check the data is valid
272-
if (typeof title !== 'string' || typeof content !== 'string') {
273-
error(400, 'Title and content are required');
263+
export const createPost = form(
264+
v.object({
265+
title: v.pipe(v.string(), v.nonEmpty()),
266+
content:v.pipe(v.string(), v.nonEmpty())
267+
}),
268+
async ({ title, content }) => {
269+
// Check the user is logged in
270+
const user = await auth.getUser();
271+
if (!user) error(401, 'Unauthorized');
272+
273+
const slug = title.toLowerCase().replace(/ /g, '-');
274+
275+
// Insert into the database
276+
await db.sql`
277+
INSERT INTO post (slug, title, content)
278+
VALUES (${slug}, ${title}, ${content})
279+
`;
280+
281+
// Redirect to the newly created page
282+
redirect(303, `/blog/${slug}`);
274283
}
275-
276-
const slug = title.toLowerCase().replace(/ /g, '-');
277-
278-
// Insert into the database
279-
await db.sql`
280-
INSERT INTO post (slug, title, content)
281-
VALUES (${slug}, ${title}, ${content})
282-
`;
283-
284-
// Redirect to the newly created page
285-
redirect(303, `/blog/${slug}`);
286-
});
284+
);
287285
```
288286
289287
...and returns an object that can be spread onto a `<form>` element. The callback is called whenever the form is submitted.
@@ -311,7 +309,184 @@ export const createPost = form(async (data) => {
311309
</form>
312310
```
313311
314-
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 `onsubmit` handler that progressively enhances the form when JavaScript is available, submitting data *without* reloading the entire page.
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.
313+
314+
```ts
315+
/// file: src/routes/count.remote.js
316+
import * as v from 'valibot';
317+
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: v.coerce.number()
325+
}),
326+
async ({ count }) => {
327+
// ...
328+
}
329+
);
330+
```
331+
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:
333+
334+
```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}
349+
```
350+
351+
To indicate a repeated field, use a `[]` suffix:
352+
353+
```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>
357+
```
358+
359+
If you'd like type safety and autocomplete when setting `name` attributes, use the form object's `field` method:
360+
361+
```svelte
362+
<label>
363+
<h2>Title</h2>
364+
<input name={+++createPost.field('title')+++} />
365+
</label>
366+
```
367+
368+
This will error during typechecking if `title` does not exist on your schema.
369+
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.
371+
372+
### Validation
373+
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:
375+
376+
```svelte
377+
<form {...createPost}>
378+
<label>
379+
<h2>Title</h2>
380+
381+
+++ {#if createPost.issues.title}
382+
{#each createPost.issues.title as issue}
383+
<p class="issue">{issue.message}</p>
384+
{/each}
385+
{/if}+++
386+
387+
<input
388+
name="title"
389+
+++aria-invalid={!!createPost.issues.title}+++
390+
/>
391+
</label>
392+
393+
<label>
394+
<h2>Write your post</h2>
395+
396+
+++ {#if createPost.issues.content}
397+
{#each createPost.issues.content as issue}
398+
<p class="issue">{issue.message}</p>
399+
{/each}
400+
{/if}+++
401+
402+
<textarea
403+
name="content"
404+
+++aria-invalid={!!createPost.issues.content}+++
405+
></textarea>
406+
</label>
407+
408+
<button>Publish!</button>
409+
</form>
410+
```
411+
412+
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:
413+
414+
```svelte
415+
<form {...createPost} oninput={() => createPost.validate()}>
416+
<!-- -->
417+
</form>
418+
```
419+
420+
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 })`.
421+
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:
423+
424+
```svelte
425+
<script>
426+
import * as v from 'valibot';
427+
import { createPost } from '../data.remote';
428+
429+
const schema = v.object({
430+
title: v.pipe(v.string(), v.nonEmpty()),
431+
content:v.pipe(v.string(), v.nonEmpty())
432+
});
433+
</script>
434+
435+
<h1>Create a new post</h1>
436+
437+
<form {...+++createPost.preflight(schema)+++}>
438+
<!-- -->
439+
</form>
440+
```
441+
442+
> [!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>`.
443+
444+
### Live inputs
445+
446+
The form object contains a `input` property which reflects its current value. As the user interacts with the form, `input` is automatically updated:
447+
448+
```svelte
449+
<form {...createPost}>
450+
<!-- -->
451+
</form>
452+
453+
<div class="preview">
454+
<h2>{createPost.input.title}</h2>
455+
<div>{@html render(createPost.input.content)}</div>
456+
</div>
457+
```
458+
459+
### Handling sensitive data
460+
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.
462+
463+
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:
464+
465+
```svelte
466+
<form {...register}>
467+
<label>
468+
Username
469+
<input
470+
name="username"
471+
value={register.input.username}
472+
aria-invalid={!!register.issues.username}
473+
/>
474+
</label>
475+
476+
<label>
477+
Password
478+
<input
479+
type="password"
480+
+++name="_password"+++
481+
+++aria-invalid={!!register.issues._password}+++
482+
/>
483+
</label>
484+
485+
<button>Sign up!</button>
486+
</form>
487+
```
488+
489+
In this example, if the data does not validate, only the first `<input>` will be populated when the page reloads.
315490
316491
### Single-flight mutations
317492
@@ -332,25 +507,31 @@ export const getPosts = query(async () => { /* ... */ });
332507

333508
export const getPost = query(v.string(), async (slug) => { /* ... */ });
334509

335-
export const createPost = form(async (data) => {
336-
// form logic goes here...
510+
export const createPost = form(
511+
v.object({/* ... */}),
512+
async (data) => {
513+
// form logic goes here...
337514

338-
// Refresh `getPosts()` on the server, and send
339-
// the data back with the result of `createPost`
340-
+++await getPosts().refresh();+++
515+
// Refresh `getPosts()` on the server, and send
516+
// the data back with the result of `createPost`
517+
+++await getPosts().refresh();+++
341518

342-
// Redirect to the newly created page
343-
redirect(303, `/blog/${slug}`);
344-
});
519+
// Redirect to the newly created page
520+
redirect(303, `/blog/${slug}`);
521+
}
522+
);
345523

346-
export const updatePost = form(async (data) => {
347-
// form logic goes here...
348-
const result = externalApi.update(post);
524+
export const updatePost = form(
525+
v.object({/* ... */}),
526+
async (data) => {
527+
// form logic goes here...
528+
const result = externalApi.update(post);
349529

350-
// The API already gives us the updated post,
351-
// no need to refresh it, we can set it directly
352-
+++await getPost(post.id).set(result);+++
353-
});
530+
// The API already gives us the updated post,
531+
// no need to refresh it, we can set it directly
532+
+++await getPost(post.id).set(result);+++
533+
}
534+
);
354535
```
355536
356537
The second is to drive the single-flight mutation from the client, which we'll see in the section on [`enhance`](#form-enhance).
@@ -388,11 +569,14 @@ export const getPosts = query(async () => { /* ... */ });
388569
export const getPost = query(v.string(), async (slug) => { /* ... */ });
389570

390571
// ---cut---
391-
export const createPost = form(async (data) => {
392-
// ...
572+
export const createPost = form(
573+
v.object({/* ... */}),
574+
async (data) => {
575+
// ...
393576

394-
return { success: true };
395-
});
577+
return { success: true };
578+
}
579+
);
396580
```
397581
398582
```svelte
@@ -403,7 +587,9 @@ export const createPost = form(async (data) => {
403587

404588
<h1>Create a new post</h1>
405589

406-
<form {...createPost}><!-- ... --></form>
590+
<form {...createPost}>
591+
<!-- -->
592+
</form>
407593

408594
{#if createPost.result?.success}
409595
<p>Successfully published!</p>
@@ -439,9 +625,7 @@ We can customize what happens when the form is submitted with the `enhance` meth
439625
showToast('Oh no! Something went wrong');
440626
}
441627
})}>
442-
<input name="title" />
443-
<textarea name="content"></textarea>
444-
<button>publish</button>
628+
<!-- -->
445629
</form>
446630
```
447631
@@ -518,7 +702,7 @@ The `command` function, like `form`, allows you to write data to the server. Unl
518702
519703
> [!NOTE] Prefer `form` where possible, since it gracefully degrades if JavaScript is disabled or fails to load.
520704
521-
As with `query`, if the function accepts an argument, it should be [validated](#query-Query-arguments) by passing a [Standard Schema](https://standardschema.dev) as the first argument to `command`.
705+
As with `query` and `form`, if the function accepts an argument, it should be [validated](#query-Query-arguments) by passing a [Standard Schema](https://standardschema.dev) as the first argument to `command`.
522706
523707
```ts
524708
/// file: likes.remote.js

0 commit comments

Comments
 (0)