feat(form-core): Change from touched error message and error message to error map and array of errors (#442)

* feature(FieldAPI): Change from touched error message and  error message to error map and array of errors

BREAKING CHANGE: The touched Error and error field has been removed will be replaced with touched errors array and errors map.

* feat: update documentation for updated fields

* chore: update Vue adapter as well

* fix: update getErrorMapKey to return onChange when change is the validation cause

* chore: remove console.log

---------

Co-authored-by: Corbin Crutchley <git@crutchcorn.dev>
This commit is contained in:
Ray Liu
2023-09-08 14:14:54 -04:00
committed by GitHub
parent 05aedcea20
commit c2f9957046
10 changed files with 289 additions and 68 deletions

View File

@@ -83,7 +83,7 @@ An object type representing the options for a field in a form.
A type representing the cause of a validation event. A type representing the cause of a validation event.
- 'change' | 'blur' | 'submit' - 'change' | 'blur' | 'submit' | 'mount'
### `FieldMeta` ### `FieldMeta`
@@ -94,13 +94,17 @@ An object type representing the metadata of a field in a form.
``` ```
- A flag indicating whether the field has been touched. - A flag indicating whether the field has been touched.
- ```tsx - ```tsx
touchedError?: ValidationError touchedErrors: ValidationError[]
``` ```
- An optional error related to the touched state of the field. - An array of errors related to the touched state of the field.
- ```tsx - ```tsx
error?: ValidationError errors: ValidationError[]
``` ```
- An optional error related to the field value. - An array of errors related related to the field value.
- ```tsx
errorMap: ValidationErrorMap
```
- A map of errors related related to the field value.
- ```tsx - ```tsx
isValidating: boolean isValidating: boolean
``` ```

View File

@@ -262,3 +262,11 @@ An object representing the validation metadata for a field.
### `ValidationError` ### `ValidationError`
A type representing a validation error. Possible values are `undefined`, `false`, `null`, or a `string` with an error message. A type representing a validation error. Possible values are `undefined`, `false`, `null`, or a `string` with an error message.
### `ValidationErrorMapKeys`
A type representing the keys used to map to `ValidationError` in `ValidationErrorMap`. It is defined with `on${Capitalize<ValidationCause>}`
### `ValidationErrorMap`
A type that represents a map with the keys as `ValidationErrorMapKeys` and the values as `ValidationError`

View File

@@ -92,7 +92,7 @@
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"solid-js": "^1.6.13", "solid-js": "^1.6.13",
"stream-to-array": "^2.3.0", "stream-to-array": "^2.3.0",
"tsup": "^7.1.0", "tsup": "^7.2.0",
"type-fest": "^3.11.0", "type-fest": "^3.11.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vitest": "^0.34.3", "vitest": "^0.34.3",

View File

@@ -1,8 +1,8 @@
import type { DeepKeys, DeepValue, Updater } from './utils' import { type DeepKeys, type DeepValue, type Updater } from './utils'
import type { FormApi, ValidationError } from './FormApi' import type { FormApi, ValidationError, ValidationErrorMap } from './FormApi'
import { Store } from '@tanstack/store' import { Store } from '@tanstack/store'
export type ValidationCause = 'change' | 'blur' | 'submit' export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
type ValidateFn<TData, TFormData> = ( type ValidateFn<TData, TFormData> = (
value: TData, value: TData,
@@ -52,8 +52,9 @@ export type FieldApiOptions<TData, TFormData> = FieldOptions<
export type FieldMeta = { export type FieldMeta = {
isTouched: boolean isTouched: boolean
touchedError?: ValidationError touchedErrors: ValidationError[]
error?: ValidationError errors: ValidationError[]
errorMap: ValidationErrorMap
isValidating: boolean isValidating: boolean
} }
@@ -110,6 +111,9 @@ export class FieldApi<TData, TFormData> {
meta: this._getMeta() ?? { meta: this._getMeta() ?? {
isValidating: false, isValidating: false,
isTouched: false, isTouched: false,
touchedErrors: [],
errors: [],
errorMap: {},
...opts.defaultMeta, ...opts.defaultMeta,
}, },
}, },
@@ -117,9 +121,9 @@ export class FieldApi<TData, TFormData> {
onUpdate: () => { onUpdate: () => {
const state = this.store.state const state = this.store.state
state.meta.touchedError = state.meta.isTouched state.meta.touchedErrors = state.meta.isTouched
? state.meta.error ? state.meta.errors
: undefined : []
this.prevState = state this.prevState = state
this.state = state this.state = state
@@ -203,6 +207,9 @@ export class FieldApi<TData, TFormData> {
({ ({
isValidating: false, isValidating: false,
isTouched: false, isTouched: false,
touchedErrors: [],
errors: [],
errorMap: {},
...this.options.defaultMeta, ...this.options.defaultMeta,
} as FieldMeta) } as FieldMeta)
@@ -239,7 +246,6 @@ export class FieldApi<TData, TFormData> {
const { onChange, onBlur } = this.options const { onChange, onBlur } = this.options
const validate = const validate =
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
if (!validate) return if (!validate) return
// Use the validationCount for all field instances to // Use the validationCount for all field instances to
@@ -247,16 +253,20 @@ export class FieldApi<TData, TFormData> {
const validationCount = (this.getInfo().validationCount || 0) + 1 const validationCount = (this.getInfo().validationCount || 0) + 1
this.getInfo().validationCount = validationCount this.getInfo().validationCount = validationCount
const error = normalizeError(validate(value as never, this as never)) const error = normalizeError(validate(value as never, this as never))
const errorMapKey = getErrorMapKey(cause)
if (this.state.meta.error !== error) { if (error && this.state.meta.errorMap[errorMapKey] !== error) {
this.setMeta((prev) => ({ this.setMeta((prev) => ({
...prev, ...prev,
error, errors: [...prev.errors, error],
errorMap: {
...prev.errorMap,
[getErrorMapKey(cause)]: error,
},
})) }))
} }
// If a sync error is encountered, cancel any async validation // If a sync error is encountered for the errorMapKey (eg. onChange), cancel any async validation
if (this.state.meta.error) { if (this.state.meta.errorMap[errorMapKey]) {
this.cancelValidateAsync() this.cancelValidateAsync()
} }
} }
@@ -293,9 +303,7 @@ export class FieldApi<TData, TFormData> {
: cause === 'submit' : cause === 'submit'
? onSubmitAsync ? onSubmitAsync
: onBlurAsync : onBlurAsync
if (!validate) return []
if (!validate) return
const debounceMs = const debounceMs =
cause === 'submit' cause === 'submit'
? 0 ? 0
@@ -328,21 +336,25 @@ export class FieldApi<TData, TFormData> {
// Only kick off validation if this validation is the latest attempt // Only kick off validation if this validation is the latest attempt
if (checkLatest()) { if (checkLatest()) {
const prevErrors = this.getMeta().errors
try { try {
const rawError = await validate(value as never, this as never) const rawError = await validate(value as never, this as never)
if (checkLatest()) { if (checkLatest()) {
const error = normalizeError(rawError) const error = normalizeError(rawError)
this.setMeta((prev) => ({ this.setMeta((prev) => ({
...prev, ...prev,
isValidating: false, isValidating: false,
error, errors: [...prev.errors, error],
errorMap: {
...prev.errorMap,
[getErrorMapKey(cause)]: error,
},
})) }))
this.getInfo().validationResolve?.(error) this.getInfo().validationResolve?.([...prevErrors, error])
} }
} catch (error) { } catch (error) {
if (checkLatest()) { if (checkLatest()) {
this.getInfo().validationReject?.(error) this.getInfo().validationReject?.([...prevErrors, error])
throw error throw error
} }
} finally { } finally {
@@ -354,26 +366,25 @@ export class FieldApi<TData, TFormData> {
} }
// Always return the latest validation promise to the caller // Always return the latest validation promise to the caller
return this.getInfo().validationPromise return this.getInfo().validationPromise ?? []
} }
validate = ( validate = (
cause: ValidationCause, cause: ValidationCause,
value?: typeof this._tdata, value?: typeof this._tdata,
): ValidationError | Promise<ValidationError> => { ): ValidationError[] | Promise<ValidationError[]> => {
// If the field is pristine and validatePristine is false, do not validate // If the field is pristine and validatePristine is false, do not validate
if (!this.state.meta.isTouched) return if (!this.state.meta.isTouched) return []
// Attempt to sync validate first // Attempt to sync validate first
this.validateSync(value, cause) this.validateSync(value, cause)
// If there is an error, return it, do not attempt async validation const errorMapKey = getErrorMapKey(cause)
if (this.state.meta.error) { // If there is an error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation
if (this.getMeta().errorMap[errorMapKey]) {
if (!this.options.asyncAlways) { if (!this.options.asyncAlways) {
return this.state.meta.error return this.state.meta.errors
} }
} }
// No error? Attempt async validation // No error? Attempt async validation
return this.validateAsync(value, cause) return this.validateAsync(value, cause)
} }
@@ -403,3 +414,16 @@ function normalizeError(rawError?: ValidationError) {
return undefined return undefined
} }
function getErrorMapKey(cause: ValidationCause) {
switch (cause) {
case 'submit':
return 'onSubmit'
case 'change':
return 'onChange'
case 'blur':
return 'onBlur'
case 'mount':
return 'onMount'
}
}

View File

@@ -1,7 +1,7 @@
import { Store } from '@tanstack/store' import { Store } from '@tanstack/store'
// //
import type { DeepKeys, DeepValue, Updater } from './utils' import type { DeepKeys, DeepValue, Updater } from './utils'
import { functionalUpdate, getBy, setBy } from './utils' import { functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils'
import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi' import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi'
export type FormOptions<TData> = { export type FormOptions<TData> = {
@@ -37,13 +37,19 @@ export type FieldInfo<TFormData> = {
export type ValidationMeta = { export type ValidationMeta = {
validationCount?: number validationCount?: number
validationAsyncCount?: number validationAsyncCount?: number
validationPromise?: Promise<ValidationError> validationPromise?: Promise<ValidationError[]>
validationResolve?: (error: ValidationError) => void validationResolve?: (errors: ValidationError[]) => void
validationReject?: (error: unknown) => void validationReject?: (errors: unknown) => void
} }
export type ValidationError = undefined | false | null | string export type ValidationError = undefined | false | null | string
export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
export type ValidationErrorMap = {
[K in ValidationErrorMapKeys]?: ValidationError
}
export type FormState<TData> = { export type FormState<TData> = {
values: TData values: TData
// Form Validation // Form Validation
@@ -117,7 +123,9 @@ export class FormApi<TFormData> {
(field) => field?.isValidating, (field) => field?.isValidating,
) )
const isFieldsValid = !fieldMetaValues.some((field) => field?.error) const isFieldsValid = !fieldMetaValues.some((field) =>
isNonEmptyArray(field?.errors),
)
const isTouched = fieldMetaValues.some((field) => field?.isTouched) const isTouched = fieldMetaValues.some((field) => field?.isTouched)
@@ -192,8 +200,7 @@ export class FormApi<TFormData> {
) )
validateAllFields = async (cause: ValidationCause) => { validateAllFields = async (cause: ValidationCause) => {
const fieldValidationPromises: Promise<ValidationError>[] = [] as any const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
this.store.batch(() => { this.store.batch(() => {
void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach( void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach(
(field) => { (field) => {

View File

@@ -30,6 +30,9 @@ describe('field api', () => {
expect(field.getMeta()).toEqual({ expect(field.getMeta()).toEqual({
isTouched: false, isTouched: false,
isValidating: false, isValidating: false,
touchedErrors: [],
errors: [],
errorMap: {},
}) })
}) })
@@ -44,6 +47,9 @@ describe('field api', () => {
expect(field.getMeta()).toEqual({ expect(field.getMeta()).toEqual({
isTouched: true, isTouched: true,
isValidating: false, isValidating: false,
touchedErrors: [],
errors: [],
errorMap: {},
}) })
}) })
@@ -193,9 +199,12 @@ describe('field api', () => {
field.mount() field.mount()
expect(field.getMeta().error).toBeUndefined() expect(field.getMeta().errors.length).toBe(0)
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onChange: 'Please enter a different value',
})
}) })
it('should run async validation onChange', async () => { it('should run async validation onChange', async () => {
@@ -219,10 +228,13 @@ describe('field api', () => {
field.mount() field.mount()
expect(field.getMeta().error).toBeUndefined() expect(field.getMeta().errors.length).toBe(0)
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
await vi.runAllTimersAsync() await vi.runAllTimersAsync()
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onChange: 'Please enter a different value',
})
}) })
it('should run async validation onChange with debounce', async () => { it('should run async validation onChange with debounce', async () => {
@@ -248,13 +260,16 @@ describe('field api', () => {
field.mount() field.mount()
expect(field.getMeta().error).toBeUndefined() expect(field.getMeta().errors.length).toBe(0)
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
field.setValue('other') field.setValue('other')
await vi.runAllTimersAsync() await vi.runAllTimersAsync()
// sleepMock will have been called 2 times without onChangeAsyncDebounceMs // sleepMock will have been called 2 times without onChangeAsyncDebounceMs
expect(sleepMock).toHaveBeenCalledTimes(1) expect(sleepMock).toHaveBeenCalledTimes(1)
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onChange: 'Please enter a different value',
})
}) })
it('should run async validation onChange with asyncDebounceMs', async () => { it('should run async validation onChange with asyncDebounceMs', async () => {
@@ -280,13 +295,16 @@ describe('field api', () => {
field.mount() field.mount()
expect(field.getMeta().error).toBeUndefined() expect(field.getMeta().errors.length).toBe(0)
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
field.setValue('other') field.setValue('other')
await vi.runAllTimersAsync() await vi.runAllTimersAsync()
// sleepMock will have been called 2 times without asyncDebounceMs // sleepMock will have been called 2 times without asyncDebounceMs
expect(sleepMock).toHaveBeenCalledTimes(1) expect(sleepMock).toHaveBeenCalledTimes(1)
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onChange: 'Please enter a different value',
})
}) })
it('should run validation onBlur', () => { it('should run validation onBlur', () => {
@@ -309,7 +327,10 @@ describe('field api', () => {
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
field.validate('blur') field.validate('blur')
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onBlur: 'Please enter a different value',
})
}) })
it('should run async validation onBlur', async () => { it('should run async validation onBlur', async () => {
@@ -333,11 +354,14 @@ describe('field api', () => {
field.mount() field.mount()
expect(field.getMeta().error).toBeUndefined() expect(field.getMeta().errors.length).toBe(0)
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
field.validate('blur') field.validate('blur')
await vi.runAllTimersAsync() await vi.runAllTimersAsync()
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onBlur: 'Please enter a different value',
})
}) })
it('should run async validation onBlur with debounce', async () => { it('should run async validation onBlur with debounce', async () => {
@@ -363,14 +387,17 @@ describe('field api', () => {
field.mount() field.mount()
expect(field.getMeta().error).toBeUndefined() expect(field.getMeta().errors.length).toBe(0)
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
field.validate('blur') field.validate('blur')
field.validate('blur') field.validate('blur')
await vi.runAllTimersAsync() await vi.runAllTimersAsync()
// sleepMock will have been called 2 times without onBlurAsyncDebounceMs // sleepMock will have been called 2 times without onBlurAsyncDebounceMs
expect(sleepMock).toHaveBeenCalledTimes(1) expect(sleepMock).toHaveBeenCalledTimes(1)
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onBlur: 'Please enter a different value',
})
}) })
it('should run async validation onBlur with asyncDebounceMs', async () => { it('should run async validation onBlur with asyncDebounceMs', async () => {
@@ -396,14 +423,17 @@ describe('field api', () => {
field.mount() field.mount()
expect(field.getMeta().error).toBeUndefined() expect(field.getMeta().errors.length).toBe(0)
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
field.validate('blur') field.validate('blur')
field.validate('blur') field.validate('blur')
await vi.runAllTimersAsync() await vi.runAllTimersAsync()
// sleepMock will have been called 2 times without asyncDebounceMs // sleepMock will have been called 2 times without asyncDebounceMs
expect(sleepMock).toHaveBeenCalledTimes(1) expect(sleepMock).toHaveBeenCalledTimes(1)
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onBlur: 'Please enter a different value',
})
}) })
it('should run async validation onSubmit', async () => { it('should run async validation onSubmit', async () => {
@@ -427,11 +457,48 @@ describe('field api', () => {
field.mount() field.mount()
expect(field.getMeta().error).toBeUndefined() expect(field.getMeta().errors.length).toBe(0)
field.setValue('other', { touch: true }) field.setValue('other', { touch: true })
field.validate('submit') field.validate('submit')
await vi.runAllTimersAsync() await vi.runAllTimersAsync()
expect(field.getMeta().error).toBe('Please enter a different value') expect(field.getMeta().errors).toContain('Please enter a different value')
expect(field.getMeta().errorMap).toMatchObject({
onSubmit: 'Please enter a different value',
})
})
it('should contain multiple errors when running validation onBlur and onChange', () => {
const form = new FormApi({
defaultValues: {
name: 'other',
},
})
const field = new FieldApi({
form,
name: 'name',
onBlur: (value) => {
if (value === 'other') return 'Please enter a different value'
return
},
onChange: (value) => {
if (value === 'other') return 'Please enter a different value'
return
},
})
field.mount()
field.setValue('other', { touch: true })
field.validate('blur')
expect(field.getMeta().errors).toStrictEqual([
'Please enter a different value',
'Please enter a different value',
])
expect(field.getMeta().errorMap).toEqual({
onBlur: 'Please enter a different value',
onChange: 'Please enter a different value',
})
}) })
it('should handle default value on field using state.value', async () => { it('should handle default value on field using state.value', async () => {

View File

@@ -101,6 +101,10 @@ function makePathArray(str: string) {
}) })
} }
export function isNonEmptyArray(obj: any) {
return !(Array.isArray(obj) && obj.length === 0)
}
export type RequiredByKey<T, K extends keyof T> = Omit<T, K> & export type RequiredByKey<T, K extends keyof T> = Omit<T, K> &
Required<Pick<T, K>> Required<Pick<T, K>>

View File

@@ -71,7 +71,7 @@ describe('useField', () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.setValue(e.target.value)} onChange={(e) => field.setValue(e.target.value)}
/> />
<p>{field.getMeta().error}</p> <p>{field.getMeta().errors}</p>
</div> </div>
)} )}
/> />
@@ -112,7 +112,7 @@ describe('useField', () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
<p>{field.getMeta().error}</p> <p>{field.getMeta().errorMap?.onChange}</p>
</div> </div>
)} )}
/> />
@@ -127,6 +127,56 @@ describe('useField', () => {
expect(getByText(error)).toBeInTheDocument() expect(getByText(error)).toBeInTheDocument()
}) })
it('should validate on change and on blur', async () => {
type Person = {
firstName: string
lastName: string
}
const onChangeError = 'Please enter a different value (onChangeError)'
const onBlurError = 'Please enter a different value (onBlurError)'
const formFactory = createFormFactory<Person>()
function Comp() {
const form = formFactory.useForm()
return (
<form.Provider>
<form.Field
name="firstName"
defaultMeta={{ isTouched: true }}
onChange={(value) =>
value === 'other' ? onChangeError : undefined
}
onBlur={(value) => (value === 'other' ? onBlurError : undefined)}
children={(field) => (
<div>
<input
data-testid="fieldinput"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<p>{field.getMeta().errorMap?.onChange}</p>
<p>{field.getMeta().errorMap?.onBlur}</p>
</div>
)}
/>
</form.Provider>
)
}
const { getByTestId, getByText, queryByText } = render(<Comp />)
const input = getByTestId('fieldinput')
expect(queryByText(onChangeError)).not.toBeInTheDocument()
expect(queryByText(onBlurError)).not.toBeInTheDocument()
await user.type(input, 'other')
expect(getByText(onChangeError)).toBeInTheDocument()
await user.click(document.body)
expect(queryByText(onBlurError)).toBeInTheDocument()
})
it('should validate async on change', async () => { it('should validate async on change', async () => {
type Person = { type Person = {
firstName: string firstName: string
@@ -157,7 +207,7 @@ describe('useField', () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
<p>{field.getMeta().error}</p> <p>{field.getMeta().errorMap?.onChange}</p>
</div> </div>
)} )}
/> />
@@ -173,6 +223,63 @@ describe('useField', () => {
expect(getByText(error)).toBeInTheDocument() expect(getByText(error)).toBeInTheDocument()
}) })
it('should validate async on change and async on blur', async () => {
type Person = {
firstName: string
lastName: string
}
const onChangeError = 'Please enter a different value (onChangeError)'
const onBlurError = 'Please enter a different value (onBlurError)'
const formFactory = createFormFactory<Person>()
function Comp() {
const form = formFactory.useForm()
return (
<form.Provider>
<form.Field
name="firstName"
defaultMeta={{ isTouched: true }}
onChangeAsync={async () => {
await sleep(10)
return onChangeError
}}
onBlurAsync={async () => {
await sleep(10)
return onBlurError
}}
children={(field) => (
<div>
<input
data-testid="fieldinput"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<p>{field.getMeta().errorMap?.onChange}</p>
<p>{field.getMeta().errorMap?.onBlur}</p>
</div>
)}
/>
</form.Provider>
)
}
const { getByTestId, getByText, queryByText } = render(<Comp />)
const input = getByTestId('fieldinput')
expect(queryByText(onChangeError)).not.toBeInTheDocument()
expect(queryByText(onBlurError)).not.toBeInTheDocument()
await user.type(input, 'other')
await waitFor(() => getByText(onChangeError))
expect(getByText(onChangeError)).toBeInTheDocument()
await user.click(document.body)
await waitFor(() => getByText(onBlurError))
expect(getByText(onBlurError)).toBeInTheDocument()
})
it('should validate async on change with debounce', async () => { it('should validate async on change with debounce', async () => {
type Person = { type Person = {
firstName: string firstName: string
@@ -205,7 +312,7 @@ describe('useField', () => {
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
/> />
<p>{field.getMeta().error}</p> <p>{field.getMeta().errors}</p>
</div> </div>
)} )}
/> />

View File

@@ -79,7 +79,7 @@ describe('useField', () => {
field.setValue((e.target as HTMLInputElement).value) field.setValue((e.target as HTMLInputElement).value)
} }
/> />
<p>{field.getMeta().error}</p> <p>{field.getMeta().errors}</p>
</div> </div>
)} )}
</form.Field> </form.Field>
@@ -122,7 +122,7 @@ describe('useField', () => {
field.handleChange((e.target as HTMLInputElement).value) field.handleChange((e.target as HTMLInputElement).value)
} }
/> />
<p>{field.getMeta().error}</p> <p>{field.getMeta().errors}</p>
</div> </div>
)} )}
</form.Field> </form.Field>
@@ -170,7 +170,7 @@ describe('useField', () => {
field.handleChange((e.target as HTMLInputElement).value) field.handleChange((e.target as HTMLInputElement).value)
} }
/> />
<p>{field.getMeta().error}</p> <p>{field.getMeta().errors}</p>
</div> </div>
)} )}
</form.Field> </form.Field>
@@ -222,7 +222,7 @@ describe('useField', () => {
field.handleChange((e.target as HTMLInputElement).value) field.handleChange((e.target as HTMLInputElement).value)
} }
/> />
<p>{field.getMeta().error}</p> <p>{field.getMeta().errors}</p>
</div> </div>
)} )}
</form.Field> </form.Field>

2
pnpm-lock.yaml generated
View File

@@ -197,7 +197,7 @@ importers:
specifier: ^2.3.0 specifier: ^2.3.0
version: 2.3.0 version: 2.3.0
tsup: tsup:
specifier: ^7.1.0 specifier: ^7.2.0
version: 7.2.0(typescript@5.2.2) version: 7.2.0(typescript@5.2.2)
type-fest: type-fest:
specifier: ^3.11.0 specifier: ^3.11.0