Skip to content

Commit 9f68452

Browse files
LeCarbonatorPascalmhharry-whorlow
authored
docs: Expand submission handling and validation documentation (#1428)
Co-authored-by: Pascal Küsgen <[email protected]> Co-authored-by: Harry Whorlow <[email protected]>
1 parent 9e8dfce commit 9f68452

File tree

8 files changed

+730
-30
lines changed

8 files changed

+730
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
id: submission-handling
3+
title: Submission handling
4+
---
5+
6+
## Passing additional data to submission handling
7+
8+
You may have multiple types of submission behaviour, for example, going back to another page or staying on the form.
9+
You can accomplish this by specifying the `onSubmitMeta` property. This meta data will be passed to the `onSubmit` function.
10+
11+
> Note: if `form.handleSubmit()` is called without metadata, it will use the provided default.
12+
13+
```angular-ts
14+
import { Component } from '@angular/core';
15+
import { injectForm } from '@tanstack/angular-form';
16+
17+
18+
type FormMeta = {
19+
submitAction: 'continue' | 'backToMenu' | null;
20+
};
21+
22+
// Metadata is not required to call form.handleSubmit().
23+
// Specify what values to use as default if no meta is passed
24+
const defaultMeta: FormMeta = {
25+
submitAction: null,
26+
};
27+
28+
@Component({
29+
selector: 'app-root',
30+
template: `
31+
<form (submit)="handleSubmit($event)">
32+
<button type="submit" (click)="
33+
handleClick({ submitAction: 'continue' })
34+
">Submit and continue</button>
35+
<button type="submit" (click)="
36+
handleClick({ submitAction: 'backToMenu' })
37+
">Submit and back to menu</button>
38+
</form>
39+
`,
40+
})
41+
export class AppComponent {
42+
form = injectForm({
43+
defaultValues: {
44+
data: '',
45+
},
46+
// Define what meta values to expect on submission
47+
onSubmitMeta: defaultMeta,
48+
onSubmit: async ({ value, meta }) => {
49+
// Do something with the values passed via handleSubmit
50+
console.log(`Selected action - ${meta.submitAction}`, value);
51+
},
52+
});
53+
54+
handleSubmit(event: SubmitEvent) {
55+
event.preventDefault();
56+
event.stopPropagation();
57+
}
58+
59+
handleClick(meta: FormMeta) {
60+
// Overwrites the default specified in onSubmitMeta
61+
this.form.handleSubmit(meta);
62+
}
63+
}
64+
```
65+
66+
## Transforming data with Standard Schemas
67+
68+
While Tanstack Form provides [Standard Schema support](./validation.md) for validation, it does not preserve the Schema's output data.
69+
70+
The value passed to the `onSubmit` function will always be the input data. To receive the output data of a Standard Schema, parse it in the `onSubmit` function:
71+
72+
```tsx
73+
import { z } from 'zod'
74+
// ...
75+
76+
const schema = z.object({
77+
age: z.string().transform((age) => Number(age)),
78+
})
79+
80+
// Tanstack Form uses the input type of Standard Schemas
81+
const defaultValues: z.input<typeof schema> = {
82+
age: '13',
83+
}
84+
85+
// ...
86+
87+
@Component({
88+
// ...
89+
})
90+
export class AppComponent {
91+
form = injectForm({
92+
defaultValues,
93+
validators: {
94+
onChange: schema,
95+
},
96+
onSubmit: ({ value }) => {
97+
const inputAge: string = value.age
98+
// Pass it through the schema to get the transformed value
99+
const result = schema.parse(value)
100+
const outputAge: number = result.age
101+
},
102+
})
103+
}
104+
```

docs/framework/angular/guides/validation.md

+127
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,131 @@ export class AppComponent {
288288
}
289289
```
290290

291+
### Setting field-level errors from the form's validators
292+
293+
You can set errors on the fields from the form's validators. One common use case for this is validating all the fields on submit by calling a single API endpoint in the form's `onSubmitAsync` validator.
294+
295+
```angular-ts
296+
@Component({
297+
selector: 'app-root',
298+
imports: [TanStackField],
299+
template: `
300+
<form (submit)="handleSubmit($event)">
301+
<div>
302+
<ng-container
303+
[tanstackField]="form"
304+
name="age"
305+
#ageField="field"
306+
>
307+
<label [for]="ageField.api.name">Age:</label>
308+
<input
309+
type="number"
310+
[name]="ageField.api.name"
311+
[value]="ageField.api.state.value"
312+
(blur)="ageField.api.handleBlur()"
313+
(input)="ageField.api.handleChange($any($event).target.valueAsNumber)"
314+
/>
315+
@if (ageField.api.state.meta.errors.length > 0) {
316+
<em role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em>
317+
}
318+
</ng-container>
319+
</div>
320+
<button type="submit">Submit</button>
321+
</form>
322+
`,
323+
})
324+
325+
export class AppComponent {
326+
form = injectForm({
327+
defaultValues: {
328+
age: 0,
329+
socials: [],
330+
details: {
331+
email: '',
332+
},
333+
},
334+
validators: {
335+
onSubmitAsync: async ({ value }) => {
336+
// Validate the value on the server
337+
const hasErrors = await verifyDataOnServer(value)
338+
if (hasErrors) {
339+
return {
340+
form: 'Invalid data', // The `form` key is optional
341+
fields: {
342+
age: 'Must be 13 or older to sign',
343+
// Set errors on nested fields with the field's name
344+
'socials[0].url': 'The provided URL does not exist',
345+
'details.email': 'An email is required',
346+
},
347+
};
348+
}
349+
350+
return null;
351+
},
352+
},
353+
});
354+
355+
handleSubmit(event: SubmitEvent) {
356+
event.preventDefault();
357+
event.stopPropagation();
358+
this.form.handleSubmit();
359+
}
360+
}
361+
```
362+
363+
> Something worth mentioning is that if you have a form validation function that returns an error, that error may be overwritten by the field-specific validation.
364+
>
365+
> This means that:
366+
>
367+
> ```angular-ts
368+
> @Component({
369+
> selector: 'app-root',
370+
> standalone: true,
371+
> imports: [TanStackField],
372+
> template: `
373+
> <div>
374+
> <ng-container
375+
> [tanstackField]="form"
376+
> name="age"
377+
> #ageField="field"
378+
> [validators]="{
379+
> onChange: fieldValidator
380+
> }"
381+
> >
382+
> <input type="number" [value]="ageField.api.state.value"
383+
> (input)="ageField.api.handleChange($any($event).target.valueAsNumber)"
384+
> />
385+
> @if (ageField.api.state.meta.errors.length > 0) {
386+
> <em role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em>
387+
> }
388+
> </ng-container>
389+
> </div>
390+
> `,
391+
> })
392+
> export class AppComponent {
393+
> form = injectForm({
394+
> defaultValues: {
395+
> age: 0,
396+
> },
397+
> validators: {
398+
> onChange: ({ value }) => {
399+
> return {
400+
> fields: {
401+
> age: value.age < 12 ? 'Too young!' : undefined,
402+
> },
403+
> };
404+
> },
405+
> },
406+
> });
407+
>
408+
> fieldValidator: FieldValidateFn<any, any, number> = ({ value }) =>
409+
> value % 2 === 0 ? 'Must be odd!' : undefined;
410+
> }
411+
>
412+
> ```
413+
>
414+
> Will only show `'Must be odd!` even if the 'Too young!' error is returned by the form-level validation.
415+
291416
## Asynchronous Functional Validation
292417
293418
While we suspect most validations will be synchronous, there are many instances where a network call or some other async operation would be useful to validate against.
@@ -429,6 +554,8 @@ TanStack Form natively supports all libraries following the [Standard Schema spe
429554

430555
_Note:_ make sure to use the latest version of the schema libraries as older versions might not support Standard Schema yet.
431556

557+
> Validation will not provide you with transformed values. See [submission handling](./submission-handling.md) for more information.
558+
432559
To use schemas from these libraries you can pass them to the `validators` props as you would do with a custom function:
433560

434561
```angular-ts

docs/framework/react/guides/submission-handling.md

+78-25
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,91 @@ id: submission-handling
33
title: Submission handling
44
---
55

6-
In a situation where you want to have multiple form submission types, for example, a form that has a button that navigates to a sub-form and another button that handles a standard submission, you can make use of the onSubmitMeta prop and the handleSubmit function overloads.
6+
## Passing additional data to submission handling
77

8-
## Basic Usage
8+
You may have multiple types of submission behaviour, for example, going back to another page or staying on the form.
9+
You can accomplish this by specifying the `onSubmitMeta` property. This meta data will be passed to the `onSubmit` function.
910

10-
First you must define the default state of the form.onSubmitMeta prop:
11+
> Note: if `form.handleSubmit()` is called without metadata, it will use the provided default.
1112
1213
```tsx
13-
const form = useForm({
14-
defaultValues: {
15-
firstName: 'Rick',
16-
},
17-
// {} is the default value passed to `onSubmit`'s `meta` property
18-
onSubmitMeta: {} as { lastName: string },
19-
onSubmit: async ({ value, meta }) => {
20-
// Do something with the values passed via handleSubmit
21-
console.log(`${value.firstName} - ${meta}`)
22-
},
23-
})
14+
import { useForm } from '@tanstack/react-form'
15+
16+
type FormMeta = {
17+
submitAction: 'continue' | 'backToMenu' | null
18+
}
19+
20+
// Metadata is not required to call form.handleSubmit().
21+
// Specify what values to use as default if no meta is passed
22+
const defaultMeta: FormMeta = {
23+
submitAction: null,
24+
}
25+
26+
function App() {
27+
const form = useForm({
28+
defaultValues: {
29+
data: '',
30+
},
31+
// Define what meta values to expect on submission
32+
onSubmitMeta: defaultMeta,
33+
onSubmit: async ({ value, meta }) => {
34+
// Do something with the values passed via handleSubmit
35+
console.log(`Selected action - ${meta.submitAction}`, value)
36+
},
37+
})
38+
39+
return (
40+
<form
41+
onSubmit={(e) => {
42+
e.preventDefault()
43+
e.stopPropagation()
44+
}}
45+
>
46+
{/* ... */}
47+
<button
48+
type="submit"
49+
// Overwrites the default specified in onSubmitMeta
50+
onClick={() => form.handleSubmit({ submitAction: 'continue' })}
51+
>
52+
Submit and continue
53+
</button>
54+
<button
55+
type="submit"
56+
onClick={() => form.handleSubmit({ submitAction: 'backToMenu' })}
57+
>
58+
Submit and back to menu
59+
</button>
60+
</form>
61+
)
62+
}
2463
```
2564

26-
Note: the default state of onSubmitMeta is `never`, so if the prop is not provided and you try to access it in `handleSubmit`, or `onSubmit` it will error.
65+
## Transforming data with Standard Schemas
2766

28-
Then when you call `onSubmit` you can provide it the predefined meta like so:
67+
While Tanstack Form provides [Standard Schema support](./validation.md) for validation, it does not preserve the Schema's output data.
68+
69+
The value passed to the `onSubmit` function will always be the input data. To receive the output data of a Standard Schema, parse it in the `onSubmit` function:
2970

3071
```tsx
31-
<form
32-
onSubmit={(e) => {
33-
e.preventDefault()
34-
e.stopPropagation()
35-
form.handleSubmit({
36-
lastName: 'Astley',
37-
})
38-
}}
39-
></form>
72+
const schema = z.object({
73+
age: z.string().transform((age) => Number(age)),
74+
})
75+
76+
// Tanstack Form uses the input type of Standard Schemas
77+
const defaultValues: z.input<typeof schema> = {
78+
age: '13',
79+
}
80+
81+
const form = useForm({
82+
defaultValues,
83+
validators: {
84+
onChange: schema,
85+
},
86+
onSubmit: ({ value }) => {
87+
const inputAge: string = value.age
88+
// Pass it through the schema to get the transformed value
89+
const result = schema.parse(value)
90+
const outputAge: number = result.age
91+
},
92+
})
4093
```

0 commit comments

Comments
 (0)