@tanstack/form-core 0.7.2 → 0.8.1
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 +171 -7
- package/build/legacy/FormApi.cjs.map +1 -1
- package/build/legacy/FormApi.js +171 -7
- 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 +170 -7
- package/build/modern/FormApi.cjs.map +1 -1
- package/build/modern/FormApi.js +170 -7
- 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 +227 -11
- 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,15 +176,29 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
mount = () => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
const doValidate = () => {
|
|
180
|
+
if (
|
|
181
|
+
this.options.validator &&
|
|
182
|
+
typeof this.options.onMount !== 'function'
|
|
183
|
+
) {
|
|
184
|
+
return (this.options.validator as Validator<TFormData>)().validate(
|
|
185
|
+
this.state.values,
|
|
186
|
+
this.options.onMount,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
return (this.options.onMount as ValidateFn<TFormData, ValidatorType>)(
|
|
177
190
|
this.state.values,
|
|
178
|
-
this
|
|
191
|
+
this,
|
|
179
192
|
)
|
|
180
193
|
}
|
|
194
|
+
if (!this.options.onMount) return
|
|
195
|
+
const error = doValidate()
|
|
196
|
+
if (error) {
|
|
197
|
+
this.store.setState((prev) => ({
|
|
198
|
+
...prev,
|
|
199
|
+
errorMap: { ...prev.errorMap, onMount: error },
|
|
200
|
+
}))
|
|
201
|
+
}
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
update = (options?: FormOptions<TFormData, ValidatorType>) => {
|
|
@@ -245,6 +266,176 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
245
266
|
return Promise.all(fieldValidationPromises)
|
|
246
267
|
}
|
|
247
268
|
|
|
269
|
+
validateSync = (cause: ValidationCause): void => {
|
|
270
|
+
const { onChange, onBlur } = this.options
|
|
271
|
+
const validate =
|
|
272
|
+
cause === 'change' ? onChange : cause === 'blur' ? onBlur : undefined
|
|
273
|
+
if (!validate) return
|
|
274
|
+
|
|
275
|
+
const errorMapKey = getErrorMapKey(cause)
|
|
276
|
+
const doValidate = () => {
|
|
277
|
+
if (this.options.validator && typeof validate !== 'function') {
|
|
278
|
+
return (this.options.validator as Validator<TFormData>)().validate(
|
|
279
|
+
this.state.values,
|
|
280
|
+
validate,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return (validate as ValidateFn<TFormData, ValidatorType>)(
|
|
285
|
+
this.state.values,
|
|
286
|
+
this,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const error = normalizeError(doValidate())
|
|
291
|
+
if (this.state.errorMap[errorMapKey] !== error) {
|
|
292
|
+
this.store.setState((prev) => ({
|
|
293
|
+
...prev,
|
|
294
|
+
errorMap: {
|
|
295
|
+
...prev.errorMap,
|
|
296
|
+
[errorMapKey]: error,
|
|
297
|
+
},
|
|
298
|
+
}))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (this.state.errorMap[errorMapKey]) {
|
|
302
|
+
this.cancelValidateAsync()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
__leaseValidateAsync = () => {
|
|
307
|
+
const count = (this.validationMeta.validationAsyncCount || 0) + 1
|
|
308
|
+
this.validationMeta.validationAsyncCount = count
|
|
309
|
+
return count
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
cancelValidateAsync = () => {
|
|
313
|
+
// Lease a new validation count to ignore any pending validations
|
|
314
|
+
this.__leaseValidateAsync()
|
|
315
|
+
// Cancel any pending validation state
|
|
316
|
+
this.store.setState((prev) => ({
|
|
317
|
+
...prev,
|
|
318
|
+
isFormValidating: false,
|
|
319
|
+
}))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
validateAsync = async (
|
|
323
|
+
cause: ValidationCause,
|
|
324
|
+
): Promise<ValidationError[]> => {
|
|
325
|
+
const {
|
|
326
|
+
onChangeAsync,
|
|
327
|
+
onBlurAsync,
|
|
328
|
+
asyncDebounceMs,
|
|
329
|
+
onBlurAsyncDebounceMs,
|
|
330
|
+
onChangeAsyncDebounceMs,
|
|
331
|
+
} = this.options
|
|
332
|
+
|
|
333
|
+
const validate =
|
|
334
|
+
cause === 'change'
|
|
335
|
+
? onChangeAsync
|
|
336
|
+
: cause === 'blur'
|
|
337
|
+
? onBlurAsync
|
|
338
|
+
: undefined
|
|
339
|
+
|
|
340
|
+
if (!validate) return []
|
|
341
|
+
const debounceMs =
|
|
342
|
+
(cause === 'change' ? onChangeAsyncDebounceMs : onBlurAsyncDebounceMs) ??
|
|
343
|
+
asyncDebounceMs ??
|
|
344
|
+
0
|
|
345
|
+
|
|
346
|
+
if (!this.state.isFormValidating) {
|
|
347
|
+
this.store.setState((prev) => ({ ...prev, isFormValidating: true }))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Use the validationCount for all field instances to
|
|
351
|
+
// track freshness of the validation
|
|
352
|
+
const validationAsyncCount = this.__leaseValidateAsync()
|
|
353
|
+
|
|
354
|
+
const checkLatest = () =>
|
|
355
|
+
validationAsyncCount === this.validationMeta.validationAsyncCount
|
|
356
|
+
|
|
357
|
+
if (!this.validationMeta.validationPromise) {
|
|
358
|
+
this.validationMeta.validationPromise = new Promise((resolve, reject) => {
|
|
359
|
+
this.validationMeta.validationResolve = resolve
|
|
360
|
+
this.validationMeta.validationReject = reject
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (debounceMs > 0) {
|
|
365
|
+
await new Promise((r) => setTimeout(r, debounceMs))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const doValidate = () => {
|
|
369
|
+
if (typeof validate === 'function') {
|
|
370
|
+
return validate(this.state.values, this) as ValidationError
|
|
371
|
+
}
|
|
372
|
+
if (this.options.validator && typeof validate !== 'function') {
|
|
373
|
+
return (this.options.validator as Validator<TFormData>)().validateAsync(
|
|
374
|
+
this.state.values,
|
|
375
|
+
validate,
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
const errorMapKey = getErrorMapKey(cause)
|
|
379
|
+
throw new Error(
|
|
380
|
+
`Form validation for ${errorMapKey}Async failed. ${errorMapKey}Async should either be a function, or \`validator\` should be correct.`,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Only kick off validation if this validation is the latest attempt
|
|
385
|
+
if (checkLatest()) {
|
|
386
|
+
const prevErrors = this.state.errors
|
|
387
|
+
try {
|
|
388
|
+
const rawError = await doValidate()
|
|
389
|
+
if (checkLatest()) {
|
|
390
|
+
const error = normalizeError(rawError)
|
|
391
|
+
this.store.setState((prev) => ({
|
|
392
|
+
...prev,
|
|
393
|
+
isFormValidating: false,
|
|
394
|
+
errorMap: {
|
|
395
|
+
...prev.errorMap,
|
|
396
|
+
[getErrorMapKey(cause)]: error,
|
|
397
|
+
},
|
|
398
|
+
}))
|
|
399
|
+
this.validationMeta.validationResolve?.([...prevErrors, error])
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (checkLatest()) {
|
|
403
|
+
this.validationMeta.validationReject?.([...prevErrors, error])
|
|
404
|
+
throw error
|
|
405
|
+
}
|
|
406
|
+
} finally {
|
|
407
|
+
if (checkLatest()) {
|
|
408
|
+
this.store.setState((prev) => ({ ...prev, isFormValidating: false }))
|
|
409
|
+
delete this.validationMeta.validationPromise
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Always return the latest validation promise to the caller
|
|
414
|
+
return (await this.validationMeta.validationPromise) ?? []
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
validate = (
|
|
418
|
+
cause: ValidationCause,
|
|
419
|
+
): ValidationError[] | Promise<ValidationError[]> => {
|
|
420
|
+
// Store the previous error for the errorMapKey (eg. onChange, onBlur, onSubmit)
|
|
421
|
+
const errorMapKey = getErrorMapKey(cause)
|
|
422
|
+
const prevError = this.state.errorMap[errorMapKey]
|
|
423
|
+
|
|
424
|
+
// Attempt to sync validate first
|
|
425
|
+
this.validateSync(cause)
|
|
426
|
+
|
|
427
|
+
const newError = this.state.errorMap[errorMapKey]
|
|
428
|
+
if (
|
|
429
|
+
prevError !== newError &&
|
|
430
|
+
!this.options.asyncAlways &&
|
|
431
|
+
!(newError === undefined && prevError !== undefined)
|
|
432
|
+
)
|
|
433
|
+
return this.state.errors
|
|
434
|
+
|
|
435
|
+
// No error? Attempt async validation
|
|
436
|
+
return this.validateAsync(cause)
|
|
437
|
+
}
|
|
438
|
+
|
|
248
439
|
handleSubmit = async () => {
|
|
249
440
|
// Check to see that the form and all fields have been touched
|
|
250
441
|
// If they have not, touch them all and run validation
|
|
@@ -279,7 +470,7 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
279
470
|
}
|
|
280
471
|
|
|
281
472
|
// Run validation for the form
|
|
282
|
-
|
|
473
|
+
await this.validate('submit')
|
|
283
474
|
|
|
284
475
|
if (!this.state.isValid) {
|
|
285
476
|
done()
|
|
@@ -428,3 +619,28 @@ export class FormApi<TFormData, ValidatorType> {
|
|
|
428
619
|
})
|
|
429
620
|
}
|
|
430
621
|
}
|
|
622
|
+
|
|
623
|
+
function normalizeError(rawError?: ValidationError) {
|
|
624
|
+
if (rawError) {
|
|
625
|
+
if (typeof rawError !== 'string') {
|
|
626
|
+
return 'Invalid Form Values'
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return rawError
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return undefined
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function getErrorMapKey(cause: ValidationCause) {
|
|
636
|
+
switch (cause) {
|
|
637
|
+
case 'submit':
|
|
638
|
+
return 'onSubmit'
|
|
639
|
+
case 'change':
|
|
640
|
+
return 'onChange'
|
|
641
|
+
case 'blur':
|
|
642
|
+
return 'onBlur'
|
|
643
|
+
case 'mount':
|
|
644
|
+
return 'onMount'
|
|
645
|
+
}
|
|
646
|
+
}
|
|
@@ -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
|
})
|