@tanstack/form-core 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/legacy/FieldApi.cjs +5 -1
- package/build/legacy/FieldApi.cjs.map +1 -1
- package/build/legacy/FieldApi.d.cts +1 -1
- package/build/legacy/FieldApi.d.ts +1 -1
- package/build/legacy/FieldApi.js +5 -1
- package/build/legacy/FieldApi.js.map +1 -1
- package/build/legacy/FormApi.cjs +172 -9
- package/build/legacy/FormApi.cjs.map +1 -1
- package/build/legacy/FormApi.js +172 -9
- package/build/legacy/FormApi.js.map +1 -1
- package/build/legacy/index.d.cts +11 -4
- package/build/legacy/index.d.ts +11 -4
- package/build/modern/FieldApi.cjs +5 -1
- package/build/modern/FieldApi.cjs.map +1 -1
- package/build/modern/FieldApi.d.cts +1 -1
- package/build/modern/FieldApi.d.ts +1 -1
- package/build/modern/FieldApi.js +5 -1
- package/build/modern/FieldApi.js.map +1 -1
- package/build/modern/FormApi.cjs +171 -9
- package/build/modern/FormApi.cjs.map +1 -1
- package/build/modern/FormApi.js +171 -9
- package/build/modern/FormApi.js.map +1 -1
- package/build/modern/index.d.cts +11 -4
- package/build/modern/index.d.ts +11 -4
- package/package.json +1 -1
- package/src/FieldApi.ts +8 -4
- package/src/FormApi.ts +224 -12
- package/src/tests/FormApi.spec.ts +352 -0
package/src/FormApi.ts
CHANGED
|
@@ -21,6 +21,7 @@ type ValidateAsyncFn<TData, ValidatorType> = (
|
|
|
21
21
|
export type FormOptions<TData, ValidatorType> = {
|
|
22
22
|
defaultValues?: TData
|
|
23
23
|
defaultState?: Partial<FormState<TData>>
|
|
24
|
+
asyncAlways?: boolean
|
|
24
25
|
asyncDebounceMs?: number
|
|
25
26
|
validator?: ValidatorType
|
|
26
27
|
onMount?: ValidateOrFn<TData, ValidatorType>
|
|
@@ -47,8 +48,8 @@ export type FieldInfo<TFormData, ValidatorType> = {
|
|
|
47
48
|
export type ValidationMeta = {
|
|
48
49
|
validationCount?: number
|
|
49
50
|
validationAsyncCount?: number
|
|
50
|
-
validationPromise?: Promise<ValidationError[]>
|
|
51
|
-
validationResolve?: (errors: ValidationError[]) => void
|
|
51
|
+
validationPromise?: Promise<ValidationError[] | undefined>
|
|
52
|
+
validationResolve?: (errors: ValidationError[] | undefined) => void
|
|
52
53
|
validationReject?: (errors: unknown) => void
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -64,7 +65,8 @@ export type FormState<TData> = {
|
|
|
64
65
|
isFormValidating: boolean
|
|
65
66
|
formValidationCount: number
|
|
66
67
|
isFormValid: boolean
|
|
67
|
-
|
|
68
|
+
errors: ValidationError[]
|
|
69
|
+
errorMap: ValidationErrorMap
|
|
68
70
|
// Fields
|
|
69
71
|
fieldMeta: Record<DeepKeys<TData>, FieldMeta>
|
|
70
72
|
isFieldsValidating: boolean
|
|
@@ -84,6 +86,8 @@ function getDefaultFormState<TData>(
|
|
|
84
86
|
): FormState<TData> {
|
|
85
87
|
return {
|
|
86
88
|
values: defaultState.values ?? ({} as never),
|
|
89
|
+
errors: defaultState.errors ?? [],
|
|
90
|
+
errorMap: defaultState.errorMap ?? {},
|
|
87
91
|
fieldMeta: defaultState.fieldMeta ?? ({} as never),
|
|
88
92
|
canSubmit: defaultState.canSubmit ?? true,
|
|
89
93
|
isFieldsValid: defaultState.isFieldsValid ?? false,
|
|
@@ -141,7 +145,10 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
141
145
|
const isTouched = fieldMetaValues.some((field) => field?.isTouched)
|
|
142
146
|
|
|
143
147
|
const isValidating = isFieldsValidating || state.isFormValidating
|
|
144
|
-
|
|
148
|
+
state.errors = Object.values(state.errorMap).filter(
|
|
149
|
+
(val: unknown) => val !== undefined,
|
|
150
|
+
)
|
|
151
|
+
const isFormValid = state.errors.length === 0
|
|
145
152
|
const isValid = isFieldsValid && isFormValid
|
|
146
153
|
const canSubmit =
|
|
147
154
|
(state.submissionAttempts === 0 && !isTouched) ||
|
|
@@ -169,14 +176,23 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
mount = () => {
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
const doValidate = () => {
|
|
180
|
+
if (typeof this.options.onMount === 'function') {
|
|
181
|
+
return this.options.onMount(this.state.values, this)
|
|
182
|
+
}
|
|
183
|
+
if (this.options.validator) {
|
|
184
|
+
return (this.options.validator as Validator<TFormData>)().validate(
|
|
185
|
+
this.state.values,
|
|
186
|
+
this.options.onMount,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
174
189
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
const error = doValidate()
|
|
191
|
+
if (error) {
|
|
192
|
+
this.store.setState((prev) => ({
|
|
193
|
+
...prev,
|
|
194
|
+
errorMap: { ...prev.errorMap, onMount: error },
|
|
195
|
+
}))
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
198
|
|
|
@@ -245,6 +261,177 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
245
261
|
return Promise.all(fieldValidationPromises)
|
|
246
262
|
}
|
|
247
263
|
|
|
264
|
+
validateSync = (cause: ValidationCause): void => {
|
|
265
|
+
const { onChange, onBlur } = this.options
|
|
266
|
+
const validate =
|
|
267
|
+
cause === 'change' ? onChange : cause === 'blur' ? onBlur : undefined
|
|
268
|
+
if (!validate) return
|
|
269
|
+
|
|
270
|
+
const errorMapKey = getErrorMapKey(cause)
|
|
271
|
+
const doValidate = () => {
|
|
272
|
+
if (typeof validate === 'function') {
|
|
273
|
+
return validate(this.state.values, this) as ValidationError
|
|
274
|
+
}
|
|
275
|
+
if (this.options.validator && typeof validate !== 'function') {
|
|
276
|
+
return (this.options.validator as Validator<TFormData>)().validate(
|
|
277
|
+
this.state.values,
|
|
278
|
+
validate,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Form validation for ${errorMapKey} failed. ${errorMapKey} should either be a function, or \`validator\` should be correct.`,
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const error = normalizeError(doValidate())
|
|
287
|
+
if (this.state.errorMap[errorMapKey] !== error) {
|
|
288
|
+
this.store.setState((prev) => ({
|
|
289
|
+
...prev,
|
|
290
|
+
errorMap: {
|
|
291
|
+
...prev.errorMap,
|
|
292
|
+
[errorMapKey]: error,
|
|
293
|
+
},
|
|
294
|
+
}))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (this.state.errorMap[errorMapKey]) {
|
|
298
|
+
this.cancelValidateAsync()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
__leaseValidateAsync = () => {
|
|
303
|
+
const count = (this.validationMeta.validationAsyncCount || 0) + 1
|
|
304
|
+
this.validationMeta.validationAsyncCount = count
|
|
305
|
+
return count
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
cancelValidateAsync = () => {
|
|
309
|
+
// Lease a new validation count to ignore any pending validations
|
|
310
|
+
this.__leaseValidateAsync()
|
|
311
|
+
// Cancel any pending validation state
|
|
312
|
+
this.store.setState((prev) => ({
|
|
313
|
+
...prev,
|
|
314
|
+
isFormValidating: false,
|
|
315
|
+
}))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
validateAsync = async (
|
|
319
|
+
cause: ValidationCause,
|
|
320
|
+
): Promise<ValidationError[]> => {
|
|
321
|
+
const {
|
|
322
|
+
onChangeAsync,
|
|
323
|
+
onBlurAsync,
|
|
324
|
+
asyncDebounceMs,
|
|
325
|
+
onBlurAsyncDebounceMs,
|
|
326
|
+
onChangeAsyncDebounceMs,
|
|
327
|
+
} = this.options
|
|
328
|
+
|
|
329
|
+
const validate =
|
|
330
|
+
cause === 'change'
|
|
331
|
+
? onChangeAsync
|
|
332
|
+
: cause === 'blur'
|
|
333
|
+
? onBlurAsync
|
|
334
|
+
: undefined
|
|
335
|
+
|
|
336
|
+
if (!validate) return []
|
|
337
|
+
const debounceMs =
|
|
338
|
+
(cause === 'change' ? onChangeAsyncDebounceMs : onBlurAsyncDebounceMs) ??
|
|
339
|
+
asyncDebounceMs ??
|
|
340
|
+
0
|
|
341
|
+
|
|
342
|
+
if (!this.state.isFormValidating) {
|
|
343
|
+
this.store.setState((prev) => ({ ...prev, isFormValidating: true }))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Use the validationCount for all field instances to
|
|
347
|
+
// track freshness of the validation
|
|
348
|
+
const validationAsyncCount = this.__leaseValidateAsync()
|
|
349
|
+
|
|
350
|
+
const checkLatest = () =>
|
|
351
|
+
validationAsyncCount === this.validationMeta.validationAsyncCount
|
|
352
|
+
|
|
353
|
+
if (!this.validationMeta.validationPromise) {
|
|
354
|
+
this.validationMeta.validationPromise = new Promise((resolve, reject) => {
|
|
355
|
+
this.validationMeta.validationResolve = resolve
|
|
356
|
+
this.validationMeta.validationReject = reject
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (debounceMs > 0) {
|
|
361
|
+
await new Promise((r) => setTimeout(r, debounceMs))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const doValidate = () => {
|
|
365
|
+
if (typeof validate === 'function') {
|
|
366
|
+
return validate(this.state.values, this) as ValidationError
|
|
367
|
+
}
|
|
368
|
+
if (this.options.validator && typeof validate !== 'function') {
|
|
369
|
+
return (this.options.validator as Validator<TFormData>)().validateAsync(
|
|
370
|
+
this.state.values,
|
|
371
|
+
validate,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
const errorMapKey = getErrorMapKey(cause)
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Form validation for ${errorMapKey}Async failed. ${errorMapKey}Async should either be a function, or \`validator\` should be correct.`,
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Only kick off validation if this validation is the latest attempt
|
|
381
|
+
if (checkLatest()) {
|
|
382
|
+
const prevErrors = this.state.errors
|
|
383
|
+
try {
|
|
384
|
+
const rawError = await doValidate()
|
|
385
|
+
if (checkLatest()) {
|
|
386
|
+
const error = normalizeError(rawError)
|
|
387
|
+
this.store.setState((prev) => ({
|
|
388
|
+
...prev,
|
|
389
|
+
isFormValidating: false,
|
|
390
|
+
errorMap: {
|
|
391
|
+
...prev.errorMap,
|
|
392
|
+
[getErrorMapKey(cause)]: error,
|
|
393
|
+
},
|
|
394
|
+
}))
|
|
395
|
+
this.validationMeta.validationResolve?.([...prevErrors, error])
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (checkLatest()) {
|
|
399
|
+
this.validationMeta.validationReject?.([...prevErrors, error])
|
|
400
|
+
throw error
|
|
401
|
+
}
|
|
402
|
+
} finally {
|
|
403
|
+
if (checkLatest()) {
|
|
404
|
+
this.store.setState((prev) => ({ ...prev, isFormValidating: false }))
|
|
405
|
+
delete this.validationMeta.validationPromise
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Always return the latest validation promise to the caller
|
|
410
|
+
return (await this.validationMeta.validationPromise) ?? []
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
validate = (
|
|
414
|
+
cause: ValidationCause,
|
|
415
|
+
): ValidationError[] | Promise<ValidationError[]> => {
|
|
416
|
+
// Store the previous error for the errorMapKey (eg. onChange, onBlur, onSubmit)
|
|
417
|
+
const errorMapKey = getErrorMapKey(cause)
|
|
418
|
+
const prevError = this.state.errorMap[errorMapKey]
|
|
419
|
+
|
|
420
|
+
// Attempt to sync validate first
|
|
421
|
+
this.validateSync(cause)
|
|
422
|
+
|
|
423
|
+
const newError = this.state.errorMap[errorMapKey]
|
|
424
|
+
if (
|
|
425
|
+
prevError !== newError &&
|
|
426
|
+
!this.options.asyncAlways &&
|
|
427
|
+
!(newError === undefined && prevError !== undefined)
|
|
428
|
+
)
|
|
429
|
+
return this.state.errors
|
|
430
|
+
|
|
431
|
+
// No error? Attempt async validation
|
|
432
|
+
return this.validateAsync(cause)
|
|
433
|
+
}
|
|
434
|
+
|
|
248
435
|
handleSubmit = async () => {
|
|
249
436
|
// Check to see that the form and all fields have been touched
|
|
250
437
|
// If they have not, touch them all and run validation
|
|
@@ -279,7 +466,7 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
279
466
|
}
|
|
280
467
|
|
|
281
468
|
// Run validation for the form
|
|
282
|
-
|
|
469
|
+
await this.validate('submit')
|
|
283
470
|
|
|
284
471
|
if (!this.state.isValid) {
|
|
285
472
|
done()
|
|
@@ -428,3 +615,28 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
428
615
|
})
|
|
429
616
|
}
|
|
430
617
|
}
|
|
618
|
+
|
|
619
|
+
function normalizeError(rawError?: ValidationError) {
|
|
620
|
+
if (rawError) {
|
|
621
|
+
if (typeof rawError !== 'string') {
|
|
622
|
+
return 'Invalid Form Values'
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return rawError
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return undefined
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function getErrorMapKey(cause: ValidationCause) {
|
|
632
|
+
switch (cause) {
|
|
633
|
+
case 'submit':
|
|
634
|
+
return 'onSubmit'
|
|
635
|
+
case 'change':
|
|
636
|
+
return 'onChange'
|
|
637
|
+
case 'blur':
|
|
638
|
+
return 'onBlur'
|
|
639
|
+
case 'mount':
|
|
640
|
+
return 'onMount'
|
|
641
|
+
}
|
|
642
|
+
}
|
|
@@ -2,6 +2,7 @@ import { expect } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import { FormApi } from '../FormApi'
|
|
4
4
|
import { FieldApi } from '../FieldApi'
|
|
5
|
+
import { sleep } from './utils'
|
|
5
6
|
|
|
6
7
|
describe('form api', () => {
|
|
7
8
|
it('should get default form state', () => {
|
|
@@ -16,6 +17,8 @@ describe('form api', () => {
|
|
|
16
17
|
isFormValid: true,
|
|
17
18
|
isFormValidating: false,
|
|
18
19
|
isSubmitted: false,
|
|
20
|
+
errors: [],
|
|
21
|
+
errorMap: {},
|
|
19
22
|
isSubmitting: false,
|
|
20
23
|
isTouched: false,
|
|
21
24
|
isValid: true,
|
|
@@ -39,6 +42,8 @@ describe('form api', () => {
|
|
|
39
42
|
fieldMeta: {},
|
|
40
43
|
canSubmit: true,
|
|
41
44
|
isFieldsValid: true,
|
|
45
|
+
errors: [],
|
|
46
|
+
errorMap: {},
|
|
42
47
|
isFieldsValidating: false,
|
|
43
48
|
isFormValid: true,
|
|
44
49
|
isFormValidating: false,
|
|
@@ -62,6 +67,8 @@ describe('form api', () => {
|
|
|
62
67
|
expect(form.state).toEqual({
|
|
63
68
|
values: {},
|
|
64
69
|
fieldMeta: {},
|
|
70
|
+
errors: [],
|
|
71
|
+
errorMap: {},
|
|
65
72
|
canSubmit: true,
|
|
66
73
|
isFieldsValid: true,
|
|
67
74
|
isFieldsValidating: false,
|
|
@@ -97,6 +104,8 @@ describe('form api', () => {
|
|
|
97
104
|
values: {
|
|
98
105
|
name: 'other',
|
|
99
106
|
},
|
|
107
|
+
errors: [],
|
|
108
|
+
errorMap: {},
|
|
100
109
|
fieldMeta: {},
|
|
101
110
|
canSubmit: true,
|
|
102
111
|
isFieldsValid: true,
|
|
@@ -129,6 +138,8 @@ describe('form api', () => {
|
|
|
129
138
|
values: {
|
|
130
139
|
name: 'test',
|
|
131
140
|
},
|
|
141
|
+
errors: [],
|
|
142
|
+
errorMap: {},
|
|
132
143
|
fieldMeta: {},
|
|
133
144
|
canSubmit: true,
|
|
134
145
|
isFieldsValid: true,
|
|
@@ -316,4 +327,345 @@ describe('form api', () => {
|
|
|
316
327
|
expect(form.state.isFieldsValid).toEqual(true)
|
|
317
328
|
expect(form.state.canSubmit).toEqual(true)
|
|
318
329
|
})
|
|
330
|
+
|
|
331
|
+
it('should run validation onChange', () => {
|
|
332
|
+
const form = new FormApi({
|
|
333
|
+
defaultValues: {
|
|
334
|
+
name: 'test',
|
|
335
|
+
},
|
|
336
|
+
onChange: (value) => {
|
|
337
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
338
|
+
return
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const field = new FieldApi({
|
|
343
|
+
form,
|
|
344
|
+
name: 'name',
|
|
345
|
+
})
|
|
346
|
+
form.mount()
|
|
347
|
+
field.mount()
|
|
348
|
+
|
|
349
|
+
expect(form.state.errors.length).toBe(0)
|
|
350
|
+
field.setValue('other', { touch: true })
|
|
351
|
+
expect(form.state.errors).toContain('Please enter a different value')
|
|
352
|
+
expect(form.state.errorMap).toMatchObject({
|
|
353
|
+
onChange: 'Please enter a different value',
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('should run async validation onChange', async () => {
|
|
358
|
+
vi.useFakeTimers()
|
|
359
|
+
|
|
360
|
+
const form = new FormApi({
|
|
361
|
+
defaultValues: {
|
|
362
|
+
name: 'test',
|
|
363
|
+
},
|
|
364
|
+
onChangeAsync: async (value) => {
|
|
365
|
+
await sleep(1000)
|
|
366
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
367
|
+
return
|
|
368
|
+
},
|
|
369
|
+
})
|
|
370
|
+
const field = new FieldApi({
|
|
371
|
+
form,
|
|
372
|
+
name: 'name',
|
|
373
|
+
})
|
|
374
|
+
form.mount()
|
|
375
|
+
|
|
376
|
+
field.mount()
|
|
377
|
+
|
|
378
|
+
expect(form.state.errors.length).toBe(0)
|
|
379
|
+
field.setValue('other', { touch: true })
|
|
380
|
+
await vi.runAllTimersAsync()
|
|
381
|
+
expect(form.state.errors).toContain('Please enter a different value')
|
|
382
|
+
expect(form.state.errorMap).toMatchObject({
|
|
383
|
+
onChange: 'Please enter a different value',
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should run async validation onChange with debounce', async () => {
|
|
388
|
+
vi.useFakeTimers()
|
|
389
|
+
const sleepMock = vi.fn().mockImplementation(sleep)
|
|
390
|
+
|
|
391
|
+
const form = new FormApi({
|
|
392
|
+
defaultValues: {
|
|
393
|
+
name: 'test',
|
|
394
|
+
},
|
|
395
|
+
onChangeAsyncDebounceMs: 1000,
|
|
396
|
+
onChangeAsync: async (value) => {
|
|
397
|
+
await sleepMock(1000)
|
|
398
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
399
|
+
return
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
const field = new FieldApi({
|
|
403
|
+
form,
|
|
404
|
+
name: 'name',
|
|
405
|
+
})
|
|
406
|
+
form.mount()
|
|
407
|
+
|
|
408
|
+
field.mount()
|
|
409
|
+
|
|
410
|
+
expect(form.state.errors.length).toBe(0)
|
|
411
|
+
field.setValue('other', { touch: true })
|
|
412
|
+
field.setValue('other')
|
|
413
|
+
await vi.runAllTimersAsync()
|
|
414
|
+
// sleepMock will have been called 2 times without onChangeAsyncDebounceMs
|
|
415
|
+
expect(sleepMock).toHaveBeenCalledTimes(1)
|
|
416
|
+
expect(form.state.errors).toContain('Please enter a different value')
|
|
417
|
+
expect(form.state.errorMap).toMatchObject({
|
|
418
|
+
onChange: 'Please enter a different value',
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('should run async validation onChange with asyncDebounceMs', async () => {
|
|
423
|
+
vi.useFakeTimers()
|
|
424
|
+
const sleepMock = vi.fn().mockImplementation(sleep)
|
|
425
|
+
|
|
426
|
+
const form = new FormApi({
|
|
427
|
+
defaultValues: {
|
|
428
|
+
name: 'test',
|
|
429
|
+
},
|
|
430
|
+
asyncDebounceMs: 1000,
|
|
431
|
+
onChangeAsync: async (value) => {
|
|
432
|
+
await sleepMock(1000)
|
|
433
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
434
|
+
return
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
const field = new FieldApi({
|
|
438
|
+
form,
|
|
439
|
+
name: 'name',
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
form.mount()
|
|
443
|
+
field.mount()
|
|
444
|
+
|
|
445
|
+
expect(form.state.errors.length).toBe(0)
|
|
446
|
+
field.setValue('other', { touch: true })
|
|
447
|
+
field.setValue('other')
|
|
448
|
+
await vi.runAllTimersAsync()
|
|
449
|
+
// sleepMock will have been called 2 times without asyncDebounceMs
|
|
450
|
+
expect(sleepMock).toHaveBeenCalledTimes(1)
|
|
451
|
+
expect(form.state.errors).toContain('Please enter a different value')
|
|
452
|
+
expect(form.state.errorMap).toMatchObject({
|
|
453
|
+
onChange: 'Please enter a different value',
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('should run validation onBlur', () => {
|
|
458
|
+
const form = new FormApi({
|
|
459
|
+
defaultValues: {
|
|
460
|
+
name: 'other',
|
|
461
|
+
},
|
|
462
|
+
onBlur: (value) => {
|
|
463
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
464
|
+
return
|
|
465
|
+
},
|
|
466
|
+
})
|
|
467
|
+
const field = new FieldApi({
|
|
468
|
+
form,
|
|
469
|
+
name: 'name',
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
form.mount()
|
|
473
|
+
field.mount()
|
|
474
|
+
|
|
475
|
+
field.setValue('other', { touch: true })
|
|
476
|
+
field.validate('blur')
|
|
477
|
+
expect(form.state.errors).toContain('Please enter a different value')
|
|
478
|
+
expect(form.state.errorMap).toMatchObject({
|
|
479
|
+
onBlur: 'Please enter a different value',
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('should run async validation onBlur', async () => {
|
|
484
|
+
vi.useFakeTimers()
|
|
485
|
+
|
|
486
|
+
const form = new FormApi({
|
|
487
|
+
defaultValues: {
|
|
488
|
+
name: 'test',
|
|
489
|
+
},
|
|
490
|
+
onBlurAsync: async (value) => {
|
|
491
|
+
await sleep(1000)
|
|
492
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
493
|
+
return
|
|
494
|
+
},
|
|
495
|
+
})
|
|
496
|
+
const field = new FieldApi({
|
|
497
|
+
form,
|
|
498
|
+
name: 'name',
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
form.mount()
|
|
502
|
+
field.mount()
|
|
503
|
+
|
|
504
|
+
expect(form.state.errors.length).toBe(0)
|
|
505
|
+
field.setValue('other', { touch: true })
|
|
506
|
+
field.validate('blur')
|
|
507
|
+
await vi.runAllTimersAsync()
|
|
508
|
+
expect(form.state.errors).toContain('Please enter a different value')
|
|
509
|
+
expect(form.state.errorMap).toMatchObject({
|
|
510
|
+
onBlur: 'Please enter a different value',
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('should run async validation onBlur with debounce', async () => {
|
|
515
|
+
vi.useFakeTimers()
|
|
516
|
+
const sleepMock = vi.fn().mockImplementation(sleep)
|
|
517
|
+
|
|
518
|
+
const form = new FormApi({
|
|
519
|
+
defaultValues: {
|
|
520
|
+
name: 'test',
|
|
521
|
+
},
|
|
522
|
+
onBlurAsyncDebounceMs: 1000,
|
|
523
|
+
onBlurAsync: async (value) => {
|
|
524
|
+
await sleepMock(10)
|
|
525
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
526
|
+
return
|
|
527
|
+
},
|
|
528
|
+
})
|
|
529
|
+
const field = new FieldApi({
|
|
530
|
+
form,
|
|
531
|
+
name: 'name',
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
form.mount()
|
|
535
|
+
field.mount()
|
|
536
|
+
|
|
537
|
+
expect(form.state.errors.length).toBe(0)
|
|
538
|
+
field.setValue('other', { touch: true })
|
|
539
|
+
field.validate('blur')
|
|
540
|
+
field.validate('blur')
|
|
541
|
+
await vi.runAllTimersAsync()
|
|
542
|
+
// sleepMock will have been called 2 times without onBlurAsyncDebounceMs
|
|
543
|
+
expect(sleepMock).toHaveBeenCalledTimes(1)
|
|
544
|
+
expect(form.state.errors).toContain('Please enter a different value')
|
|
545
|
+
expect(form.state.errorMap).toMatchObject({
|
|
546
|
+
onBlur: 'Please enter a different value',
|
|
547
|
+
})
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('should run async validation onBlur with asyncDebounceMs', async () => {
|
|
551
|
+
vi.useFakeTimers()
|
|
552
|
+
const sleepMock = vi.fn().mockImplementation(sleep)
|
|
553
|
+
|
|
554
|
+
const form = new FormApi({
|
|
555
|
+
defaultValues: {
|
|
556
|
+
name: 'test',
|
|
557
|
+
},
|
|
558
|
+
asyncDebounceMs: 1000,
|
|
559
|
+
onBlurAsync: async (value) => {
|
|
560
|
+
await sleepMock(10)
|
|
561
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
562
|
+
return
|
|
563
|
+
},
|
|
564
|
+
})
|
|
565
|
+
const field = new FieldApi({
|
|
566
|
+
form,
|
|
567
|
+
name: 'name',
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
form.mount()
|
|
571
|
+
field.mount()
|
|
572
|
+
|
|
573
|
+
expect(form.state.errors.length).toBe(0)
|
|
574
|
+
field.setValue('other', { touch: true })
|
|
575
|
+
field.validate('blur')
|
|
576
|
+
field.validate('blur')
|
|
577
|
+
await vi.runAllTimersAsync()
|
|
578
|
+
// sleepMock will have been called 2 times without asyncDebounceMs
|
|
579
|
+
expect(sleepMock).toHaveBeenCalledTimes(1)
|
|
580
|
+
expect(form.state.errors).toContain('Please enter a different value')
|
|
581
|
+
expect(form.state.errorMap).toMatchObject({
|
|
582
|
+
onBlur: 'Please enter a different value',
|
|
583
|
+
})
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
it('should contain multiple errors when running validation onBlur and onChange', () => {
|
|
587
|
+
const form = new FormApi({
|
|
588
|
+
defaultValues: {
|
|
589
|
+
name: 'other',
|
|
590
|
+
},
|
|
591
|
+
onBlur: (value) => {
|
|
592
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
593
|
+
return
|
|
594
|
+
},
|
|
595
|
+
onChange: (value) => {
|
|
596
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
597
|
+
return
|
|
598
|
+
},
|
|
599
|
+
})
|
|
600
|
+
const field = new FieldApi({
|
|
601
|
+
form,
|
|
602
|
+
name: 'name',
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
form.mount()
|
|
606
|
+
field.mount()
|
|
607
|
+
|
|
608
|
+
field.setValue('other', { touch: true })
|
|
609
|
+
field.validate('blur')
|
|
610
|
+
expect(form.state.errors).toStrictEqual([
|
|
611
|
+
'Please enter a different value',
|
|
612
|
+
'Please enter a different value',
|
|
613
|
+
])
|
|
614
|
+
expect(form.state.errorMap).toEqual({
|
|
615
|
+
onBlur: 'Please enter a different value',
|
|
616
|
+
onChange: 'Please enter a different value',
|
|
617
|
+
})
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it('should reset onChange errors when the issue is resolved', () => {
|
|
621
|
+
const form = new FormApi({
|
|
622
|
+
defaultValues: {
|
|
623
|
+
name: 'other',
|
|
624
|
+
},
|
|
625
|
+
onChange: (value) => {
|
|
626
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
627
|
+
return
|
|
628
|
+
},
|
|
629
|
+
})
|
|
630
|
+
const field = new FieldApi({
|
|
631
|
+
form,
|
|
632
|
+
name: 'name',
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
form.mount()
|
|
636
|
+
field.mount()
|
|
637
|
+
|
|
638
|
+
field.setValue('other', { touch: true })
|
|
639
|
+
expect(form.state.errors).toStrictEqual(['Please enter a different value'])
|
|
640
|
+
expect(form.state.errorMap).toEqual({
|
|
641
|
+
onChange: 'Please enter a different value',
|
|
642
|
+
})
|
|
643
|
+
field.setValue('test', { touch: true })
|
|
644
|
+
expect(form.state.errors).toStrictEqual([])
|
|
645
|
+
expect(form.state.errorMap).toEqual({})
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
it('should return error onMount', () => {
|
|
649
|
+
const form = new FormApi({
|
|
650
|
+
defaultValues: {
|
|
651
|
+
name: 'other',
|
|
652
|
+
},
|
|
653
|
+
onMount: (value) => {
|
|
654
|
+
if (value.name === 'other') return 'Please enter a different value'
|
|
655
|
+
return
|
|
656
|
+
},
|
|
657
|
+
})
|
|
658
|
+
const field = new FieldApi({
|
|
659
|
+
form,
|
|
660
|
+
name: 'name',
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
form.mount()
|
|
664
|
+
field.mount()
|
|
665
|
+
|
|
666
|
+
expect(form.state.errors).toStrictEqual(['Please enter a different value'])
|
|
667
|
+
expect(form.state.errorMap).toEqual({
|
|
668
|
+
onMount: 'Please enter a different value',
|
|
669
|
+
})
|
|
670
|
+
})
|
|
319
671
|
})
|