@tanstack/form-core
Advanced tools
Comparing version 0.18.1 to 0.19.0
import { Store } from '@tanstack/store'; | ||
import type { FormApi } from './FormApi.js'; | ||
import type { FieldInfo, FormApi } from './FormApi.js'; | ||
import type { ValidationCause, ValidationError, ValidationErrorMap, Validator } from './types.js'; | ||
@@ -22,5 +22,7 @@ import type { Updater } from './utils.js'; | ||
onChangeAsyncDebounceMs?: number; | ||
onChangeListenTo?: DeepKeys<TParentData>[]; | ||
onBlur?: FieldValidateOrFn<TParentData, TName, TFieldValidator, TFormValidator, TData>; | ||
onBlurAsync?: FieldAsyncValidateOrFn<TParentData, TName, TFieldValidator, TFormValidator, TData>; | ||
onBlurAsyncDebounceMs?: number; | ||
onBlurListenTo?: DeepKeys<TParentData>[]; | ||
onSubmit?: FieldValidateOrFn<TParentData, TName, TFieldValidator, TFormValidator, TData>; | ||
@@ -83,3 +85,3 @@ onSubmitAsync?: FieldAsyncValidateOrFn<TParentData, TName, TFieldValidator, TFormValidator, TData>; | ||
setMeta: (updater: Updater<FieldMeta>) => void; | ||
getInfo: () => import("./FormApi").FieldInfo<TParentData, TFormValidator>; | ||
getInfo: () => FieldInfo<TParentData, TFormValidator>; | ||
pushValue: (value: TData extends any[] ? TData[number] : never) => void; | ||
@@ -89,10 +91,11 @@ insertValue: (index: number, value: TData extends any[] ? TData[number] : never) => void; | ||
swapValues: (aIndex: number, bIndex: number) => void; | ||
getLinkedFields: (cause: ValidationCause) => FieldApi<any, any, any, any, any>[]; | ||
moveValue: (aIndex: number, bIndex: number) => void; | ||
validateSync: (value: TData | undefined, cause: ValidationCause) => { | ||
validateSync: (cause: ValidationCause) => { | ||
hasErrored: boolean; | ||
}; | ||
validateAsync: (value: TData | undefined, cause: ValidationCause) => Promise<ValidationError[]>; | ||
validate: (cause: ValidationCause, value?: TData) => ValidationError[] | Promise<ValidationError[]>; | ||
validateAsync: (cause: ValidationCause) => Promise<ValidationError[]>; | ||
validate: (cause: ValidationCause) => ValidationError[] | Promise<ValidationError[]>; | ||
handleChange: (updater: Updater<TData>) => void; | ||
handleBlur: () => void; | ||
} |
@@ -68,3 +68,3 @@ import { Store } from "@tanstack/store"; | ||
this.form.setFieldValue(this.name, updater, options); | ||
this.validate("change", this.state.value); | ||
this.validate("change"); | ||
}; | ||
@@ -88,14 +88,39 @@ this._getMeta = () => this.form.getFieldMeta(this.name); | ||
this.swapValues = (aIndex, bIndex) => this.form.swapFieldValues(this.name, aIndex, bIndex); | ||
this.getLinkedFields = (cause) => { | ||
const fields = Object.values(this.form.fieldInfo); | ||
const linkedFields = []; | ||
for (const field of fields) { | ||
if (!field.instance) | ||
continue; | ||
const { onChangeListenTo, onBlurListenTo } = field.instance.options.validators || {}; | ||
if (cause === "change" && (onChangeListenTo == null ? void 0 : onChangeListenTo.includes(this.name))) { | ||
linkedFields.push(field.instance); | ||
} | ||
if (cause === "blur" && (onBlurListenTo == null ? void 0 : onBlurListenTo.includes(this.name))) { | ||
linkedFields.push(field.instance); | ||
} | ||
} | ||
return linkedFields; | ||
}; | ||
this.moveValue = (aIndex, bIndex) => this.form.moveFieldValues(this.name, aIndex, bIndex); | ||
this.validateSync = (value = this.state.value, cause) => { | ||
this.validateSync = (cause) => { | ||
const validates = getSyncValidatorArray(cause, this.options); | ||
const linkedFields = this.getLinkedFields(cause); | ||
const linkedFieldValidates = linkedFields.reduce( | ||
(acc, field) => { | ||
const fieldValidates = getSyncValidatorArray(cause, field.options); | ||
fieldValidates.forEach((validate) => { | ||
validate.field = field; | ||
}); | ||
return acc.concat(fieldValidates); | ||
}, | ||
[] | ||
); | ||
let hasErrored = false; | ||
this.form.store.batch(() => { | ||
for (const validateObj of validates) { | ||
if (!validateObj.validate) | ||
continue; | ||
const validateFieldFn = (field, validateObj) => { | ||
const error = normalizeError( | ||
this.runValidator({ | ||
field.runValidator({ | ||
validate: validateObj.validate, | ||
value: { value, fieldApi: this }, | ||
value: { value: field.getValue(), fieldApi: field }, | ||
type: "validate" | ||
@@ -105,4 +130,4 @@ }) | ||
const errorMapKey = getErrorMapKey(validateObj.cause); | ||
if (this.state.meta.errorMap[errorMapKey] !== error) { | ||
this.setMeta((prev) => ({ | ||
if (field.state.meta.errorMap[errorMapKey] !== error) { | ||
field.setMeta((prev) => ({ | ||
...prev, | ||
@@ -118,3 +143,13 @@ errorMap: { | ||
} | ||
}; | ||
for (const validateObj of validates) { | ||
if (!validateObj.validate) | ||
continue; | ||
validateFieldFn(this, validateObj); | ||
} | ||
for (const fieldValitateObj of linkedFieldValidates) { | ||
if (!fieldValitateObj.validate) | ||
continue; | ||
validateFieldFn(fieldValitateObj.field, fieldValitateObj); | ||
} | ||
}); | ||
@@ -133,13 +168,26 @@ const submitErrKey = getErrorMapKey("submit"); | ||
}; | ||
this.validateAsync = async (value = this.state.value, cause) => { | ||
this.validateAsync = async (cause) => { | ||
const validates = getAsyncValidatorArray(cause, this.options); | ||
const linkedFields = this.getLinkedFields(cause); | ||
const linkedFieldValidates = linkedFields.reduce( | ||
(acc, field) => { | ||
const fieldValidates = getAsyncValidatorArray(cause, field.options); | ||
fieldValidates.forEach((validate) => { | ||
validate.field = field; | ||
}); | ||
return acc.concat(fieldValidates); | ||
}, | ||
[] | ||
); | ||
if (!this.state.meta.isValidating) { | ||
this.setMeta((prev) => ({ ...prev, isValidating: true })); | ||
} | ||
const promises = []; | ||
for (const validateObj of validates) { | ||
if (!validateObj.validate) | ||
continue; | ||
for (const linkedField of linkedFields) { | ||
linkedField.setMeta((prev) => ({ ...prev, isValidating: true })); | ||
} | ||
const validatesPromises = []; | ||
const linkedPromises = []; | ||
const validateFieldAsyncFn = (field, validateObj, promises) => { | ||
const key = getErrorMapKey(validateObj.cause); | ||
const fieldValidatorMeta = this.getInfo().validationMetaMap[key]; | ||
const fieldValidatorMeta = field.getInfo().validationMetaMap[key]; | ||
fieldValidatorMeta == null ? void 0 : fieldValidatorMeta.lastAbortController.abort(); | ||
@@ -163,4 +211,4 @@ const controller = new AbortController(); | ||
value: { | ||
value, | ||
fieldApi: this, | ||
value: field.getValue(), | ||
fieldApi: field, | ||
signal: controller.signal | ||
@@ -180,3 +228,3 @@ }, | ||
const error = normalizeError(rawError); | ||
this.setMeta((prev) => { | ||
field.setMeta((prev) => { | ||
return { | ||
@@ -194,11 +242,29 @@ ...prev, | ||
); | ||
}; | ||
for (const validateObj of validates) { | ||
if (!validateObj.validate) | ||
continue; | ||
validateFieldAsyncFn(this, validateObj, validatesPromises); | ||
} | ||
for (const fieldValitateObj of linkedFieldValidates) { | ||
if (!fieldValitateObj.validate) | ||
continue; | ||
validateFieldAsyncFn( | ||
fieldValitateObj.field, | ||
fieldValitateObj, | ||
linkedPromises | ||
); | ||
} | ||
let results = []; | ||
if (promises.length) { | ||
results = await Promise.all(promises); | ||
if (validatesPromises.length || linkedPromises.length) { | ||
results = await Promise.all(validatesPromises); | ||
await Promise.all(linkedPromises); | ||
} | ||
this.setMeta((prev) => ({ ...prev, isValidating: false })); | ||
for (const linkedField of linkedFields) { | ||
linkedField.setMeta((prev) => ({ ...prev, isValidating: false })); | ||
} | ||
return results.filter(Boolean); | ||
}; | ||
this.validate = (cause, value) => { | ||
this.validate = (cause) => { | ||
if (!this.state.meta.isTouched) | ||
@@ -210,7 +276,7 @@ return []; | ||
} | ||
const { hasErrored } = this.validateSync(value, cause); | ||
const { hasErrored } = this.validateSync(cause); | ||
if (hasErrored && !this.options.asyncAlways) { | ||
return this.state.meta.errors; | ||
} | ||
return this.validateAsync(value, cause); | ||
return this.validateAsync(cause); | ||
}; | ||
@@ -217,0 +283,0 @@ this.handleChange = (updater) => { |
@@ -25,3 +25,3 @@ import type { ValidationCause } from './types.js'; | ||
} | ||
interface AsyncValidator<T> { | ||
export interface AsyncValidator<T> { | ||
cause: ValidationCause; | ||
@@ -35,3 +35,3 @@ validate: T; | ||
} | ||
interface SyncValidator<T> { | ||
export interface SyncValidator<T> { | ||
cause: ValidationCause; | ||
@@ -38,0 +38,0 @@ validate: T; |
{ | ||
"name": "@tanstack/form-core", | ||
"version": "0.18.1", | ||
"version": "0.19.0", | ||
"description": "Powerful, type-safe, framework agnostic forms.", | ||
@@ -5,0 +5,0 @@ "author": "tannerlinsley", |
import { Store } from '@tanstack/store' | ||
import { getAsyncValidatorArray, getSyncValidatorArray } from './utils' | ||
import type { FormApi } from './FormApi' | ||
import type { FieldInfo, FormApi } from './FormApi' | ||
import type { | ||
@@ -10,3 +10,3 @@ ValidationCause, | ||
} from './types' | ||
import type { Updater } from './utils' | ||
import type { AsyncValidator, SyncValidator, Updater } from './utils' | ||
import type { DeepKeys, DeepValue, NoInfer } from './util-types' | ||
@@ -154,2 +154,3 @@ | ||
onChangeAsyncDebounceMs?: number | ||
onChangeListenTo?: DeepKeys<TParentData>[] | ||
onBlur?: FieldValidateOrFn< | ||
@@ -170,2 +171,3 @@ TParentData, | ||
onBlurAsyncDebounceMs?: number | ||
onBlurListenTo?: DeepKeys<TParentData>[] | ||
onSubmit?: FieldValidateOrFn< | ||
@@ -453,3 +455,3 @@ TParentData, | ||
this.form.setFieldValue(this.name, updater as never, options) | ||
this.validate('change', this.state.value) | ||
this.validate('change') | ||
} | ||
@@ -489,8 +491,45 @@ | ||
getLinkedFields = (cause: ValidationCause) => { | ||
const fields = Object.values(this.form.fieldInfo) as FieldInfo< | ||
any, | ||
TFormValidator | ||
>[] | ||
const linkedFields: FieldApi<any, any, any, any>[] = [] | ||
for (const field of fields) { | ||
if (!field.instance) continue | ||
const { onChangeListenTo, onBlurListenTo } = | ||
field.instance.options.validators || {} | ||
if ( | ||
cause === 'change' && | ||
onChangeListenTo?.includes(this.name as string) | ||
) { | ||
linkedFields.push(field.instance) | ||
} | ||
if (cause === 'blur' && onBlurListenTo?.includes(this.name as string)) { | ||
linkedFields.push(field.instance) | ||
} | ||
} | ||
return linkedFields | ||
} | ||
moveValue = (aIndex: number, bIndex: number) => | ||
this.form.moveFieldValues(this.name, aIndex, bIndex) | ||
validateSync = (value = this.state.value, cause: ValidationCause) => { | ||
validateSync = (cause: ValidationCause) => { | ||
const validates = getSyncValidatorArray(cause, this.options) | ||
const linkedFields = this.getLinkedFields(cause) | ||
const linkedFieldValidates = linkedFields.reduce( | ||
(acc, field) => { | ||
const fieldValidates = getSyncValidatorArray(cause, field.options) | ||
fieldValidates.forEach((validate) => { | ||
;(validate as any).field = field | ||
}) | ||
return acc.concat(fieldValidates as never) | ||
}, | ||
[] as Array<SyncValidator<any> & { field: FieldApi<any, any, any, any> }>, | ||
) | ||
// Needs type cast as eslint errantly believes this is always falsy | ||
@@ -500,8 +539,10 @@ let hasErrored = false as boolean | ||
this.form.store.batch(() => { | ||
for (const validateObj of validates) { | ||
if (!validateObj.validate) continue | ||
const validateFieldFn = ( | ||
field: FieldApi<any, any, any, any>, | ||
validateObj: SyncValidator<any>, | ||
) => { | ||
const error = normalizeError( | ||
this.runValidator({ | ||
field.runValidator({ | ||
validate: validateObj.validate, | ||
value: { value, fieldApi: this }, | ||
value: { value: field.getValue(), fieldApi: field }, | ||
type: 'validate', | ||
@@ -511,4 +552,4 @@ }), | ||
const errorMapKey = getErrorMapKey(validateObj.cause) | ||
if (this.state.meta.errorMap[errorMapKey] !== error) { | ||
this.setMeta((prev) => ({ | ||
if (field.state.meta.errorMap[errorMapKey] !== error) { | ||
field.setMeta((prev) => ({ | ||
...prev, | ||
@@ -525,2 +566,11 @@ errorMap: { | ||
} | ||
for (const validateObj of validates) { | ||
if (!validateObj.validate) continue | ||
validateFieldFn(this, validateObj) | ||
} | ||
for (const fieldValitateObj of linkedFieldValidates) { | ||
if (!fieldValitateObj.validate) continue | ||
validateFieldFn(fieldValitateObj.field, fieldValitateObj) | ||
} | ||
}) | ||
@@ -550,5 +600,19 @@ | ||
validateAsync = async (value = this.state.value, cause: ValidationCause) => { | ||
validateAsync = async (cause: ValidationCause) => { | ||
const validates = getAsyncValidatorArray(cause, this.options) | ||
const linkedFields = this.getLinkedFields(cause) | ||
const linkedFieldValidates = linkedFields.reduce( | ||
(acc, field) => { | ||
const fieldValidates = getAsyncValidatorArray(cause, field.options) | ||
fieldValidates.forEach((validate) => { | ||
;(validate as any).field = field | ||
}) | ||
return acc.concat(fieldValidates as never) | ||
}, | ||
[] as Array< | ||
AsyncValidator<any> & { field: FieldApi<any, any, any, any> } | ||
>, | ||
) | ||
if (!this.state.meta.isValidating) { | ||
@@ -558,2 +622,6 @@ this.setMeta((prev) => ({ ...prev, isValidating: true })) | ||
for (const linkedField of linkedFields) { | ||
linkedField.setMeta((prev) => ({ ...prev, isValidating: true })) | ||
} | ||
/** | ||
@@ -563,8 +631,12 @@ * We have to use a for loop and generate our promises this way, otherwise it won't be sync | ||
*/ | ||
const promises: Promise<ValidationError | undefined>[] = [] | ||
const validatesPromises: Promise<ValidationError | undefined>[] = [] | ||
const linkedPromises: Promise<ValidationError | undefined>[] = [] | ||
for (const validateObj of validates) { | ||
if (!validateObj.validate) continue | ||
const validateFieldAsyncFn = ( | ||
field: FieldApi<any, any, any, any>, | ||
validateObj: AsyncValidator<any>, | ||
promises: Promise<ValidationError | undefined>[], | ||
) => { | ||
const key = getErrorMapKey(validateObj.cause) | ||
const fieldValidatorMeta = this.getInfo().validationMetaMap[key] | ||
const fieldValidatorMeta = field.getInfo().validationMetaMap[key] | ||
@@ -590,4 +662,4 @@ fieldValidatorMeta?.lastAbortController.abort() | ||
value: { | ||
value, | ||
fieldApi: this, | ||
value: field.getValue(), | ||
fieldApi: field, | ||
signal: controller.signal, | ||
@@ -607,3 +679,3 @@ }, | ||
const error = normalizeError(rawError) | ||
this.setMeta((prev) => { | ||
field.setMeta((prev) => { | ||
return { | ||
@@ -624,5 +696,20 @@ ...prev, | ||
// TODO: Dedupe this logic to reduce bundle size | ||
for (const validateObj of validates) { | ||
if (!validateObj.validate) continue | ||
validateFieldAsyncFn(this, validateObj, validatesPromises) | ||
} | ||
for (const fieldValitateObj of linkedFieldValidates) { | ||
if (!fieldValitateObj.validate) continue | ||
validateFieldAsyncFn( | ||
fieldValitateObj.field, | ||
fieldValitateObj, | ||
linkedPromises, | ||
) | ||
} | ||
let results: ValidationError[] = [] | ||
if (promises.length) { | ||
results = await Promise.all(promises) | ||
if (validatesPromises.length || linkedPromises.length) { | ||
results = await Promise.all(validatesPromises) | ||
await Promise.all(linkedPromises) | ||
} | ||
@@ -632,2 +719,6 @@ | ||
for (const linkedField of linkedFields) { | ||
linkedField.setMeta((prev) => ({ ...prev, isValidating: false })) | ||
} | ||
return results.filter(Boolean) | ||
@@ -638,3 +729,2 @@ } | ||
cause: ValidationCause, | ||
value?: TData, | ||
): ValidationError[] | Promise<ValidationError[]> => { | ||
@@ -649,3 +739,3 @@ // If the field is pristine and validatePristine is false, do not validate | ||
// Attempt to sync validate first | ||
const { hasErrored } = this.validateSync(value, cause) | ||
const { hasErrored } = this.validateSync(cause) | ||
@@ -656,3 +746,3 @@ if (hasErrored && !this.options.asyncAlways) { | ||
// No error? Attempt async validation | ||
return this.validateAsync(value, cause) | ||
return this.validateAsync(cause) | ||
} | ||
@@ -659,0 +749,0 @@ |
@@ -724,2 +724,160 @@ import { describe, expect, it, vi } from 'vitest' | ||
}) | ||
it('should run onChange on a linked field', () => { | ||
const form = new FormApi({ | ||
defaultValues: { | ||
password: '', | ||
confirm_password: '', | ||
}, | ||
}) | ||
const passField = new FieldApi({ | ||
form, | ||
name: 'password', | ||
}) | ||
const passconfirmField = new FieldApi({ | ||
form, | ||
name: 'confirm_password', | ||
validators: { | ||
onChangeListenTo: ['password'], | ||
onChange: ({ value, fieldApi }) => { | ||
if (value !== fieldApi.form.getFieldValue('password')) { | ||
return 'Passwords do not match' | ||
} | ||
return undefined | ||
}, | ||
}, | ||
}) | ||
passField.mount() | ||
passconfirmField.mount() | ||
passField.setValue('one', { touch: true }) | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([ | ||
'Passwords do not match', | ||
]) | ||
passconfirmField.setValue('one', { touch: true }) | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([]) | ||
passField.setValue('two', { touch: true }) | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([ | ||
'Passwords do not match', | ||
]) | ||
}) | ||
it('should run onBlur on a linked field', () => { | ||
const form = new FormApi({ | ||
defaultValues: { | ||
password: '', | ||
confirm_password: '', | ||
}, | ||
}) | ||
const passField = new FieldApi({ | ||
form, | ||
name: 'password', | ||
}) | ||
const passconfirmField = new FieldApi({ | ||
form, | ||
name: 'confirm_password', | ||
validators: { | ||
onBlurListenTo: ['password'], | ||
onBlur: ({ value, fieldApi }) => { | ||
if (value !== fieldApi.form.getFieldValue('password')) { | ||
return 'Passwords do not match' | ||
} | ||
return undefined | ||
}, | ||
}, | ||
}) | ||
passField.mount() | ||
passconfirmField.mount() | ||
passField.setValue('one', { touch: true }) | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([]) | ||
passField.handleBlur() | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([ | ||
'Passwords do not match', | ||
]) | ||
passconfirmField.setValue('one', { touch: true }) | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([ | ||
'Passwords do not match', | ||
]) | ||
passField.handleBlur() | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([]) | ||
passField.setValue('two', { touch: true }) | ||
passField.handleBlur() | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([ | ||
'Passwords do not match', | ||
]) | ||
}) | ||
it('should run onChangeAsync on a linked field', async () => { | ||
vi.useRealTimers() | ||
let resolve!: () => void | ||
let promise = new Promise((r) => { | ||
resolve = r as never | ||
}) | ||
const fn = vi.fn() | ||
const form = new FormApi({ | ||
defaultValues: { | ||
password: '', | ||
confirm_password: '', | ||
}, | ||
}) | ||
const passField = new FieldApi({ | ||
form, | ||
name: 'password', | ||
}) | ||
const passconfirmField = new FieldApi({ | ||
form, | ||
name: 'confirm_password', | ||
validators: { | ||
onChangeListenTo: ['password'], | ||
onChangeAsync: async ({ value, fieldApi }) => { | ||
await promise | ||
fn() | ||
if (value !== fieldApi.form.getFieldValue('password')) { | ||
return 'Passwords do not match' | ||
} | ||
return undefined | ||
}, | ||
}, | ||
}) | ||
passField.mount() | ||
passconfirmField.mount() | ||
passField.setValue('one', { touch: true }) | ||
resolve() | ||
// Allow for a micro-tick to allow the promise to resolve | ||
await sleep(1) | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([ | ||
'Passwords do not match', | ||
]) | ||
promise = new Promise((r) => { | ||
resolve = r as never | ||
}) | ||
passconfirmField.setValue('one', { touch: true }) | ||
resolve() | ||
// Allow for a micro-tick to allow the promise to resolve | ||
await sleep(1) | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([]) | ||
promise = new Promise((r) => { | ||
resolve = r as never | ||
}) | ||
passField.setValue('two', { touch: true }) | ||
resolve() | ||
// Allow for a micro-tick to allow the promise to resolve | ||
await sleep(1) | ||
expect(passconfirmField.state.meta.errors).toStrictEqual([ | ||
'Passwords do not match', | ||
]) | ||
}) | ||
}) |
@@ -160,3 +160,3 @@ import type { ValidationCause } from './types' | ||
interface AsyncValidator<T> { | ||
export interface AsyncValidator<T> { | ||
cause: ValidationCause | ||
@@ -230,3 +230,3 @@ validate: T | ||
interface SyncValidator<T> { | ||
export interface SyncValidator<T> { | ||
cause: ValidationCause | ||
@@ -233,0 +233,0 @@ validate: T |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
374713
6224