diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 13e91dfb9..5b9439410 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1,9 +1,13 @@ import { Store } from '@tanstack/store' -import type { FormApi, ValidationErrorMap } from './FormApi' -import type { ValidationError, Validator } from './types' +import type { FormApi } from './FormApi' +import type { + ValidationCause, + ValidationError, + ValidationErrorMap, + Validator, +} from './types' import type { DeepKeys, DeepValue, Updater } from './utils' - -export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' +import { runValidatorOrAdapter } from './utils' type ValidateFn< TParentData, @@ -361,26 +365,19 @@ export class FieldApi< _runValidator( validate: T, - value: typeof this.state.value, + value: TData, methodName: M, - ): ReturnType>[M]> { - if (this.options.validatorAdapter && typeof validate !== 'function') { - return (this.options.validatorAdapter as Validator)()[methodName]( - value, - validate, - ) as never - } - - if (this.form.options.validatorAdapter && typeof validate !== 'function') { - return (this.form.options.validatorAdapter as Validator)()[ - methodName - ](value, validate) as never - } - - return (validate as ValidateFn)( - value, - this as never, - ) as never + ) { + return runValidatorOrAdapter({ + validateFn: validate, + value: value, + methodName: methodName, + suppliedThis: this, + adapters: [ + this.form.options.validatorAdapter, + this.options.validatorAdapter as never, + ], + }) } validateSync = (value = this.state.value, cause: ValidationCause) => { diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4d0858ec7..415423742 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -6,9 +6,15 @@ import { getBy, isNonEmptyArray, setBy, + runValidatorOrAdapter, } from './utils' -import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi' -import type { ValidationError, Validator } from './types' +import type { FieldApi, FieldMeta } from './FieldApi' +import type { + ValidationError, + ValidationErrorMap, + Validator, + ValidationCause, +} from './types' type ValidateFn = ( values: TData, @@ -63,12 +69,6 @@ export type ValidationMeta = { validationReject?: (errors: unknown) => void } -export type ValidationErrorMapKeys = `on${Capitalize}` - -export type ValidationErrorMap = { - [K in ValidationErrorMapKeys]?: ValidationError -} - export type FormState = { values: TData // Form Validation @@ -185,23 +185,24 @@ export class FormApi { this.update(opts || {}) } + _runValidator( + validate: T, + value: TFormData, + methodName: M, + ) { + return runValidatorOrAdapter({ + validateFn: validate, + value: value, + methodName: methodName, + suppliedThis: this, + adapters: [this.options.validatorAdapter as never], + }) + } + mount = () => { - const doValidate = () => { - if ( - this.options.validatorAdapter && - typeof this.options.validators?.onMount !== 'function' - ) { - return (this.options.validatorAdapter as Validator)().validate( - this.state.values, - this.options.validators?.onMount, - ) - } - return ( - this.options.validators?.onMount as ValidateFn - )(this.state.values, this) - } - if (!this.options.validators?.onMount) return - const error = doValidate() + const { onMount } = this.options.validators || {} + if (!onMount) return + const error = this._runValidator(onMount, this.state.values, 'validate') if (error) { this.store.setState((prev) => ({ ...prev, @@ -282,21 +283,10 @@ export class FormApi { if (!validate) return const errorMapKey = getErrorMapKey(cause) - const doValidate = () => { - if (this.options.validatorAdapter && typeof validate !== 'function') { - return (this.options.validatorAdapter as Validator)().validate( - this.state.values, - validate, - ) - } - return (validate as ValidateFn)( - this.state.values, - this, - ) - } - - const error = normalizeError(doValidate()) + const error = normalizeError( + this._runValidator(validate, this.state.values, 'validate'), + ) if (this.state.errorMap[errorMapKey] !== error) { this.store.setState((prev) => ({ ...prev, @@ -374,27 +364,15 @@ export class FormApi { await new Promise((r) => setTimeout(r, debounceMs)) } - const doValidate = () => { - if (typeof validate === 'function') { - return validate(this.state.values, this) as ValidationError - } - if (this.options.validatorAdapter && typeof validate !== 'function') { - return (this.options.validatorAdapter as Validator)().validateAsync( - this.state.values, - validate, - ) - } - const errorMapKey = getErrorMapKey(cause) - throw new Error( - `Form validation for ${errorMapKey}Async failed. ${errorMapKey}Async should either be a function, or \`validator\` should be correct.`, - ) - } - // Only kick off validation if this validation is the latest attempt if (checkLatest()) { const prevErrors = this.state.errors try { - const rawError = await doValidate() + const rawError = await this._runValidator( + validate, + this.state.values, + 'validateAsync', + ) if (checkLatest()) { const error = normalizeError(rawError) this.store.setState((prev) => ({ diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index 54bae7324..f0956ddea 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -5,3 +5,11 @@ export type Validator = () => { validate(value: Type, fn: Fn): ValidationError validateAsync(value: Type, fn: Fn): Promise } + +export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' + +export type ValidationErrorMapKeys = `on${Capitalize}` + +export type ValidationErrorMap = { + [K in ValidationErrorMapKeys]?: ValidationError +} diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 9f2204a9d..5f30c6d81 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -1,3 +1,5 @@ +import type { Validator } from './types' + export type UpdaterFn = (input: TInput) => TOutput export type Updater = @@ -141,6 +143,31 @@ export function isNonEmptyArray(obj: any) { return !(Array.isArray(obj) && obj.length === 0) } +export function runValidatorOrAdapter< + TData, + SuppliedThis, + M extends 'validate' | 'validateAsync', +>(props: { + validateFn: unknown + // Order matters, first is run first + adapters: Array | undefined> + value: TData + methodName: M + suppliedThis: SuppliedThis +}): ReturnType>[M]> { + for (const adapter of props.adapters) { + if (adapter && typeof props.validateFn !== 'function') { + return (adapter as Validator)()[props.methodName]( + props.value, + props.validateFn, + ) as never + } + } + + const validateFn: (...vals: any[]) => any = props.validateFn as never + return validateFn(props.value, props.suppliedThis) as never +} + export type RequiredByKey = Omit & Required> diff --git a/packages/yup-form-adapter/src/tests/FieldApi.spec.ts b/packages/yup-form-adapter/src/tests/FieldApi.spec.ts index 48151cf4e..793377fb9 100644 --- a/packages/yup-form-adapter/src/tests/FieldApi.spec.ts +++ b/packages/yup-form-adapter/src/tests/FieldApi.spec.ts @@ -104,7 +104,7 @@ describe('yup field api', () => { validators: { onChangeAsync: async (val) => (val === 'a' ? 'Test' : undefined), onChangeAsyncDebounceMs: 0, - } + }, }) field.mount()