Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions docs/framework/angular/guides/submission-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
id: submission-handling
title: Submission handling
---

## Passing additional data to submission handling

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

> Note: if `form.handleSubmit()` is called without metadata, it will use the provided default.

```angular-ts
import { Component } from '@angular/core';
import { injectForm } from '@tanstack/angular-form';


type FormMeta = {
submitAction: 'continue' | 'backToMenu' | null;
};

// Metadata is not required to call form.handleSubmit().
// Specify what values to use as default if no meta is passed
const defaultMeta: FormMeta = {
submitAction: null,
};

@Component({
selector: 'app-root',
template: `
<form (submit)="handleSubmit($event)">
<button type="submit" (click)="
handleClick({ submitAction: 'continue' })
">Submit and continue</button>
<button type="submit" (click)="
handleClick({ submitAction: 'backToMenu' })
">Submit and back to menu</button>
</form>
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
data: '',
},
// Define what meta values to expect on submission
onSubmitMeta: defaultMeta,
onSubmit: async ({ value, meta }) => {
// Do something with the values passed via handleSubmit
console.log(`Selected action - ${meta.submitAction}`, value);
},
});

handleSubmit(event: SubmitEvent) {
event.preventDefault();
event.stopPropagation();
}

handleClick(meta: FormMeta) {
// Overwrites the default specified in onSubmitMeta
this.form.handleSubmit(meta);
}
}
```

## Transforming data with Standard Schemas

While Tanstack Form provides [Standard Schema support](./validation.md) for validation, it does not preserve the Schema's output data.

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:

```tsx
import { z } from 'zod'
// ...

const schema = z.object({
age: z.string().transform((age) => Number(age)),
})

// Tanstack Form uses the input type of Standard Schemas
const defaultValues: z.input<typeof schema> = {
age: '13',
}

// ...

@Component({
// ...
})
export class AppComponent {
form = injectForm({
defaultValues,
validators: {
onChange: schema,
},
onSubmit: ({ value }) => {
const inputAge: string = value.age
// Pass it through the schema to get the transformed value
const result = schema.parse(value)
const outputAge: number = result.age
},
})
}
```
127 changes: 127 additions & 0 deletions docs/framework/angular/guides/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,131 @@ export class AppComponent {
}
```

### Setting field-level errors from the form's validators

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.

```angular-ts
@Component({
selector: 'app-root',
imports: [TanStackField],
template: `
<form (submit)="handleSubmit($event)">
<div>
<ng-container
[tanstackField]="form"
name="age"
#ageField="field"
>
<label [for]="ageField.api.name">Age:</label>
<input
type="number"
[name]="ageField.api.name"
[value]="ageField.api.state.value"
(blur)="ageField.api.handleBlur()"
(input)="ageField.api.handleChange($any($event).target.valueAsNumber)"
/>
@if (ageField.api.state.meta.errors.length > 0) {
<em role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em>
}
</ng-container>
</div>
<button type="submit">Submit</button>
</form>
`,
})

export class AppComponent {
form = injectForm({
defaultValues: {
age: 0,
socials: [],
details: {
email: '',
},
},
validators: {
onSubmitAsync: async ({ value }) => {
// Validate the value on the server
const hasErrors = await verifyDataOnServer(value)
if (hasErrors) {
return {
form: 'Invalid data', // The `form` key is optional
fields: {
age: 'Must be 13 or older to sign',
// Set errors on nested fields with the field's name
'socials[0].url': 'The provided URL does not exist',
'details.email': 'An email is required',
},
};
}

return null;
},
},
});

handleSubmit(event: SubmitEvent) {
event.preventDefault();
event.stopPropagation();
this.form.handleSubmit();
}
}
```

> 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.
>
> This means that:
>
> ```angular-ts
> @Component({
> selector: 'app-root',
> standalone: true,
> imports: [TanStackField],
> template: `
> <div>
> <ng-container
> [tanstackField]="form"
> name="age"
> #ageField="field"
> [validators]="{
> onChange: fieldValidator
> }"
> >
> <input type="number" [value]="ageField.api.state.value"
> (input)="ageField.api.handleChange($any($event).target.valueAsNumber)"
> />
> @if (ageField.api.state.meta.errors.length > 0) {
> <em role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em>
> }
> </ng-container>
> </div>
> `,
> })
> export class AppComponent {
> form = injectForm({
> defaultValues: {
> age: 0,
> },
> validators: {
> onChange: ({ value }) => {
> return {
> fields: {
> age: value.age < 12 ? 'Too young!' : undefined,
> },
> };
> },
> },
> });
>
> fieldValidator: FieldValidateFn<any, any, number> = ({ value }) =>
> value % 2 === 0 ? 'Must be odd!' : undefined;
> }
>
> ```
>
> Will only show `'Must be odd!` even if the 'Too young!' error is returned by the form-level validation.

## Asynchronous Functional Validation

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.
Expand Down Expand Up @@ -429,6 +554,8 @@ TanStack Form natively supports all libraries following the [Standard Schema spe

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

> Validation will not provide you with transformed values. See [submission handling](./submission-handling.md) for more information.

To use schemas from these libraries you can pass them to the `validators` props as you would do with a custom function:

```angular-ts
Expand Down
103 changes: 78 additions & 25 deletions docs/framework/react/guides/submission-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,91 @@ id: submission-handling
title: Submission handling
---

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.
## Passing additional data to submission handling

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

First you must define the default state of the form.onSubmitMeta prop:
> Note: if `form.handleSubmit()` is called without metadata, it will use the provided default.

```tsx
const form = useForm({
defaultValues: {
firstName: 'Rick',
},
// {} is the default value passed to `onSubmit`'s `meta` property
onSubmitMeta: {} as { lastName: string },
onSubmit: async ({ value, meta }) => {
// Do something with the values passed via handleSubmit
console.log(`${value.firstName} - ${meta}`)
},
})
import { useForm } from '@tanstack/react-form'

type FormMeta = {
submitAction: 'continue' | 'backToMenu' | null
}

// Metadata is not required to call form.handleSubmit().
// Specify what values to use as default if no meta is passed
const defaultMeta: FormMeta = {
submitAction: null,
}

function App() {
const form = useForm({
defaultValues: {
data: '',
},
// Define what meta values to expect on submission
onSubmitMeta: defaultMeta,
onSubmit: async ({ value, meta }) => {
// Do something with the values passed via handleSubmit
console.log(`Selected action - ${meta.submitAction}`, value)
},
})

return (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
}}
>
{/* ... */}
<button
type="submit"
// Overwrites the default specified in onSubmitMeta
onClick={() => form.handleSubmit({ submitAction: 'continue' })}
>
Submit and continue
</button>
<button
type="submit"
onClick={() => form.handleSubmit({ submitAction: 'backToMenu' })}
>
Submit and back to menu
</button>
</form>
)
}
```

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.
## Transforming data with Standard Schemas

Then when you call `onSubmit` you can provide it the predefined meta like so:
While Tanstack Form provides [Standard Schema support](./validation.md) for validation, it does not preserve the Schema's output data.

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:

```tsx
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit({
lastName: 'Astley',
})
}}
></form>
const schema = z.object({
age: z.string().transform((age) => Number(age)),
})

// Tanstack Form uses the input type of Standard Schemas
const defaultValues: z.input<typeof schema> = {
age: '13',
}

const form = useForm({
defaultValues,
validators: {
onChange: schema,
},
onSubmit: ({ value }) => {
const inputAge: string = value.age
// Pass it through the schema to get the transformed value
const result = schema.parse(value)
const outputAge: number = result.age
},
})
```
Loading