diff --git a/apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md b/apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md index f0e26ff52..eb1e5f4a7 100644 --- a/apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md +++ b/apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md @@ -451,6 +451,43 @@ Alternatively, you could use `select` and `select multiple`: > [!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(...)`. +### Programmatic validation + +In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action: + +```js +/// file: src/routes/shop/data.remote.js +import * as v from 'valibot'; +import { form } from '$app/server'; +import * as db from '$lib/server/database'; + +export const buyHotcakes = form( + v.object({ + qty: v.pipe( + v.number(), + v.minValue(1, 'you must buy at least one hotcake') + ) + }), + async (data, invalid) => { + try { + await db.buy(data.qty); + } catch (e) { + if (e.code === 'OUT_OF_STOCK') { + invalid( + invalid.qty(`we don't have enough hotcakes`) + ); + } + } + } +); +``` + +The `invalid` function works as both a function and a proxy: + +- Call `invalid(issue1, issue2, ...issueN)` to throw a validation error +- If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`) +- Use `invalid.fieldName(message)` to create an issue for a specific field. Like `fields` this is type-safe and you can use regular property access syntax to create issues for deeply nested objects (e.g. `invalid.profile.email('Email already exists')` or `invalid.items[0].qty('Insufficient stock')`) + ### Validation 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`: @@ -756,6 +793,27 @@ await submit().updates( The override will be applied immediately, and released when the submission completes (or fails). +### 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. + +```svelte + + + +

Todos

+ +{#each await getTodos() as todo} + {@const modify = modifyTodo.for(todo.id)} +
+ + +
+{/each} +``` + ### buttonProps By default, submitting a form will send a request to the URL indicated by the `
` 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. diff --git a/apps/svelte.dev/content/docs/kit/98-reference/10-@sveltejs-kit.md b/apps/svelte.dev/content/docs/kit/98-reference/10-@sveltejs-kit.md index 97db143f0..130223931 100644 --- a/apps/svelte.dev/content/docs/kit/98-reference/10-@sveltejs-kit.md +++ b/apps/svelte.dev/content/docs/kit/98-reference/10-@sveltejs-kit.md @@ -1253,6 +1253,26 @@ The content of the error. +## Invalid + +A function and proxy object used to imperatively create validation errors in form handlers. + +Call `invalid(issue1, issue2, ...issueN)` to throw a validation error. +If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`) +Access properties to create field-specific issues: `invalid.fieldName('message')`. +The type structure mirrors the input data structure for type-safe field access. + +
+ +```dts +type Invalid = (( + ...issues: Array +) => never) & + InvalidField; +``` + +
+ ## KitConfig See the [configuration reference](/docs/kit/configuration) for details. @@ -2315,8 +2335,8 @@ type RemoteForm< [attachment: symbol]: (node: HTMLFormElement) => void; }; /** - * Create an instance of the form for the given key. - * The key is stringified and used for deduplication to potentially reuse existing instances. + * 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} @@ -2329,7 +2349,7 @@ type RemoteForm< * ``` */ for( - key: string | number | boolean + id: ExtractId ): Omit, 'for'>; /** Preflight checks */ preflight( diff --git a/apps/svelte.dev/content/docs/kit/98-reference/20-$app-server.md b/apps/svelte.dev/content/docs/kit/98-reference/20-$app-server.md index fb2d50757..b2a658895 100644 --- a/apps/svelte.dev/content/docs/kit/98-reference/20-$app-server.md +++ b/apps/svelte.dev/content/docs/kit/98-reference/20-$app-server.md @@ -82,7 +82,9 @@ See [Remote functions](/docs/kit/remote-functions#form) for full documentation. ```dts function form( - fn: () => MaybePromise + fn: ( + invalid: import('@sveltejs/kit').Invalid + ) => MaybePromise ): RemoteForm; ``` @@ -93,7 +95,10 @@ function form( ```dts function form( validate: 'unchecked', - fn: (data: Input) => MaybePromise + fn: ( + data: Input, + invalid: import('@sveltejs/kit').Invalid + ) => MaybePromise ): RemoteForm; ``` @@ -111,7 +116,10 @@ function form< >( validate: Schema, fn: ( - data: StandardSchemaV1.InferOutput + data: StandardSchemaV1.InferOutput, + invalid: import('@sveltejs/kit').Invalid< + StandardSchemaV1.InferOutput + > ) => MaybePromise ): RemoteForm, Output>; ```