mirror of
https://github.com/LukeHagar/form.git
synced 2025-12-06 04:19:43 +00:00
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:
@@ -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.
|
||||
|
||||
- 'change' | 'blur' | 'submit'
|
||||
- 'change' | 'blur' | 'submit' | 'mount'
|
||||
|
||||
### `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.
|
||||
- ```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
|
||||
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
|
||||
isValidating: boolean
|
||||
```
|
||||
|
||||
@@ -262,3 +262,11 @@ An object representing the validation metadata for a field.
|
||||
### `ValidationError`
|
||||
|
||||
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`
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"solid-js": "^1.6.13",
|
||||
"stream-to-array": "^2.3.0",
|
||||
"tsup": "^7.1.0",
|
||||
"tsup": "^7.2.0",
|
||||
"type-fest": "^3.11.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vitest": "^0.34.3",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { DeepKeys, DeepValue, Updater } from './utils'
|
||||
import type { FormApi, ValidationError } from './FormApi'
|
||||
import { type DeepKeys, type DeepValue, type Updater } from './utils'
|
||||
import type { FormApi, ValidationError, ValidationErrorMap } from './FormApi'
|
||||
import { Store } from '@tanstack/store'
|
||||
|
||||
export type ValidationCause = 'change' | 'blur' | 'submit'
|
||||
export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
|
||||
|
||||
type ValidateFn<TData, TFormData> = (
|
||||
value: TData,
|
||||
@@ -52,8 +52,9 @@ export type FieldApiOptions<TData, TFormData> = FieldOptions<
|
||||
|
||||
export type FieldMeta = {
|
||||
isTouched: boolean
|
||||
touchedError?: ValidationError
|
||||
error?: ValidationError
|
||||
touchedErrors: ValidationError[]
|
||||
errors: ValidationError[]
|
||||
errorMap: ValidationErrorMap
|
||||
isValidating: boolean
|
||||
}
|
||||
|
||||
@@ -110,6 +111,9 @@ export class FieldApi<TData, TFormData> {
|
||||
meta: this._getMeta() ?? {
|
||||
isValidating: false,
|
||||
isTouched: false,
|
||||
touchedErrors: [],
|
||||
errors: [],
|
||||
errorMap: {},
|
||||
...opts.defaultMeta,
|
||||
},
|
||||
},
|
||||
@@ -117,9 +121,9 @@ export class FieldApi<TData, TFormData> {
|
||||
onUpdate: () => {
|
||||
const state = this.store.state
|
||||
|
||||
state.meta.touchedError = state.meta.isTouched
|
||||
? state.meta.error
|
||||
: undefined
|
||||
state.meta.touchedErrors = state.meta.isTouched
|
||||
? state.meta.errors
|
||||
: []
|
||||
|
||||
this.prevState = state
|
||||
this.state = state
|
||||
@@ -203,6 +207,9 @@ export class FieldApi<TData, TFormData> {
|
||||
({
|
||||
isValidating: false,
|
||||
isTouched: false,
|
||||
touchedErrors: [],
|
||||
errors: [],
|
||||
errorMap: {},
|
||||
...this.options.defaultMeta,
|
||||
} as FieldMeta)
|
||||
|
||||
@@ -239,7 +246,6 @@ export class FieldApi<TData, TFormData> {
|
||||
const { onChange, onBlur } = this.options
|
||||
const validate =
|
||||
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
|
||||
|
||||
if (!validate) return
|
||||
|
||||
// Use the validationCount for all field instances to
|
||||
@@ -247,16 +253,20 @@ export class FieldApi<TData, TFormData> {
|
||||
const validationCount = (this.getInfo().validationCount || 0) + 1
|
||||
this.getInfo().validationCount = validationCount
|
||||
const error = normalizeError(validate(value as never, this as never))
|
||||
|
||||
if (this.state.meta.error !== error) {
|
||||
const errorMapKey = getErrorMapKey(cause)
|
||||
if (error && this.state.meta.errorMap[errorMapKey] !== error) {
|
||||
this.setMeta((prev) => ({
|
||||
...prev,
|
||||
error,
|
||||
errors: [...prev.errors, error],
|
||||
errorMap: {
|
||||
...prev.errorMap,
|
||||
[getErrorMapKey(cause)]: error,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// If a sync error is encountered, cancel any async validation
|
||||
if (this.state.meta.error) {
|
||||
// If a sync error is encountered for the errorMapKey (eg. onChange), cancel any async validation
|
||||
if (this.state.meta.errorMap[errorMapKey]) {
|
||||
this.cancelValidateAsync()
|
||||
}
|
||||
}
|
||||
@@ -293,9 +303,7 @@ export class FieldApi<TData, TFormData> {
|
||||
: cause === 'submit'
|
||||
? onSubmitAsync
|
||||
: onBlurAsync
|
||||
|
||||
if (!validate) return
|
||||
|
||||
if (!validate) return []
|
||||
const debounceMs =
|
||||
cause === 'submit'
|
||||
? 0
|
||||
@@ -328,21 +336,25 @@ export class FieldApi<TData, TFormData> {
|
||||
|
||||
// Only kick off validation if this validation is the latest attempt
|
||||
if (checkLatest()) {
|
||||
const prevErrors = this.getMeta().errors
|
||||
try {
|
||||
const rawError = await validate(value as never, this as never)
|
||||
|
||||
if (checkLatest()) {
|
||||
const error = normalizeError(rawError)
|
||||
this.setMeta((prev) => ({
|
||||
...prev,
|
||||
isValidating: false,
|
||||
error,
|
||||
errors: [...prev.errors, error],
|
||||
errorMap: {
|
||||
...prev.errorMap,
|
||||
[getErrorMapKey(cause)]: error,
|
||||
},
|
||||
}))
|
||||
this.getInfo().validationResolve?.(error)
|
||||
this.getInfo().validationResolve?.([...prevErrors, error])
|
||||
}
|
||||
} catch (error) {
|
||||
if (checkLatest()) {
|
||||
this.getInfo().validationReject?.(error)
|
||||
this.getInfo().validationReject?.([...prevErrors, error])
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
@@ -354,26 +366,25 @@ export class FieldApi<TData, TFormData> {
|
||||
}
|
||||
|
||||
// Always return the latest validation promise to the caller
|
||||
return this.getInfo().validationPromise
|
||||
return this.getInfo().validationPromise ?? []
|
||||
}
|
||||
|
||||
validate = (
|
||||
cause: ValidationCause,
|
||||
value?: typeof this._tdata,
|
||||
): ValidationError | Promise<ValidationError> => {
|
||||
): ValidationError[] | Promise<ValidationError[]> => {
|
||||
// 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
|
||||
this.validateSync(value, cause)
|
||||
|
||||
// If there is an error, return it, do not attempt async validation
|
||||
if (this.state.meta.error) {
|
||||
const errorMapKey = getErrorMapKey(cause)
|
||||
// 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) {
|
||||
return this.state.meta.error
|
||||
return this.state.meta.errors
|
||||
}
|
||||
}
|
||||
|
||||
// No error? Attempt async validation
|
||||
return this.validateAsync(value, cause)
|
||||
}
|
||||
@@ -403,3 +414,16 @@ function normalizeError(rawError?: ValidationError) {
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getErrorMapKey(cause: ValidationCause) {
|
||||
switch (cause) {
|
||||
case 'submit':
|
||||
return 'onSubmit'
|
||||
case 'change':
|
||||
return 'onChange'
|
||||
case 'blur':
|
||||
return 'onBlur'
|
||||
case 'mount':
|
||||
return 'onMount'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Store } from '@tanstack/store'
|
||||
//
|
||||
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'
|
||||
|
||||
export type FormOptions<TData> = {
|
||||
@@ -37,13 +37,19 @@ export type FieldInfo<TFormData> = {
|
||||
export type ValidationMeta = {
|
||||
validationCount?: number
|
||||
validationAsyncCount?: number
|
||||
validationPromise?: Promise<ValidationError>
|
||||
validationResolve?: (error: ValidationError) => void
|
||||
validationReject?: (error: unknown) => void
|
||||
validationPromise?: Promise<ValidationError[]>
|
||||
validationResolve?: (errors: ValidationError[]) => void
|
||||
validationReject?: (errors: unknown) => void
|
||||
}
|
||||
|
||||
export type ValidationError = undefined | false | null | string
|
||||
|
||||
export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
|
||||
|
||||
export type ValidationErrorMap = {
|
||||
[K in ValidationErrorMapKeys]?: ValidationError
|
||||
}
|
||||
|
||||
export type FormState<TData> = {
|
||||
values: TData
|
||||
// Form Validation
|
||||
@@ -117,7 +123,9 @@ export class FormApi<TFormData> {
|
||||
(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)
|
||||
|
||||
@@ -192,8 +200,7 @@ export class FormApi<TFormData> {
|
||||
)
|
||||
|
||||
validateAllFields = async (cause: ValidationCause) => {
|
||||
const fieldValidationPromises: Promise<ValidationError>[] = [] as any
|
||||
|
||||
const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
|
||||
this.store.batch(() => {
|
||||
void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach(
|
||||
(field) => {
|
||||
|
||||
@@ -30,6 +30,9 @@ describe('field api', () => {
|
||||
expect(field.getMeta()).toEqual({
|
||||
isTouched: false,
|
||||
isValidating: false,
|
||||
touchedErrors: [],
|
||||
errors: [],
|
||||
errorMap: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,6 +47,9 @@ describe('field api', () => {
|
||||
expect(field.getMeta()).toEqual({
|
||||
isTouched: true,
|
||||
isValidating: false,
|
||||
touchedErrors: [],
|
||||
errors: [],
|
||||
errorMap: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -193,9 +199,12 @@ describe('field api', () => {
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().error).toBeUndefined()
|
||||
expect(field.getMeta().errors.length).toBe(0)
|
||||
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 () => {
|
||||
@@ -219,10 +228,13 @@ describe('field api', () => {
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().error).toBeUndefined()
|
||||
expect(field.getMeta().errors.length).toBe(0)
|
||||
field.setValue('other', { touch: true })
|
||||
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 () => {
|
||||
@@ -248,13 +260,16 @@ describe('field api', () => {
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().error).toBeUndefined()
|
||||
expect(field.getMeta().errors.length).toBe(0)
|
||||
field.setValue('other', { touch: true })
|
||||
field.setValue('other')
|
||||
await vi.runAllTimersAsync()
|
||||
// sleepMock will have been called 2 times without onChangeAsyncDebounceMs
|
||||
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 () => {
|
||||
@@ -280,13 +295,16 @@ describe('field api', () => {
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().error).toBeUndefined()
|
||||
expect(field.getMeta().errors.length).toBe(0)
|
||||
field.setValue('other', { touch: true })
|
||||
field.setValue('other')
|
||||
await vi.runAllTimersAsync()
|
||||
// sleepMock will have been called 2 times without asyncDebounceMs
|
||||
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', () => {
|
||||
@@ -309,7 +327,10 @@ describe('field api', () => {
|
||||
|
||||
field.setValue('other', { touch: true })
|
||||
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 () => {
|
||||
@@ -333,11 +354,14 @@ describe('field api', () => {
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().error).toBeUndefined()
|
||||
expect(field.getMeta().errors.length).toBe(0)
|
||||
field.setValue('other', { touch: true })
|
||||
field.validate('blur')
|
||||
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 () => {
|
||||
@@ -363,14 +387,17 @@ describe('field api', () => {
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().error).toBeUndefined()
|
||||
expect(field.getMeta().errors.length).toBe(0)
|
||||
field.setValue('other', { touch: true })
|
||||
field.validate('blur')
|
||||
field.validate('blur')
|
||||
await vi.runAllTimersAsync()
|
||||
// sleepMock will have been called 2 times without onBlurAsyncDebounceMs
|
||||
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 () => {
|
||||
@@ -396,14 +423,17 @@ describe('field api', () => {
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().error).toBeUndefined()
|
||||
expect(field.getMeta().errors.length).toBe(0)
|
||||
field.setValue('other', { touch: true })
|
||||
field.validate('blur')
|
||||
field.validate('blur')
|
||||
await vi.runAllTimersAsync()
|
||||
// sleepMock will have been called 2 times without asyncDebounceMs
|
||||
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 () => {
|
||||
@@ -427,11 +457,48 @@ describe('field api', () => {
|
||||
|
||||
field.mount()
|
||||
|
||||
expect(field.getMeta().error).toBeUndefined()
|
||||
expect(field.getMeta().errors.length).toBe(0)
|
||||
field.setValue('other', { touch: true })
|
||||
field.validate('submit')
|
||||
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 () => {
|
||||
|
||||
@@ -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> &
|
||||
Required<Pick<T, K>>
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('useField', () => {
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.setValue(e.target.value)}
|
||||
/>
|
||||
<p>{field.getMeta().error}</p>
|
||||
<p>{field.getMeta().errors}</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@@ -112,7 +112,7 @@ describe('useField', () => {
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
<p>{field.getMeta().error}</p>
|
||||
<p>{field.getMeta().errorMap?.onChange}</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@@ -127,6 +127,56 @@ describe('useField', () => {
|
||||
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 () => {
|
||||
type Person = {
|
||||
firstName: string
|
||||
@@ -157,7 +207,7 @@ describe('useField', () => {
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
<p>{field.getMeta().error}</p>
|
||||
<p>{field.getMeta().errorMap?.onChange}</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@@ -173,6 +223,63 @@ describe('useField', () => {
|
||||
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 () => {
|
||||
type Person = {
|
||||
firstName: string
|
||||
@@ -205,7 +312,7 @@ describe('useField', () => {
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
/>
|
||||
<p>{field.getMeta().error}</p>
|
||||
<p>{field.getMeta().errors}</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('useField', () => {
|
||||
field.setValue((e.target as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
<p>{field.getMeta().error}</p>
|
||||
<p>{field.getMeta().errors}</p>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
@@ -122,7 +122,7 @@ describe('useField', () => {
|
||||
field.handleChange((e.target as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
<p>{field.getMeta().error}</p>
|
||||
<p>{field.getMeta().errors}</p>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
@@ -170,7 +170,7 @@ describe('useField', () => {
|
||||
field.handleChange((e.target as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
<p>{field.getMeta().error}</p>
|
||||
<p>{field.getMeta().errors}</p>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
@@ -222,7 +222,7 @@ describe('useField', () => {
|
||||
field.handleChange((e.target as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
<p>{field.getMeta().error}</p>
|
||||
<p>{field.getMeta().errors}</p>
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -197,7 +197,7 @@ importers:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
tsup:
|
||||
specifier: ^7.1.0
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0(typescript@5.2.2)
|
||||
type-fest:
|
||||
specifier: ^3.11.0
|
||||
|
||||
Reference in New Issue
Block a user