Skip to content

Commit cca8de7

Browse files
feat: Add alternative validation methods (#1622)
* chore: initial ideation * chore: move to correct location * chore: more work * chore: pass both tests * ci: apply automated fixes and generate docs * chore: fix ESlint * chore: added async logic * ci: apply automated fixes and generate docs * chore: add back default validation to the RHF values * ci: apply automated fixes and generate docs * chore: remove old async and sync validation logic * chore: mostly correct TS types * chore: fix minor type errors * chore: fix type tests * chore: fix Angular TS types * chore: fix Lit's types * docs: update React types * chore: finish solid types * chore: fix Svelte types * chore: fix Vue types * chore: add revalidate mode * chore: revert specific changes made to track handlers * chore: fix revalidate mode to behave as it does in RHF * chore: rename the submission attempts * ci: apply automated fixes and generate docs * chore: reintroduce validation logic * chore: fix submission validation logic * chore: fix types against `main` * chore: fix types * chore: regenerate lockfile and package upgrade * chore: regen lockfile against main * chore: add debounce timing to dynamic * chore: add tests to field validation as well * chore: add basic dynamic validation docs for React * docs: add example docs for dynamic usage * docs: add Lit dynamic validation docs * ci: apply automated fixes and generate docs * docs: add dynamic validation docs to Angular * docs: add solid dynamic validation example * docs: add Svelte docs for dynamic * docs: add dynamic validation to Vue * ci: apply automated fixes and generate docs * chore: fix CI * chore: move server validation to right place * fix: server validation should now function as-expected * chore: fix CI --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b4388e7 commit cca8de7

File tree

88 files changed

+7948
-4177
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+7948
-4177
lines changed

docs/config.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
"label": "Form Validation",
103103
"to": "framework/react/guides/validation"
104104
},
105+
{
106+
"label": "Dynamic Validation",
107+
"to": "framework/react/guides/dynamic-validation"
108+
},
105109
{
106110
"label": "Async Initial Values",
107111
"to": "framework/react/guides/async-initial-values"
@@ -163,6 +167,10 @@
163167
"label": "Form Validation",
164168
"to": "framework/vue/guides/validation"
165169
},
170+
{
171+
"label": "Dynamic Validation",
172+
"to": "framework/vue/guides/dynamic-validation"
173+
},
166174
{
167175
"label": "Async Initial Values",
168176
"to": "framework/vue/guides/async-initial-values"
@@ -188,6 +196,10 @@
188196
"label": "Form Validation",
189197
"to": "framework/angular/guides/validation"
190198
},
199+
{
200+
"label": "Dynamic Validation",
201+
"to": "framework/angular/guides/dynamic-validation"
202+
},
191203
{
192204
"label": "Arrays",
193205
"to": "framework/angular/guides/arrays"
@@ -209,6 +221,10 @@
209221
"label": "Form Validation",
210222
"to": "framework/solid/guides/validation"
211223
},
224+
{
225+
"label": "Dynamic Validation",
226+
"to": "framework/solid/guides/dynamic-validation"
227+
},
212228
{
213229
"label": "Async Initial Values",
214230
"to": "framework/solid/guides/async-initial-values"
@@ -238,6 +254,10 @@
238254
"label": "Form Validation",
239255
"to": "framework/lit/guides/validation"
240256
},
257+
{
258+
"label": "Dynamic Validation",
259+
"to": "framework/lit/guides/dynamic-validation"
260+
},
241261
{
242262
"label": "Arrays",
243263
"to": "framework/lit/guides/arrays"
@@ -255,6 +275,10 @@
255275
"label": "Form Validation",
256276
"to": "framework/svelte/guides/validation"
257277
},
278+
{
279+
"label": "Dynamic Validation",
280+
"to": "framework/svelte/guides/dynamic-validation"
281+
},
258282
{
259283
"label": "Async Initial Values",
260284
"to": "framework/svelte/guides/async-initial-values"
@@ -527,6 +551,10 @@
527551
"label": "Form Composition",
528552
"to": "framework/react/examples/large-form"
529553
},
554+
{
555+
"label": "Dynamic Validation",
556+
"to": "framework/react/examples/dynamic"
557+
},
530558
{
531559
"label": "TanStack Query Integration",
532560
"to": "framework/react/examples/query-integration"
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
---
2+
id: dynamic-validation
3+
title: Dynamic Validation
4+
---
5+
6+
In many cases, you want to change the validation rules based depending on the state of the form or other conditions. The most popular
7+
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.
8+
9+
We support this through our `onDynamic` validation function.
10+
11+
```angular-ts
12+
import { Component } from '@angular/core'
13+
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
14+
15+
@Component({
16+
selector: 'app-root',
17+
standalone: true,
18+
imports: [TanStackField],
19+
template: `
20+
<!-- Your form template here -->
21+
`,
22+
})
23+
export class AppComponent {
24+
form = injectForm({
25+
defaultValues: {
26+
firstName: '',
27+
lastName: '',
28+
},
29+
// If this is omitted, onDynamic will not be called
30+
validationLogic: revalidateLogic(),
31+
validators: {
32+
onDynamic: ({ value }) => {
33+
if (!value.firstName) {
34+
return { firstName: 'A first name is required' }
35+
}
36+
return undefined
37+
},
38+
},
39+
})
40+
}
41+
```
42+
43+
> By default `onDynamic` is not called, so you need to pass `revalidateLogic()` to the `validationLogic` option of `injectForm`.
44+
45+
## Revalidation Options
46+
47+
`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.
48+
49+
It takes two arguments:
50+
51+
- `mode`: The mode of validation prior to the first form submission. This can be one of the following:
52+
- `change`: Validate on every change.
53+
- `blur`: Validate on blur.
54+
- `submit`: Validate on submit. (**default**)
55+
56+
- `modeAfterSubmission`: The mode of validation after the form has been submitted. This can be one of the following:
57+
- `change`: Validate on every change. (**default**)
58+
- `blur`: Validate on blur.
59+
- `submit`: Validate on submit.
60+
61+
You can, for example, use the following to revalidate on blur after the first submission:
62+
63+
```angular-ts
64+
@Component({
65+
selector: 'app-root',
66+
standalone: true,
67+
imports: [TanStackField],
68+
template: `
69+
<!-- Your form template here -->
70+
`,
71+
})
72+
export class AppComponent {
73+
form = injectForm({
74+
// ...
75+
validationLogic: revalidateLogic({
76+
mode: 'submit',
77+
modeAfterSubmission: 'blur',
78+
}),
79+
// ...
80+
})
81+
}
82+
```
83+
84+
## Accessing Errors
85+
86+
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`.
87+
88+
```angular-ts
89+
import { Component } from '@angular/core'
90+
import { TanStackField, injectForm, injectStore, revalidateLogic } from '@tanstack/angular-form'
91+
92+
@Component({
93+
selector: 'app-root',
94+
standalone: true,
95+
imports: [TanStackField],
96+
template: `
97+
<p>{{ formErrorMap().onDynamic?.firstName }}</p>
98+
`,
99+
})
100+
export class AppComponent {
101+
form = injectForm({
102+
// ...
103+
validationLogic: revalidateLogic(),
104+
validators: {
105+
onDynamic: ({ value }) => {
106+
if (!value.firstName) {
107+
return { firstName: 'A first name is required' }
108+
}
109+
return undefined
110+
},
111+
},
112+
})
113+
114+
formErrorMap = injectStore(this.form, (state) => state.errorMap)
115+
}
116+
```
117+
118+
## Usage with Other Validation Logic
119+
120+
You can use `onDynamic` validation alongside other validation logic, such as `onChange` or `onBlur`.
121+
122+
```angular-ts
123+
import { Component } from '@angular/core'
124+
import { TanStackField, injectForm, injectStore, revalidateLogic } from '@tanstack/angular-form'
125+
126+
@Component({
127+
selector: 'app-root',
128+
standalone: true,
129+
imports: [TanStackField],
130+
template: `
131+
<div>
132+
<p>{{ formErrorMap().onChange?.firstName }}</p>
133+
<p>{{ formErrorMap().onDynamic?.lastName }}</p>
134+
</div>
135+
`,
136+
})
137+
export class AppComponent {
138+
form = injectForm({
139+
defaultValues: {
140+
firstName: '',
141+
lastName: '',
142+
},
143+
validationLogic: revalidateLogic(),
144+
validators: {
145+
onChange: ({ value }) => {
146+
if (!value.firstName) {
147+
return { firstName: 'A first name is required' }
148+
}
149+
return undefined
150+
},
151+
onDynamic: ({ value }) => {
152+
if (!value.lastName) {
153+
return { lastName: 'A last name is required' }
154+
}
155+
return undefined
156+
},
157+
},
158+
})
159+
160+
formErrorMap = injectStore(this.form, (state) => state.errorMap)
161+
}
162+
```
163+
164+
### Usage with Fields
165+
166+
You can also use `onDynamic` validation with fields, just like you would with other validation logic.
167+
168+
```angular-ts
169+
import { Component } from '@angular/core'
170+
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
171+
import type { FieldValidateFn } from '@tanstack/angular-form'
172+
173+
@Component({
174+
selector: 'app-root',
175+
standalone: true,
176+
imports: [TanStackField],
177+
template: `
178+
<form (submit)="handleSubmit($event)">
179+
<ng-container
180+
[tanstackField]="form"
181+
name="age"
182+
[validators]="{
183+
onDynamic: ageValidator
184+
}"
185+
#age="field"
186+
>
187+
<input
188+
type="number"
189+
[value]="age.api.state.value"
190+
(blur)="age.api.handleBlur()"
191+
(input)="age.api.handleChange($any($event).target.valueAsNumber)"
192+
/>
193+
@if (age.api.state.meta.errorMap.onDynamic) {
194+
<p style="color: red">
195+
{{ age.api.state.meta.errorMap.onDynamic }}
196+
</p>
197+
}
198+
</ng-container>
199+
<button type="submit">Submit</button>
200+
</form>
201+
`,
202+
})
203+
export class AppComponent {
204+
ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
205+
value > 18 ? undefined : 'Age must be greater than 18'
206+
207+
form = injectForm({
208+
defaultValues: {
209+
name: '',
210+
age: 0,
211+
},
212+
validationLogic: revalidateLogic(),
213+
onSubmit({ value }) {
214+
alert(JSON.stringify(value))
215+
},
216+
})
217+
218+
handleSubmit(event: SubmitEvent) {
219+
event.preventDefault()
220+
event.stopPropagation()
221+
this.form.handleSubmit()
222+
}
223+
}
224+
```
225+
226+
### Async Validation
227+
228+
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.
229+
230+
```angular-ts
231+
import { Component } from '@angular/core'
232+
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
233+
234+
@Component({
235+
selector: 'app-root',
236+
standalone: true,
237+
imports: [TanStackField],
238+
template: `
239+
<!-- Your form template here -->
240+
`,
241+
})
242+
export class AppComponent {
243+
form = injectForm({
244+
defaultValues: {
245+
username: '',
246+
},
247+
validationLogic: revalidateLogic(),
248+
validators: {
249+
onDynamicAsyncDebounceMs: 500, // Debounce the async validation by 500ms
250+
onDynamicAsync: async ({ value }) => {
251+
if (!value.username) {
252+
return { username: 'Username is required' }
253+
}
254+
// Simulate an async validation
255+
const isValid = await validateUsername(value.username)
256+
return isValid ? undefined : { username: 'Username is already taken' }
257+
},
258+
},
259+
})
260+
}
261+
```
262+
263+
### Standard Schema Validation
264+
265+
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.
266+
267+
```angular-ts
268+
import { Component } from '@angular/core'
269+
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
270+
import { z } from 'zod'
271+
272+
@Component({
273+
selector: 'app-root',
274+
standalone: true,
275+
imports: [TanStackField],
276+
template: `
277+
<!-- Your form template here -->
278+
`,
279+
})
280+
export class AppComponent {
281+
schema = z.object({
282+
firstName: z.string().min(1, 'A first name is required'),
283+
lastName: z.string().min(1, 'A last name is required'),
284+
})
285+
286+
form = injectForm({
287+
defaultValues: {
288+
firstName: '',
289+
lastName: '',
290+
},
291+
validationLogic: revalidateLogic(),
292+
validators: {
293+
onDynamic: this.schema,
294+
},
295+
})
296+
}
297+
```

0 commit comments

Comments
 (0)