Skip to content

Add alternative validation methods #1622

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 50 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8e5ac2d
chore: initial ideation
crutchcorn Jul 22, 2025
9d224e1
chore: move to correct location
crutchcorn Jul 22, 2025
6f65c16
chore: more work
crutchcorn Jul 22, 2025
7c7b38f
chore: pass both tests
crutchcorn Jul 22, 2025
b8416c8
ci: apply automated fixes and generate docs
autofix-ci[bot] Jul 22, 2025
ac68951
chore: fix ESlint
crutchcorn Jul 23, 2025
6b21692
chore: added async logic
crutchcorn Aug 2, 2025
541ce2d
ci: apply automated fixes and generate docs
autofix-ci[bot] Aug 2, 2025
d1ae4bc
chore: add back default validation to the RHF values
crutchcorn Aug 2, 2025
b7a440c
ci: apply automated fixes and generate docs
autofix-ci[bot] Aug 2, 2025
47c78b6
chore: remove old async and sync validation logic
crutchcorn Aug 2, 2025
ca32189
chore: mostly correct TS types
crutchcorn Aug 2, 2025
ce0bee8
chore: fix minor type errors
crutchcorn Aug 2, 2025
12ab107
chore: fix type tests
crutchcorn Aug 2, 2025
59f120e
chore: fix Angular TS types
crutchcorn Aug 2, 2025
a6c0646
chore: fix Lit's types
crutchcorn Aug 2, 2025
9cf835b
docs: update React types
crutchcorn Aug 2, 2025
f04cffe
chore: finish solid types
crutchcorn Aug 2, 2025
2268fab
chore: fix Svelte types
crutchcorn Aug 2, 2025
9dca4b7
chore: fix Vue types
crutchcorn Aug 2, 2025
b68da69
chore: add revalidate mode
crutchcorn Aug 2, 2025
da1041c
chore: revert specific changes made to track handlers
crutchcorn Aug 3, 2025
32b69a9
chore: fix revalidate mode to behave as it does in RHF
crutchcorn Aug 3, 2025
7ee43b2
chore: rename the submission attempts
crutchcorn Aug 3, 2025
d35c64b
ci: apply automated fixes and generate docs
autofix-ci[bot] Aug 3, 2025
d0c204a
chore: reintroduce validation logic
crutchcorn Aug 3, 2025
8cefb21
chore: fix submission validation logic
crutchcorn Aug 3, 2025
b573a61
chore: fix types against `main`
crutchcorn Aug 3, 2025
b1b3221
chore: fix types
crutchcorn Aug 4, 2025
9b056cf
chore: regenerate lockfile and package upgrade
crutchcorn Aug 4, 2025
da3df18
chore: regen lockfile against main
crutchcorn Aug 4, 2025
205dabe
Merge branch 'main' into rhf-validate
crutchcorn Aug 4, 2025
eaff3e7
chore: add debounce timing to dynamic
crutchcorn Aug 4, 2025
e38cab2
chore: add tests to field validation as well
crutchcorn Aug 4, 2025
9e136b5
chore: add basic dynamic validation docs for React
crutchcorn Aug 4, 2025
6007b1f
docs: add example docs for dynamic usage
crutchcorn Aug 4, 2025
36f5db8
docs: add Lit dynamic validation docs
crutchcorn Aug 4, 2025
b01c811
ci: apply automated fixes and generate docs
autofix-ci[bot] Aug 4, 2025
369ad04
docs: add dynamic validation docs to Angular
crutchcorn Aug 4, 2025
c783613
Merge branch 'rhf-validate' of https://github.com/TanStack/form into …
crutchcorn Aug 4, 2025
a354397
docs: add solid dynamic validation example
crutchcorn Aug 4, 2025
1c779cb
docs: add Svelte docs for dynamic
crutchcorn Aug 4, 2025
2140cea
docs: add dynamic validation to Vue
crutchcorn Aug 4, 2025
3bd661f
ci: apply automated fixes and generate docs
autofix-ci[bot] Aug 4, 2025
f0eb07d
chore: fix CI
crutchcorn Aug 4, 2025
7bd0177
Merge branch 'rhf-validate' of https://github.com/TanStack/form into …
crutchcorn Aug 4, 2025
c50a835
chore: move server validation to right place
crutchcorn Aug 4, 2025
31c3aea
fix: server validation should now function as-expected
crutchcorn Aug 4, 2025
33be9b2
Merge branch 'main' into rhf-validate
crutchcorn Aug 4, 2025
fcb613e
chore: fix CI
crutchcorn Aug 4, 2025
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
28 changes: 28 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@
"label": "Form Validation",
"to": "framework/react/guides/validation"
},
{
"label": "Dynamic Validation",
"to": "framework/react/guides/dynamic-validation"
},
{
"label": "Async Initial Values",
"to": "framework/react/guides/async-initial-values"
Expand Down Expand Up @@ -163,6 +167,10 @@
"label": "Form Validation",
"to": "framework/vue/guides/validation"
},
{
"label": "Dynamic Validation",
"to": "framework/vue/guides/dynamic-validation"
},
{
"label": "Async Initial Values",
"to": "framework/vue/guides/async-initial-values"
Expand All @@ -188,6 +196,10 @@
"label": "Form Validation",
"to": "framework/angular/guides/validation"
},
{
"label": "Dynamic Validation",
"to": "framework/angular/guides/dynamic-validation"
},
{
"label": "Arrays",
"to": "framework/angular/guides/arrays"
Expand All @@ -209,6 +221,10 @@
"label": "Form Validation",
"to": "framework/solid/guides/validation"
},
{
"label": "Dynamic Validation",
"to": "framework/solid/guides/dynamic-validation"
},
{
"label": "Async Initial Values",
"to": "framework/solid/guides/async-initial-values"
Expand Down Expand Up @@ -238,6 +254,10 @@
"label": "Form Validation",
"to": "framework/lit/guides/validation"
},
{
"label": "Dynamic Validation",
"to": "framework/lit/guides/dynamic-validation"
},
{
"label": "Arrays",
"to": "framework/lit/guides/arrays"
Expand All @@ -255,6 +275,10 @@
"label": "Form Validation",
"to": "framework/svelte/guides/validation"
},
{
"label": "Dynamic Validation",
"to": "framework/svelte/guides/dynamic-validation"
},
{
"label": "Async Initial Values",
"to": "framework/svelte/guides/async-initial-values"
Expand Down Expand Up @@ -527,6 +551,10 @@
"label": "Form Composition",
"to": "framework/react/examples/large-form"
},
{
"label": "Dynamic Validation",
"to": "framework/react/examples/dynamic"
},
{
"label": "TanStack Query Integration",
"to": "framework/react/examples/query-integration"
Expand Down
297 changes: 297 additions & 0 deletions docs/framework/angular/guides/dynamic-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
---
id: dynamic-validation
title: Dynamic Validation
---

In many cases, you want to change the validation rules based depending on the state of the form or other conditions. The most popular
example of this is when you want to validate a field differently based on whether the user has submitted the form for the first time or not.

We support this through our `onDynamic` validation function.

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

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<!-- Your form template here -->
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
firstName: '',
lastName: '',
},
// If this is omitted, onDynamic will not be called
validationLogic: revalidateLogic(),
validators: {
onDynamic: ({ value }) => {
if (!value.firstName) {
return { firstName: 'A first name is required' }
}
return undefined
},
},
})
}
```

> By default `onDynamic` is not called, so you need to pass `revalidateLogic()` to the `validationLogic` option of `injectForm`.

## Revalidation Options

`revalidateLogic` allows you to specify when validation should be run and change the validation rules dynamically based on the current submission state of the form.

It takes two arguments:

- `mode`: The mode of validation prior to the first form submission. This can be one of the following:
- `change`: Validate on every change.
- `blur`: Validate on blur.
- `submit`: Validate on submit. (**default**)

- `modeAfterSubmission`: The mode of validation after the form has been submitted. This can be one of the following:
- `change`: Validate on every change. (**default**)
- `blur`: Validate on blur.
- `submit`: Validate on submit.

You can, for example, use the following to revalidate on blur after the first submission:

```angular-ts
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<!-- Your form template here -->
`,
})
export class AppComponent {
form = injectForm({
// ...
validationLogic: revalidateLogic({
mode: 'submit',
modeAfterSubmission: 'blur',
}),
// ...
})
}
```

## Accessing Errors

Just as you might access errors from an `onChange` or `onBlur` validation, you can access the errors from the `onDynamic` validation function using the form's error map through `injectStore`.

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, injectStore, revalidateLogic } from '@tanstack/angular-form'

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<p>{{ formErrorMap().onDynamic?.firstName }}</p>
`,
})
export class AppComponent {
form = injectForm({
// ...
validationLogic: revalidateLogic(),
validators: {
onDynamic: ({ value }) => {
if (!value.firstName) {
return { firstName: 'A first name is required' }
}
return undefined
},
},
})

formErrorMap = injectStore(this.form, (state) => state.errorMap)
}
```

## Usage with Other Validation Logic

You can use `onDynamic` validation alongside other validation logic, such as `onChange` or `onBlur`.

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, injectStore, revalidateLogic } from '@tanstack/angular-form'

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<div>
<p>{{ formErrorMap().onChange?.firstName }}</p>
<p>{{ formErrorMap().onDynamic?.lastName }}</p>
</div>
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
firstName: '',
lastName: '',
},
validationLogic: revalidateLogic(),
validators: {
onChange: ({ value }) => {
if (!value.firstName) {
return { firstName: 'A first name is required' }
}
return undefined
},
onDynamic: ({ value }) => {
if (!value.lastName) {
return { lastName: 'A last name is required' }
}
return undefined
},
},
})

formErrorMap = injectStore(this.form, (state) => state.errorMap)
}
```

### Usage with Fields

You can also use `onDynamic` validation with fields, just like you would with other validation logic.

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
import type { FieldValidateFn } from '@tanstack/angular-form'

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<form (submit)="handleSubmit($event)">
<ng-container
[tanstackField]="form"
name="age"
[validators]="{
onDynamic: ageValidator
}"
#age="field"
>
<input
type="number"
[value]="age.api.state.value"
(blur)="age.api.handleBlur()"
(input)="age.api.handleChange($any($event).target.valueAsNumber)"
/>
@if (age.api.state.meta.errorMap.onDynamic) {
<p style="color: red">
{{ age.api.state.meta.errorMap.onDynamic }}
</p>
}
</ng-container>
<button type="submit">Submit</button>
</form>
`,
})
export class AppComponent {
ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
value > 18 ? undefined : 'Age must be greater than 18'

form = injectForm({
defaultValues: {
name: '',
age: 0,
},
validationLogic: revalidateLogic(),
onSubmit({ value }) {
alert(JSON.stringify(value))
},
})

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

### Async Validation

Async validation can also be used with `onDynamic` just like with other validation logic. You can even debounce the async validation to avoid excessive calls.

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

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<!-- Your form template here -->
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
username: '',
},
validationLogic: revalidateLogic(),
validators: {
onDynamicAsyncDebounceMs: 500, // Debounce the async validation by 500ms
onDynamicAsync: async ({ value }) => {
if (!value.username) {
return { username: 'Username is required' }
}
// Simulate an async validation
const isValid = await validateUsername(value.username)
return isValid ? undefined : { username: 'Username is already taken' }
},
},
})
}
```

### Standard Schema Validation

You can also use standard schema validation libraries like Valibot or Zod with `onDynamic` validation. This allows you to define complex validation rules that can change dynamically based on the form state.

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
import { z } from 'zod'

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<!-- Your form template here -->
`,
})
export class AppComponent {
schema = z.object({
firstName: z.string().min(1, 'A first name is required'),
lastName: z.string().min(1, 'A last name is required'),
})

form = injectForm({
defaultValues: {
firstName: '',
lastName: '',
},
validationLogic: revalidateLogic(),
validators: {
onDynamic: this.schema,
},
})
}
```
Loading