@tanstack/form-core
Advanced tools
Comparing version 0.2.0 to 0.3.0
{ | ||
"name": "@tanstack/form-core", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Powerful, type-safe, framework agnostic forms.", | ||
@@ -5,0 +5,0 @@ "author": "tannerlinsley", |
@@ -1,6 +0,6 @@ | ||
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' | ||
@@ -55,4 +55,5 @@ type ValidateFn<TData, TFormData> = ( | ||
isTouched: boolean | ||
touchedError?: ValidationError | ||
error?: ValidationError | ||
touchedErrors: ValidationError[] | ||
errors: ValidationError[] | ||
errorMap: ValidationErrorMap | ||
isValidating: boolean | ||
@@ -114,2 +115,5 @@ } | ||
isTouched: false, | ||
touchedErrors: [], | ||
errors: [], | ||
errorMap: {}, | ||
...opts.defaultMeta, | ||
@@ -122,5 +126,5 @@ }, | ||
state.meta.touchedError = state.meta.isTouched | ||
? state.meta.error | ||
: undefined | ||
state.meta.touchedErrors = state.meta.isTouched | ||
? state.meta.errors | ||
: [] | ||
@@ -209,2 +213,5 @@ this.prevState = state | ||
isTouched: false, | ||
touchedErrors: [], | ||
errors: [], | ||
errorMap: {}, | ||
...this.options.defaultMeta, | ||
@@ -246,3 +253,2 @@ } as FieldMeta) | ||
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur | ||
if (!validate) return | ||
@@ -255,12 +261,16 @@ | ||
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() | ||
@@ -302,5 +312,3 @@ } | ||
: onBlurAsync | ||
if (!validate) return | ||
if (!validate) return [] | ||
const debounceMs = | ||
@@ -338,5 +346,5 @@ cause === 'submit' | ||
if (checkLatest()) { | ||
const prevErrors = this.getMeta().errors | ||
try { | ||
const rawError = await validate(value as never, this as never) | ||
if (checkLatest()) { | ||
@@ -347,9 +355,13 @@ const error = normalizeError(rawError) | ||
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 | ||
@@ -366,3 +378,3 @@ } | ||
// Always return the latest validation promise to the caller | ||
return this.getInfo().validationPromise | ||
return this.getInfo().validationPromise ?? [] | ||
} | ||
@@ -373,16 +385,15 @@ | ||
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 | ||
@@ -417,1 +428,14 @@ return this.validateAsync(value, cause) | ||
} | ||
function getErrorMapKey(cause: ValidationCause) { | ||
switch (cause) { | ||
case 'submit': | ||
return 'onSubmit' | ||
case 'change': | ||
return 'onChange' | ||
case 'blur': | ||
return 'onBlur' | ||
case 'mount': | ||
return 'onMount' | ||
} | ||
} |
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' | ||
@@ -40,5 +40,5 @@ | ||
validationAsyncCount?: number | ||
validationPromise?: Promise<ValidationError> | ||
validationResolve?: (error: ValidationError) => void | ||
validationReject?: (error: unknown) => void | ||
validationPromise?: Promise<ValidationError[]> | ||
validationResolve?: (errors: ValidationError[]) => void | ||
validationReject?: (errors: unknown) => void | ||
} | ||
@@ -48,2 +48,8 @@ | ||
export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}` | ||
export type ValidationErrorMap = { | ||
[K in ValidationErrorMapKeys]?: ValidationError | ||
} | ||
export type FormState<TData> = { | ||
@@ -122,3 +128,5 @@ values: TData | ||
const isFieldsValid = !fieldMetaValues.some((field) => field?.error) | ||
const isFieldsValid = !fieldMetaValues.some((field) => | ||
isNonEmptyArray(field?.errors), | ||
) | ||
@@ -198,4 +206,3 @@ const isTouched = fieldMetaValues.some((field) => field?.isTouched) | ||
validateAllFields = async (cause: ValidationCause) => { | ||
const fieldValidationPromises: Promise<ValidationError>[] = [] as any | ||
const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any | ||
this.store.batch(() => { | ||
@@ -202,0 +209,0 @@ void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach( |
@@ -33,2 +33,5 @@ import { expect } from 'vitest' | ||
isValidating: false, | ||
touchedErrors: [], | ||
errors: [], | ||
errorMap: {}, | ||
}) | ||
@@ -48,2 +51,5 @@ }) | ||
isValidating: false, | ||
touchedErrors: [], | ||
errors: [], | ||
errorMap: {}, | ||
}) | ||
@@ -198,5 +204,8 @@ }) | ||
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', | ||
}) | ||
}) | ||
@@ -225,6 +234,9 @@ | ||
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', | ||
}) | ||
}) | ||
@@ -255,3 +267,3 @@ | ||
expect(field.getMeta().error).toBeUndefined() | ||
expect(field.getMeta().errors.length).toBe(0) | ||
field.setValue('other', { touch: true }) | ||
@@ -262,3 +274,6 @@ field.setValue('other') | ||
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', | ||
}) | ||
}) | ||
@@ -289,3 +304,3 @@ | ||
expect(field.getMeta().error).toBeUndefined() | ||
expect(field.getMeta().errors.length).toBe(0) | ||
field.setValue('other', { touch: true }) | ||
@@ -296,3 +311,6 @@ field.setValue('other') | ||
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', | ||
}) | ||
}) | ||
@@ -320,3 +338,6 @@ | ||
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', | ||
}) | ||
}) | ||
@@ -345,7 +366,10 @@ | ||
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', | ||
}) | ||
}) | ||
@@ -376,3 +400,3 @@ | ||
expect(field.getMeta().error).toBeUndefined() | ||
expect(field.getMeta().errors.length).toBe(0) | ||
field.setValue('other', { touch: true }) | ||
@@ -384,3 +408,6 @@ field.validate('blur') | ||
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', | ||
}) | ||
}) | ||
@@ -411,3 +438,3 @@ | ||
expect(field.getMeta().error).toBeUndefined() | ||
expect(field.getMeta().errors.length).toBe(0) | ||
field.setValue('other', { touch: true }) | ||
@@ -419,3 +446,6 @@ field.validate('blur') | ||
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', | ||
}) | ||
}) | ||
@@ -444,9 +474,46 @@ | ||
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 () => { | ||
@@ -453,0 +520,0 @@ interface Form { |
@@ -104,2 +104,6 @@ export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput | ||
export function isNonEmptyArray(obj: any) { | ||
return !(Array.isArray(obj) && obj.length === 0) | ||
} | ||
export type RequiredByKey<T, K extends keyof T> = Omit<T, K> & | ||
@@ -106,0 +110,0 @@ Required<Pick<T, K>> |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
49784
10
1563