You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: better remote form field interactions (#14481)
* feat: better remote form field interactions
Shortly after merging enhanced form validation we noticed that we can do better with respects to interacting with the form, specifically `field()/issues/input`.
This removes those properties in favor of a new `fields` property which makes interacting with the form much easier. It's using a proxy under the hood.
* value() / value(...) with correct POJO at all levels
TODO: doesn't work for nested due to Object.create(null)
* WIP has(...)
* get value(input?) working
* remove name method
* remove as(...) from non-leaves, implement allIssues
* allow schema to contain number/boolean
* snake_case
* coerce numbers and booleans
* redact implementation details
* don't return input to client unnecessarily, only redact sensitive fields when reloading
* toggle checkboxes off
* wip
* docs
* more coercion
* fix types
* handle checkbox arrays, disallow radio arrays
* as('select'), initial(...) (TODO, do we really want it?), preserve server issues
* remove initial(...) method
* fix most tests
* fix
* get checkboxes working properly
* various
* fix and DRY out error messages
* fix radio inputs
* document fields
* more docs
* tweak
* fix docs
* fix
* changeset
* Update .changeset/shaky-ties-look.md
* allow fields.set to work during SSR
* we can get rid of file_transport since we're sanitizing issues
* bit more consistency in naming
* expose RemoteFormField and RemoteFormFieldValue
* expose RemoteFormFieldType
* Update packages/kit/src/runtime/form-utils.svelte.js
Co-authored-by: Patrick <[email protected]>
* tighten up types
* tidy up
---------
Co-authored-by: Rich Harris <[email protected]>
Co-authored-by: Rich Harris <[email protected]>
Co-authored-by: Patrick <[email protected]>
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)...
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.
303
+
304
+
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`.
305
+
306
+
### Fields
307
+
308
+
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:
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.
326
+
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.
312
327
313
-
```ts
314
-
/// file: src/routes/count.remote.js
328
+
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...
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:
<input {...info.likesDogs.as('checkbox')} />I like dogs
372
+
</label>
373
+
374
+
<h2>My best attributes</h2>
375
+
<input {...attributes[0].as('text')} />
376
+
<input {...attributes[1].as('text')} />
377
+
<input {...attributes[2].as('text')} />
378
+
379
+
<button>submit</button>
380
+
</form>
348
381
```
349
382
350
-
To indicate a repeated field, use a `[]` suffix:
383
+
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.
384
+
385
+
> [!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>()`.
386
+
387
+
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(...)`:
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.
> [!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(...)`.
370
452
371
453
### Validation
372
454
373
-
If the submitted data doesn't pass the schema, the callback will not run. Instead, the form object's `issues` object will be populated:
455
+
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`:
374
456
375
457
```svelte
376
458
<form {...createPost}>
377
459
<label>
378
460
<h2>Title</h2>
379
461
380
-
+++ {#ifcreatePost.issues.title}
381
-
{#each createPost.issues.title as issue}
382
-
<p class="issue">{issue.message}</p>
383
-
{/each}
384
-
{/if}+++
462
+
+++ {#each createPost.fields.title.issues() as issue}
463
+
<p class="issue">{issue.message}</p>
464
+
{/each}+++
385
465
386
-
<input
387
-
name="title"
388
-
+++aria-invalid={!!createPost.issues.title}+++
389
-
/>
466
+
<input {...createPost.fields.title.as('text')} />
390
467
</label>
391
468
392
469
<label>
393
470
<h2>Write your post</h2>
394
471
395
-
+++ {#ifcreatePost.issues.content}
396
-
{#each createPost.issues.content as issue}
397
-
<p class="issue">{issue.message}</p>
398
-
{/each}
399
-
{/if}+++
472
+
+++ {#each createPost.fields.content.issues() as issue}
@@ -418,7 +490,7 @@ You don't need to wait until the form is submitted to validate the data — you
418
490
419
491
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 })`.
420
492
421
-
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:
493
+
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:
422
494
423
495
```svelte
424
496
<script>
@@ -440,45 +512,65 @@ For client-side validation, you can specify a _preflight_ schema which will popu
440
512
441
513
> [!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>`.
442
514
443
-
### Live inputs
515
+
To get a list of _all_ issues, rather than just those belonging to a single field, you can use the `fields.allIssues()` method:
444
516
445
-
The form object contains a `input` property which reflects its current value. As the user interacts with the form, `input` is automatically updated:
517
+
```svelte
518
+
{#each createPost.fields.allIssues() as issue}
519
+
<p>{issue.message}</p>
520
+
{/each}
521
+
```
522
+
523
+
### Getting/setting inputs
524
+
525
+
Each field has a `value()` method that reflects its current value. As the user interacts with the form, it is automatically updated:
Alternatively, `createPost.fields.value()` would return a `{ title, content }` object.
539
+
540
+
You can update a field (or a collection of fields) via the `set(...)` method:
541
+
542
+
```svelte
543
+
<script>
544
+
import { createPost } from'../data.remote';
545
+
546
+
// this...
547
+
createPost.fields.set({
548
+
title:'My new blog post',
549
+
content:'Lorem ipsum dolor sit amet...'
550
+
});
551
+
552
+
// ...is equivalent to this:
553
+
createPost.fields.title.set('My new blog post');
554
+
createPost.fields.content.set('Lorem ipsum dolor sit amet');
555
+
</script>
556
+
```
557
+
458
558
### Handling sensitive data
459
559
460
-
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.
560
+
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.
461
561
462
562
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:
0 commit comments