Skip to content

Commit

Permalink
chore: refactor form and field API validation mechanism to share code
Browse files Browse the repository at this point in the history
  • Loading branch information
crutchcorn committed Dec 4, 2023
1 parent 94df01d commit f9067ab
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 79 deletions.
43 changes: 20 additions & 23 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -361,26 +365,19 @@ export class FieldApi<

_runValidator<T, M extends 'validate' | 'validateAsync'>(
validate: T,
value: typeof this.state.value,
value: TData,
methodName: M,
): ReturnType<ReturnType<Validator<TData>>[M]> {
if (this.options.validatorAdapter && typeof validate !== 'function') {
return (this.options.validatorAdapter as Validator<TData>)()[methodName](
value,
validate,
) as never
}

if (this.form.options.validatorAdapter && typeof validate !== 'function') {
return (this.form.options.validatorAdapter as Validator<TData>)()[
methodName
](value, validate) as never
}

return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)(
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) => {
Expand Down
88 changes: 33 additions & 55 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TData, ValidatorType> = (
values: TData,
Expand Down Expand Up @@ -63,12 +69,6 @@ export type ValidationMeta = {
validationReject?: (errors: unknown) => void
}

export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`

export type ValidationErrorMap = {
[K in ValidationErrorMapKeys]?: ValidationError
}

export type FormState<TData> = {
values: TData
// Form Validation
Expand Down Expand Up @@ -185,23 +185,24 @@ export class FormApi<TFormData, ValidatorType> {
this.update(opts || {})
}

_runValidator<T, M extends 'validate' | 'validateAsync'>(
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<TFormData>)().validate(
this.state.values,
this.options.validators?.onMount,
)
}
return (
this.options.validators?.onMount as ValidateFn<TFormData, ValidatorType>
)(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,
Expand Down Expand Up @@ -282,21 +283,10 @@ export class FormApi<TFormData, ValidatorType> {
if (!validate) return

const errorMapKey = getErrorMapKey(cause)
const doValidate = () => {
if (this.options.validatorAdapter && typeof validate !== 'function') {
return (this.options.validatorAdapter as Validator<TFormData>)().validate(
this.state.values,
validate,
)
}

return (validate as ValidateFn<TFormData, ValidatorType>)(
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,
Expand Down Expand Up @@ -374,27 +364,15 @@ export class FormApi<TFormData, ValidatorType> {
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<TFormData>)().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) => ({
Expand Down
8 changes: 8 additions & 0 deletions packages/form-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ export type Validator<Type, Fn = unknown> = () => {
validate(value: Type, fn: Fn): ValidationError
validateAsync(value: Type, fn: Fn): Promise<ValidationError>
}

export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'

export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`

export type ValidationErrorMap = {
[K in ValidationErrorMapKeys]?: ValidationError
}
27 changes: 27 additions & 0 deletions packages/form-core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Validator } from './types'

export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput

export type Updater<TInput, TOutput = TInput> =
Expand Down Expand Up @@ -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<Validator<any> | undefined>
value: TData
methodName: M
suppliedThis: SuppliedThis
}): ReturnType<ReturnType<Validator<TData>>[M]> {
for (const adapter of props.adapters) {
if (adapter && typeof props.validateFn !== 'function') {
return (adapter as Validator<TData>)()[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<T, K extends keyof T> = Omit<T, K> &
Required<Pick<T, K>>

Expand Down
2 changes: 1 addition & 1 deletion packages/yup-form-adapter/src/tests/FieldApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('yup field api', () => {
validators: {
onChangeAsync: async (val) => (val === 'a' ? 'Test' : undefined),
onChangeAsyncDebounceMs: 0,
}
},
})

field.mount()
Expand Down

0 comments on commit f9067ab

Please sign in to comment.