From c3fac9e28bf0e0b9b25457d24190b08fe7e33175 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 27 Aug 2024 09:11:58 -0700 Subject: [PATCH 01/16] feat: add ability to pass errors to specific fields from Zod --- examples/react/zod/src/index.tsx | 23 +++++--------- packages/form-core/src/FieldApi.ts | 15 ++++++++-- packages/form-core/src/FormApi.ts | 10 ++++++- packages/form-core/src/types.ts | 9 ++++-- packages/form-core/src/utils.ts | 6 +++- packages/zod-form-adapter/src/validator.ts | 35 +++++++++++++++------- 6 files changed, 66 insertions(+), 32 deletions(-) diff --git a/examples/react/zod/src/index.tsx b/examples/react/zod/src/index.tsx index 8d85302b2..104520f62 100644 --- a/examples/react/zod/src/index.tsx +++ b/examples/react/zod/src/index.tsx @@ -26,8 +26,14 @@ export default function App() { // Do something with form data console.log(value) }, + validators: { + onChange: z.object({ + firstName: z.string().min(2), + lastName: z.string().max(4), + }), + }, // Add a validator to support Zod usage in Form and Field - validatorAdapter: zodValidator(), + validatorAdapter: zodValidator({}), }) return ( @@ -44,21 +50,6 @@ export default function App() { {/* A type-safe field component*/} { - await new Promise((resolve) => setTimeout(resolve, 1000)) - return !value.includes('error') - }, - { - message: "No 'error' allowed in first name", - }, - ), - }} children={(field) => { // Avoid hasty abstractions. Render props are great! return ( diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index b44dfdee8..50acdd3cf 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -2,6 +2,7 @@ import { Store } from '@tanstack/store' import { getAsyncValidatorArray, getBy, getSyncValidatorArray } from './utils' import type { FieldInfo, FieldsErrorMapFromValidator, FormApi } from './FormApi' import type { + APITypes, UpdateMetaOptions, ValidationCause, ValidationError, @@ -488,7 +489,11 @@ export class FieldApi< * @private */ runValidator< - TValue extends { value: TData; fieldApi: FieldApi }, + TValue extends { + value: TData + fieldApi: FieldApi + api: APITypes + }, TType extends 'validate' | 'validateAsync', >(props: { validate: TType extends 'validate' @@ -543,6 +548,7 @@ export class FieldApi< value: { value: this.state.value, fieldApi: this, + api: 'field', }, type: 'validate', }) @@ -757,7 +763,11 @@ export class FieldApi< ? normalizeError( field.runValidator({ validate: validateObj.validate, - value: { value: field.getValue(), fieldApi: field }, + value: { + value: field.getValue(), + api: 'field', + fieldApi: field, + }, type: 'validate', }), ) @@ -884,6 +894,7 @@ export class FieldApi< value: field.getValue(), fieldApi: field, signal: controller.signal, + api: 'field', }, type: 'validateAsync', }), diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 27f6159ca..d27c3ef83 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -12,6 +12,7 @@ import type { Updater } from './utils' import type { DeepKeys, DeepValue } from './util-types' import type { FieldApi, FieldMeta } from './FieldApi' import type { + APITypes, FormValidationError, UpdateMetaOptions, ValidationCause, @@ -442,7 +443,11 @@ export class FormApi< * @private */ runValidator< - TValue extends { value: TFormData; formApi: FormApi }, + TValue extends { + value: TFormData + formApi: FormApi + api: APITypes + }, TType extends 'validate' | 'validateAsync', >(props: { validate: TType extends 'validate' @@ -467,6 +472,7 @@ export class FormApi< value: { value: this.state.values, formApi: this, + api: 'form', }, type: 'validate', }) @@ -643,6 +649,7 @@ export class FormApi< value: { value: this.state.values, formApi: this, + api: 'form', }, type: 'validate', }) @@ -760,6 +767,7 @@ export class FormApi< value: { value: this.state.values, formApi: this, + api: 'form', signal: controller.signal, }, type: 'validateAsync', diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index 869260ba5..d3f51c78d 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -2,13 +2,18 @@ import { type DeepKeys } from './util-types' export type ValidationError = undefined | false | null | string +export type APITypes = 'form' | 'field' + /** * If/when TypeScript supports higher-kinded types, this should not be `unknown` anymore * @private */ export type Validator = () => { - validate(options: { value: Type }, fn: Fn): ValidationError - validateAsync(options: { value: Type }, fn: Fn): Promise + validate(options: { value: Type; api: APITypes }, fn: Fn): ValidationError + validateAsync( + options: { value: Type; api: APITypes }, + fn: Fn, + ): Promise } /** diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 535b1d3a0..46849d518 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -139,7 +139,11 @@ const intReplace = `${intPrefix}$1` /** * @private */ -export function makePathArray(str: string) { +export function makePathArray(str: string | Array) { + if (Array.isArray(str)) { + return [...str] + } + if (typeof str !== 'string') { throw new Error('Path must be a string.') } diff --git a/packages/zod-form-adapter/src/validator.ts b/packages/zod-form-adapter/src/validator.ts index 077263699..c1c3d34eb 100644 --- a/packages/zod-form-adapter/src/validator.ts +++ b/packages/zod-form-adapter/src/validator.ts @@ -1,27 +1,42 @@ +import { setBy } from '@tanstack/form-core' import type { Validator, ValidatorAdapterParams } from '@tanstack/form-core' import type { ZodIssue, ZodType } from 'zod' type Params = ValidatorAdapterParams +export function prefixSchemaToErorrs(zodErrors: ZodIssue[]) { + let schema = {} as object + for (const zodError of zodErrors) { + schema = setBy(schema, zodError.path, () => zodError.message) + } + return { fields: schema } as never +} + +export const mapIssues = (issues: ZodIssue[]) => + issues.map((issue) => issue.message).join(', ') + export const zodValidator = (params: Params = {}): Validator => () => { return { - validate({ value }, fn) { + validate({ value, api }, fn) { + const transformErrors = + (params.transformErrors ?? api === 'form') + ? prefixSchemaToErorrs + : mapIssues const result = fn.safeParse(value) if (result.success) return - if (params.transformErrors) { - return params.transformErrors(result.error.issues) - } - return result.error.issues.map((issue) => issue.message).join(', ') + return transformErrors(result.error.issues) }, - async validateAsync({ value }, fn) { + async validateAsync({ value, api }, fn) { + const transformErrors = + (params.transformErrors ?? api === 'form') + ? prefixSchemaToErorrs + : mapIssues + const result = await fn.safeParseAsync(value) if (result.success) return - if (params.transformErrors) { - return params.transformErrors(result.error.issues) - } - return result.error.issues.map((issue) => issue.message).join(', ') + return transformErrors(result.error.issues) }, } } From 9ff14cad597d23d8ff997e3aabde51f2e56eb9c1 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 27 Aug 2024 09:20:50 -0700 Subject: [PATCH 02/16] chore: code cleanup --- packages/form-core/src/types.ts | 7 +++++-- packages/zod-form-adapter/src/validator.ts | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index d3f51c78d..c3d6034d6 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -9,11 +9,14 @@ export type APITypes = 'form' | 'field' * @private */ export type Validator = () => { - validate(options: { value: Type; api: APITypes }, fn: Fn): ValidationError + validate( + options: { value: Type; api: APITypes }, + fn: Fn, + ): ValidationError | FormValidationError validateAsync( options: { value: Type; api: APITypes }, fn: Fn, - ): Promise + ): Promise> } /** diff --git a/packages/zod-form-adapter/src/validator.ts b/packages/zod-form-adapter/src/validator.ts index c1c3d34eb..d2b141911 100644 --- a/packages/zod-form-adapter/src/validator.ts +++ b/packages/zod-form-adapter/src/validator.ts @@ -9,10 +9,17 @@ export function prefixSchemaToErorrs(zodErrors: ZodIssue[]) { for (const zodError of zodErrors) { schema = setBy(schema, zodError.path, () => zodError.message) } - return { fields: schema } as never + return schema } -export const mapIssues = (issues: ZodIssue[]) => +export function defaultFormTransformer(zodErrors: ZodIssue[]) { + return { + form: mapIssuesToSingleString(zodErrors), + fields: prefixSchemaToErorrs(zodErrors), + } +} + +export const mapIssuesToSingleString = (issues: ZodIssue[]) => issues.map((issue) => issue.message).join(', ') export const zodValidator = @@ -22,8 +29,8 @@ export const zodValidator = validate({ value, api }, fn) { const transformErrors = (params.transformErrors ?? api === 'form') - ? prefixSchemaToErorrs - : mapIssues + ? defaultFormTransformer + : mapIssuesToSingleString const result = fn.safeParse(value) if (result.success) return return transformErrors(result.error.issues) @@ -31,8 +38,8 @@ export const zodValidator = async validateAsync({ value, api }, fn) { const transformErrors = (params.transformErrors ?? api === 'form') - ? prefixSchemaToErorrs - : mapIssues + ? defaultFormTransformer + : mapIssuesToSingleString const result = await fn.safeParseAsync(value) if (result.success) return From 929df46a731cc01da09bd4850f56e86d45db388f Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Wed, 28 Aug 2024 02:19:58 -0700 Subject: [PATCH 03/16] chore: fix various issues --- packages/form-core/src/FieldApi.ts | 8 +++++- packages/form-core/src/FormApi.ts | 33 +++++++++++++++++----- packages/form-core/src/types.ts | 11 ++++++-- packages/zod-form-adapter/src/validator.ts | 10 +++---- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 50acdd3cf..eba622a9c 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1,4 +1,5 @@ import { Store } from '@tanstack/store' +import { FormValidationError } from './types' import { getAsyncValidatorArray, getBy, getSyncValidatorArray } from './utils' import type { FieldInfo, FieldsErrorMapFromValidator, FormApi } from './FormApi' import type { @@ -501,7 +502,12 @@ export class FieldApi< : FieldAsyncValidateOrFn value: TValue type: TType - }): ReturnType>[TType]> { + // When `api` is 'field', the return type cannot be `FormValidationError` + }): ReturnType>[TType]> extends infer TR + ? TR extends Promise + ? Promise>> + : Exclude> + : never { const adapters = [ this.form.options.validatorAdapter, this.options.validatorAdapter, diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index d27c3ef83..6cc67bee6 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1,4 +1,5 @@ import { Store } from '@tanstack/store' +import { ServerValidationErrorMap } from './types' import { deleteBy, functionalUpdate, @@ -235,7 +236,7 @@ export type FormState = { /** * The error map for the form itself. */ - errorMap: ValidationErrorMap + errorMap: ServerValidationErrorMap /** * An internal mechanism used for keeping track of validation logic in a form. */ @@ -321,6 +322,12 @@ function getDefaultFormState( } } +const isFormValidationError = ( + error: unknown, +): error is FormValidationError => { + return typeof error === 'object' +} + /** * A class representing the Form API. It handles the logic and interactions with the form state. * @@ -394,9 +401,18 @@ export class FormApi< const isPristine = !isDirty const isValidating = isFieldsValidating || state.isFormValidating - state.errors = Object.values(state.errorMap).filter( - (val: unknown) => val !== undefined, - ) + state.errors = Object.values(state.errorMap).reduce((prev, curr) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (curr === undefined) return prev + if (typeof curr === 'string') { + prev.push(curr) + return prev + } else if (curr && isFormValidationError(curr)) { + prev.push(curr.form) + return prev + } + return prev + }, [] as ValidationError[]) const isFormValid = state.errors.length === 0 const isValid = isFieldsValid && isFormValid const canSubmit = @@ -755,7 +771,10 @@ export class FormApi< promises.push( new Promise>(async (resolve) => { - let rawError!: ValidationError | undefined + let rawError!: + | ValidationError + | FormValidationError + | undefined try { rawError = await new Promise((rawResolve, rawReject) => { setTimeout(async () => { @@ -1215,7 +1234,7 @@ export class FormApi< } } -function normalizeError(rawError?: FormValidationError): { +function normalizeError(rawError?: FormValidationError): { formError: ValidationError fieldErrors?: Partial, ValidationError>> } { @@ -1223,7 +1242,7 @@ function normalizeError(rawError?: FormValidationError): { if (typeof rawError === 'object') { const formError = normalizeError(rawError.form).formError const fieldErrors = rawError.fields - return { formError, fieldErrors } + return { formError, fieldErrors } as never } if (typeof rawError !== 'string') { diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index c3d6034d6..b83de85b7 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -12,11 +12,11 @@ export type Validator = () => { validate( options: { value: Type; api: APITypes }, fn: Fn, - ): ValidationError | FormValidationError + ): ValidationError | FormValidationError validateAsync( options: { value: Type; api: APITypes }, fn: Fn, - ): Promise> + ): Promise> } /** @@ -45,6 +45,13 @@ export type ValidationErrorMap = { [K in ValidationErrorMapKeys]?: ValidationError } +/** + * @private + */ +export type ServerValidationErrorMap = { + [K in ValidationErrorMapKeys]?: ValidationError | FormValidationError +} + /** * @private * diff --git a/packages/zod-form-adapter/src/validator.ts b/packages/zod-form-adapter/src/validator.ts index d2b141911..a46048d5f 100644 --- a/packages/zod-form-adapter/src/validator.ts +++ b/packages/zod-form-adapter/src/validator.ts @@ -28,18 +28,16 @@ export const zodValidator = return { validate({ value, api }, fn) { const transformErrors = - (params.transformErrors ?? api === 'form') - ? defaultFormTransformer - : mapIssuesToSingleString + params.transformErrors ?? + (api === 'form' ? defaultFormTransformer : mapIssuesToSingleString) const result = fn.safeParse(value) if (result.success) return return transformErrors(result.error.issues) }, async validateAsync({ value, api }, fn) { const transformErrors = - (params.transformErrors ?? api === 'form') - ? defaultFormTransformer - : mapIssuesToSingleString + params.transformErrors ?? + (api === 'form' ? defaultFormTransformer : mapIssuesToSingleString) const result = await fn.safeParseAsync(value) if (result.success) return From 4ef738fb84317d07e67ab2c7a4e15c12b3e1dc49 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Wed, 28 Aug 2024 02:41:10 -0700 Subject: [PATCH 04/16] chore: more TSC and lib fixes --- .../src/index.tsx | 2 +- examples/react/zod/src/index.tsx | 23 +++++++++++------ packages/form-core/src/FieldApi.ts | 8 ++---- packages/form-core/src/FormApi.ts | 2 +- .../src/nextjs/createServerValidate.ts | 20 ++++++++++++--- .../src/start/createServerValidate.tsx | 20 ++++++++++++--- packages/react-form/tests/useForm.test.tsx | 22 ++++++++-------- packages/solid-form/tests/createForm.test.tsx | 25 +++++++++---------- 8 files changed, 77 insertions(+), 45 deletions(-) diff --git a/examples/react/field-errors-from-form-validators/src/index.tsx b/examples/react/field-errors-from-form-validators/src/index.tsx index c1892c1ad..c5ee0bac7 100644 --- a/examples/react/field-errors-from-form-validators/src/index.tsx +++ b/examples/react/field-errors-from-form-validators/src/index.tsx @@ -111,7 +111,7 @@ export default function App() { children={([errorMap]) => errorMap.onSubmit ? (
- There was an error on the form: {errorMap.onSubmit} + There was an error on the form: {errorMap.onSubmit?.toString()}
) : null } diff --git a/examples/react/zod/src/index.tsx b/examples/react/zod/src/index.tsx index 104520f62..8d85302b2 100644 --- a/examples/react/zod/src/index.tsx +++ b/examples/react/zod/src/index.tsx @@ -26,14 +26,8 @@ export default function App() { // Do something with form data console.log(value) }, - validators: { - onChange: z.object({ - firstName: z.string().min(2), - lastName: z.string().max(4), - }), - }, // Add a validator to support Zod usage in Form and Field - validatorAdapter: zodValidator({}), + validatorAdapter: zodValidator(), }) return ( @@ -50,6 +44,21 @@ export default function App() { {/* A type-safe field component*/} { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return !value.includes('error') + }, + { + message: "No 'error' allowed in first name", + }, + ), + }} children={(field) => { // Avoid hasty abstractions. Render props are great! return ( diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index eba622a9c..45f64fb8e 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1,9 +1,9 @@ import { Store } from '@tanstack/store' -import { FormValidationError } from './types' import { getAsyncValidatorArray, getBy, getSyncValidatorArray } from './utils' import type { FieldInfo, FieldsErrorMapFromValidator, FormApi } from './FormApi' import type { APITypes, + FormValidationError, UpdateMetaOptions, ValidationCause, ValidationError, @@ -503,11 +503,7 @@ export class FieldApi< value: TValue type: TType // When `api` is 'field', the return type cannot be `FormValidationError` - }): ReturnType>[TType]> extends infer TR - ? TR extends Promise - ? Promise>> - : Exclude> - : never { + }): TType extends 'validate' ? ValidationError : Promise { const adapters = [ this.form.options.validatorAdapter, this.options.validatorAdapter, diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 6cc67bee6..d08f0660a 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1,5 +1,4 @@ import { Store } from '@tanstack/store' -import { ServerValidationErrorMap } from './types' import { deleteBy, functionalUpdate, @@ -15,6 +14,7 @@ import type { FieldApi, FieldMeta } from './FieldApi' import type { APITypes, FormValidationError, + ServerValidationErrorMap, UpdateMetaOptions, ValidationCause, ValidationError, diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts index 13154c975..14ab85f82 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -2,6 +2,7 @@ import { decode } from 'decode-formdata' import { ServerValidateError } from './error' import type { FormOptions, + FormValidationError, ValidationError, Validator, } from '@tanstack/form-core' @@ -18,6 +19,12 @@ interface CreateServerValidateOptions< onServerValidate: OnServerValidateFn } +const isFormValidationError = ( + error: unknown, +): error is FormValidationError => { + return typeof error === 'object' +} + export const createServerValidate = < TFormData, @@ -30,7 +37,7 @@ export const createServerValidate = async (formData: FormData, info?: Parameters[1]) => { const { validatorAdapter, onServerValidate } = defaultOpts - const runValidator = (propsValue: { value: TFormData }) => { + const runValidator = (propsValue: { value: TFormData; api: 'form' }) => { if (validatorAdapter && typeof onServerValidate !== 'function') { return validatorAdapter().validate(propsValue, onServerValidate) } @@ -40,16 +47,23 @@ export const createServerValidate = const values = decode(formData, info) as never as TFormData - const onServerError = runValidator({ value: values }) + const onServerError = runValidator({ value: values, api: 'form' }) if (!onServerError) return + const onServerErrorStr = + onServerError && + typeof onServerError !== 'string' && + isFormValidationError(onServerError) + ? onServerError.form + : onServerError + const formState: ServerFormState = { errorMap: { onServer: onServerError, }, values, - errors: onServerError ? [onServerError] : [], + errors: onServerErrorStr ? [onServerErrorStr] : [], } throw new ServerValidateError({ diff --git a/packages/react-form/src/start/createServerValidate.tsx b/packages/react-form/src/start/createServerValidate.tsx index 7d82edfe4..dc64da417 100644 --- a/packages/react-form/src/start/createServerValidate.tsx +++ b/packages/react-form/src/start/createServerValidate.tsx @@ -3,6 +3,7 @@ import { _tanstackInternalsCookie } from './utils' import { ServerValidateError } from './error' import type { FormOptions, + FormValidationError, ValidationError, Validator, } from '@tanstack/form-core' @@ -22,6 +23,12 @@ interface CreateServerValidateOptions< onServerValidate: OnServerValidateFn } +const isFormValidationError = ( + error: unknown, +): error is FormValidationError => { + return typeof error === 'object' +} + export const createServerValidate = < TFormData, @@ -34,7 +41,7 @@ export const createServerValidate = async (ctx: Ctx, formData: FormData, info?: Parameters[1]) => { const { validatorAdapter, onServerValidate } = defaultOpts - const runValidator = (propsValue: { value: TFormData }) => { + const runValidator = (propsValue: { value: TFormData; api: 'form' }) => { if (validatorAdapter && typeof onServerValidate !== 'function') { return validatorAdapter().validate(propsValue, onServerValidate) } @@ -46,16 +53,23 @@ export const createServerValidate = const data = decode(formData, info) as never as TFormData - const onServerError = runValidator({ value: data }) + const onServerError = runValidator({ value: data, api: 'form' }) if (!onServerError) return + const onServerErrorStr = + onServerError && + typeof onServerError !== 'string' && + isFormValidationError(onServerError) + ? onServerError.form + : onServerError + const formState: ServerFormState = { errorMap: { onServer: onServerError, }, values: data, - errors: onServerError ? [onServerError] : [], + errors: onServerErrorStr ? [onServerErrorStr] : [], } const cookie = await _tanstackInternalsCookie.serialize(formState) diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx index 298d25328..6a7eeb655 100644 --- a/packages/react-form/tests/useForm.test.tsx +++ b/packages/react-form/tests/useForm.test.tsx @@ -190,7 +190,7 @@ describe('useForm', () => { /> )} /> -

{onChangeError}

+

{onChangeError?.toString()}

) } @@ -287,7 +287,7 @@ describe('useForm', () => { onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} /> -

{errors.onChange}

+

{errors.onChange?.toString()}

)} /> @@ -338,8 +338,8 @@ describe('useForm', () => { onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} /> -

{errors.onChange}

-

{errors.onBlur}

+

{errors.onChange?.toString()}

+

{errors.onBlur?.toString()}

)} /> @@ -402,8 +402,8 @@ describe('useForm', () => { )} /> -

{errors.onChange}

-

{errors.onBlur}

+

{errors.onChange?.toString()}

+

{errors.onBlur?.toString()}

) } @@ -462,7 +462,7 @@ describe('useForm', () => { onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} /> -

{errors.onChange}

+

{errors.onChange?.toString()}

)} /> @@ -523,7 +523,7 @@ describe('useForm', () => { )} /> -

{errors.onChange}

+

{errors.onChange?.toString()}

) } @@ -588,8 +588,8 @@ describe('useForm', () => { onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} /> -

{errors.onChange}

-

{errors.onBlur}

+

{errors.onChange?.toString()}

+

{errors.onBlur?.toString()}

)} /> @@ -708,7 +708,7 @@ describe('useForm', () => { )} /> -

{onChangeError}

+

{onChangeError?.toString()}

) } diff --git a/packages/solid-form/tests/createForm.test.tsx b/packages/solid-form/tests/createForm.test.tsx index e002631f4..1a7caca88 100644 --- a/packages/solid-form/tests/createForm.test.tsx +++ b/packages/solid-form/tests/createForm.test.tsx @@ -2,9 +2,8 @@ import { describe, expect, it, vi } from 'vitest' import { render, screen, waitFor } from '@solidjs/testing-library' import { userEvent } from '@testing-library/user-event' import { Show, createSignal, onCleanup } from 'solid-js' -import { createForm } from '../src/index' +import { type ServerValidationErrorMap, createForm } from '../src/index' import { sleep } from './utils' -import type { ValidationErrorMap } from '@tanstack/form-core' const user = userEvent.setup() @@ -211,7 +210,7 @@ describe('createForm', () => { }, })) - const [errors, setErrors] = createSignal() + const [errors, setErrors] = createSignal() onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( @@ -233,7 +232,7 @@ describe('createForm', () => { }) } /> -

{errors()?.onChange}

+

{errors()?.onChange?.toString()}

) }} @@ -267,7 +266,7 @@ describe('createForm', () => { }, })) - const [errors, setErrors] = createSignal() + const [errors, setErrors] = createSignal() onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( @@ -284,8 +283,8 @@ describe('createForm', () => { onBlur={field().handleBlur} onInput={(e) => field().handleChange(e.currentTarget.value)} /> -

{errors()?.onChange}

-

{errors()?.onBlur}

+

{errors()?.onChange?.toString()}

+

{errors()?.onBlur?.toString()}

)} /> @@ -320,7 +319,7 @@ describe('createForm', () => { }, })) - const [errors, setErrors] = createSignal() + const [errors, setErrors] = createSignal() onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( @@ -337,7 +336,7 @@ describe('createForm', () => { onBlur={field().handleBlur} onInput={(e) => field().handleChange(e.currentTarget.value)} /> -

{errors()?.onChange}

+

{errors()?.onChange?.toString()}

)} /> @@ -375,7 +374,7 @@ describe('createForm', () => { }, })) - const [errors, setErrors] = createSignal() + const [errors, setErrors] = createSignal() onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( @@ -392,8 +391,8 @@ describe('createForm', () => { onBlur={field().handleBlur} onInput={(e) => field().handleChange(e.currentTarget.value)} /> -

{errors()?.onChange}

-

{errors()?.onBlur}

+

{errors()?.onChange?.toString()}

+

{errors()?.onBlur?.toString()}

)} /> @@ -437,7 +436,7 @@ describe('createForm', () => { const [errors, setErrors] = createSignal() onCleanup( form.store.subscribe(() => - setErrors(form.state.errorMap.onChange || ''), + setErrors(form.state.errorMap.onChange?.toString() || ''), ), ) From 4d6e7cbb301ec5e11990e6fbaf36d1abf13bce3e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:53:48 +0000 Subject: [PATCH 05/16] ci: apply automated fixes --- .../react/field-errors-from-form-validators/src/index.tsx | 5 ++++- packages/react-form/src/nextjs/createServerValidate.ts | 5 ++++- packages/react-form/src/start/createServerValidate.tsx | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/react/field-errors-from-form-validators/src/index.tsx b/examples/react/field-errors-from-form-validators/src/index.tsx index c5ee0bac7..a195a68c1 100644 --- a/examples/react/field-errors-from-form-validators/src/index.tsx +++ b/examples/react/field-errors-from-form-validators/src/index.tsx @@ -111,7 +111,10 @@ export default function App() { children={([errorMap]) => errorMap.onSubmit ? (
- There was an error on the form: {errorMap.onSubmit?.toString()} + + There was an error on the form:{' '} + {errorMap.onSubmit?.toString()} +
) : null } diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts index bdfbfc9cd..56bfa0323 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -45,7 +45,10 @@ export const createServerValidate = async (formData: FormData, info?: Parameters[1]) => { const { validatorAdapter, onServerValidate } = defaultOpts - const runValidator = async (propsValue: { value: TFormData; api: 'form' }) => { + const runValidator = async (propsValue: { + value: TFormData + api: 'form' + }) => { if (validatorAdapter && typeof onServerValidate !== 'function') { return validatorAdapter().validateAsync(propsValue, onServerValidate) } diff --git a/packages/react-form/src/start/createServerValidate.tsx b/packages/react-form/src/start/createServerValidate.tsx index b6f43fb50..df7c5c2fd 100644 --- a/packages/react-form/src/start/createServerValidate.tsx +++ b/packages/react-form/src/start/createServerValidate.tsx @@ -49,7 +49,10 @@ export const createServerValidate = async (ctx: Ctx, formData: FormData, info?: Parameters[1]) => { const { validatorAdapter, onServerValidate } = defaultOpts - const runValidator = async (propsValue: { value: TFormData; api: 'form' }) => { + const runValidator = async (propsValue: { + value: TFormData + api: 'form' + }) => { if (validatorAdapter && typeof onServerValidate !== 'function') { return validatorAdapter().validateAsync(propsValue, onServerValidate) } From 0c01ebf75e50a14ca4b43260182bb87b925217ae Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Wed, 28 Aug 2024 04:17:41 -0700 Subject: [PATCH 06/16] feat: add Yup support to schema valdiation --- packages/yup-form-adapter/src/validator.ts | 58 +++++++++++++++++----- packages/zod-form-adapter/src/validator.ts | 4 +- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/packages/yup-form-adapter/src/validator.ts b/packages/yup-form-adapter/src/validator.ts index 59aa9cf3f..0f07535e6 100644 --- a/packages/yup-form-adapter/src/validator.ts +++ b/packages/yup-form-adapter/src/validator.ts @@ -1,34 +1,68 @@ -import type { Validator, ValidatorAdapterParams } from '@tanstack/form-core' +import { + type Validator, + type ValidatorAdapterParams, + setBy, +} from '@tanstack/form-core' import type { AnySchema, ValidationError as YupError } from 'yup' type Params = ValidatorAdapterParams +export function prefixSchemaToErrors(error: YupError) { + let schema = {} as object + for (const yupError of error.inner) { + schema = setBy(schema, yupError.path, () => yupError.message) + } + return schema +} + +export function defaultFormTransformer(error: YupError) { + return { + form: mapIssuesToSingleString(error), + fields: prefixSchemaToErrors(error), + } +} + +export const mapIssuesToSingleString = (error: YupError) => + error.errors.join(', ') + +const executeParamsTransformErrors = + (transformErrors: NonNullable) => + (e: YupError) => { + return transformErrors(e.errors) + } + export const yupValidator = (params: Params = {}): Validator => () => { return { - validate({ value }, fn) { + validate({ value, api }, fn) { try { - fn.validateSync(value) + fn.validateSync(value, { abortEarly: false }) return } catch (_e) { const e = _e as YupError - if (params.transformErrors) { - return params.transformErrors(e.errors) - } - return e.errors.join(', ') + const transformErrors = params.transformErrors + ? executeParamsTransformErrors(params.transformErrors) + : api === 'form' + ? defaultFormTransformer + : mapIssuesToSingleString + + return transformErrors(e) } }, - async validateAsync({ value }, fn) { + async validateAsync({ value, api }, fn) { try { await fn.validate(value) return } catch (_e) { const e = _e as YupError - if (params.transformErrors) { - return params.transformErrors(e.errors) - } - return e.errors.join(', ') + const transformErrors = params.transformErrors + ? executeParamsTransformErrors(params.transformErrors) + : api === 'form' + ? defaultFormTransformer + : mapIssuesToSingleString + + return transformErrors(e) } }, } diff --git a/packages/zod-form-adapter/src/validator.ts b/packages/zod-form-adapter/src/validator.ts index a46048d5f..905e66859 100644 --- a/packages/zod-form-adapter/src/validator.ts +++ b/packages/zod-form-adapter/src/validator.ts @@ -4,7 +4,7 @@ import type { ZodIssue, ZodType } from 'zod' type Params = ValidatorAdapterParams -export function prefixSchemaToErorrs(zodErrors: ZodIssue[]) { +export function prefixSchemaToErrors(zodErrors: ZodIssue[]) { let schema = {} as object for (const zodError of zodErrors) { schema = setBy(schema, zodError.path, () => zodError.message) @@ -15,7 +15,7 @@ export function prefixSchemaToErorrs(zodErrors: ZodIssue[]) { export function defaultFormTransformer(zodErrors: ZodIssue[]) { return { form: mapIssuesToSingleString(zodErrors), - fields: prefixSchemaToErorrs(zodErrors), + fields: prefixSchemaToErrors(zodErrors), } } From 24da3d921055adca5279acf337dcc59951f8f760 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Wed, 28 Aug 2024 04:27:35 -0700 Subject: [PATCH 07/16] chore: rename contexts from code review --- packages/form-core/src/FieldApi.ts | 11 +++++------ packages/form-core/src/FormApi.ts | 14 +++++++------- packages/form-core/src/types.ts | 8 ++++---- .../react-form/src/nextjs/createServerValidate.ts | 4 ++-- .../react-form/src/start/createServerValidate.tsx | 4 ++-- packages/solid-form/tests/createForm.test.tsx | 10 +++++----- packages/yup-form-adapter/src/validator.ts | 8 ++++---- packages/zod-form-adapter/src/validator.ts | 8 ++++---- 8 files changed, 33 insertions(+), 34 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 45f64fb8e..9dcbef0c9 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -2,12 +2,11 @@ import { Store } from '@tanstack/store' import { getAsyncValidatorArray, getBy, getSyncValidatorArray } from './utils' import type { FieldInfo, FieldsErrorMapFromValidator, FormApi } from './FormApi' import type { - APITypes, - FormValidationError, UpdateMetaOptions, ValidationCause, ValidationError, ValidationErrorMap, + ValidationSource, Validator, } from './types' import type { AsyncValidator, SyncValidator, Updater } from './utils' @@ -493,7 +492,7 @@ export class FieldApi< TValue extends { value: TData fieldApi: FieldApi - api: APITypes + validationSource: ValidationSource }, TType extends 'validate' | 'validateAsync', >(props: { @@ -550,7 +549,7 @@ export class FieldApi< value: { value: this.state.value, fieldApi: this, - api: 'field', + validationSource: 'field', }, type: 'validate', }) @@ -767,7 +766,7 @@ export class FieldApi< validate: validateObj.validate, value: { value: field.getValue(), - api: 'field', + validationSource: 'field', fieldApi: field, }, type: 'validate', @@ -896,7 +895,7 @@ export class FieldApi< value: field.getValue(), fieldApi: field, signal: controller.signal, - api: 'field', + validationSource: 'field', }, type: 'validateAsync', }), diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index d08f0660a..e3e1c964a 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -12,14 +12,14 @@ import type { Updater } from './utils' import type { DeepKeys, DeepValue } from './util-types' import type { FieldApi, FieldMeta } from './FieldApi' import type { - APITypes, FormValidationError, - ServerValidationErrorMap, + FormValidationErrorMap, UpdateMetaOptions, ValidationCause, ValidationError, ValidationErrorMap, ValidationErrorMapKeys, + ValidationSource, Validator, } from './types' @@ -236,7 +236,7 @@ export type FormState = { /** * The error map for the form itself. */ - errorMap: ServerValidationErrorMap + errorMap: FormValidationErrorMap /** * An internal mechanism used for keeping track of validation logic in a form. */ @@ -462,7 +462,7 @@ export class FormApi< TValue extends { value: TFormData formApi: FormApi - api: APITypes + validationSource: ValidationSource }, TType extends 'validate' | 'validateAsync', >(props: { @@ -488,7 +488,7 @@ export class FormApi< value: { value: this.state.values, formApi: this, - api: 'form', + validationSource: 'form', }, type: 'validate', }) @@ -665,7 +665,7 @@ export class FormApi< value: { value: this.state.values, formApi: this, - api: 'form', + validationSource: 'form', }, type: 'validate', }) @@ -786,7 +786,7 @@ export class FormApi< value: { value: this.state.values, formApi: this, - api: 'form', + validationSource: 'form', signal: controller.signal, }, type: 'validateAsync', diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index b83de85b7..b1b882a85 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -2,7 +2,7 @@ import { type DeepKeys } from './util-types' export type ValidationError = undefined | false | null | string -export type APITypes = 'form' | 'field' +export type ValidationSource = 'form' | 'field' /** * If/when TypeScript supports higher-kinded types, this should not be `unknown` anymore @@ -10,11 +10,11 @@ export type APITypes = 'form' | 'field' */ export type Validator = () => { validate( - options: { value: Type; api: APITypes }, + options: { value: Type; validationSource: ValidationSource }, fn: Fn, ): ValidationError | FormValidationError validateAsync( - options: { value: Type; api: APITypes }, + options: { value: Type; validationSource: ValidationSource }, fn: Fn, ): Promise> } @@ -48,7 +48,7 @@ export type ValidationErrorMap = { /** * @private */ -export type ServerValidationErrorMap = { +export type FormValidationErrorMap = { [K in ValidationErrorMapKeys]?: ValidationError | FormValidationError } diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts index 56bfa0323..f5993443e 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -47,7 +47,7 @@ export const createServerValidate = const runValidator = async (propsValue: { value: TFormData - api: 'form' + validationSource: 'form' }) => { if (validatorAdapter && typeof onServerValidate !== 'function') { return validatorAdapter().validateAsync(propsValue, onServerValidate) @@ -58,7 +58,7 @@ export const createServerValidate = const values = decode(formData, info) as never as TFormData - const onServerError = await runValidator({ value: values, api: 'form' }) + const onServerError = await runValidator({ value: values, validationSource: 'form' }) if (!onServerError) return diff --git a/packages/react-form/src/start/createServerValidate.tsx b/packages/react-form/src/start/createServerValidate.tsx index df7c5c2fd..764b6e518 100644 --- a/packages/react-form/src/start/createServerValidate.tsx +++ b/packages/react-form/src/start/createServerValidate.tsx @@ -51,7 +51,7 @@ export const createServerValidate = const runValidator = async (propsValue: { value: TFormData - api: 'form' + validationSource: 'form' }) => { if (validatorAdapter && typeof onServerValidate !== 'function') { return validatorAdapter().validateAsync(propsValue, onServerValidate) @@ -64,7 +64,7 @@ export const createServerValidate = const data = decode(formData, info) as never as TFormData - const onServerError = await runValidator({ value: data, api: 'form' }) + const onServerError = await runValidator({ value: data, validationSource: 'form' }) if (!onServerError) return diff --git a/packages/solid-form/tests/createForm.test.tsx b/packages/solid-form/tests/createForm.test.tsx index 1a7caca88..8fec7003e 100644 --- a/packages/solid-form/tests/createForm.test.tsx +++ b/packages/solid-form/tests/createForm.test.tsx @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { render, screen, waitFor } from '@solidjs/testing-library' import { userEvent } from '@testing-library/user-event' import { Show, createSignal, onCleanup } from 'solid-js' -import { type ServerValidationErrorMap, createForm } from '../src/index' +import { type FormValidationErrorMap, createForm } from '../src/index' import { sleep } from './utils' const user = userEvent.setup() @@ -210,7 +210,7 @@ describe('createForm', () => { }, })) - const [errors, setErrors] = createSignal() + const [errors, setErrors] = createSignal() onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( @@ -266,7 +266,7 @@ describe('createForm', () => { }, })) - const [errors, setErrors] = createSignal() + const [errors, setErrors] = createSignal() onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( @@ -319,7 +319,7 @@ describe('createForm', () => { }, })) - const [errors, setErrors] = createSignal() + const [errors, setErrors] = createSignal() onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( @@ -374,7 +374,7 @@ describe('createForm', () => { }, })) - const [errors, setErrors] = createSignal() + const [errors, setErrors] = createSignal() onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) return ( diff --git a/packages/yup-form-adapter/src/validator.ts b/packages/yup-form-adapter/src/validator.ts index 0f07535e6..5f1bdace0 100644 --- a/packages/yup-form-adapter/src/validator.ts +++ b/packages/yup-form-adapter/src/validator.ts @@ -35,7 +35,7 @@ export const yupValidator = (params: Params = {}): Validator => () => { return { - validate({ value, api }, fn) { + validate({ value, validationSource }, fn) { try { fn.validateSync(value, { abortEarly: false }) return @@ -43,14 +43,14 @@ export const yupValidator = const e = _e as YupError const transformErrors = params.transformErrors ? executeParamsTransformErrors(params.transformErrors) - : api === 'form' + : validationSource === 'form' ? defaultFormTransformer : mapIssuesToSingleString return transformErrors(e) } }, - async validateAsync({ value, api }, fn) { + async validateAsync({ value, validationSource }, fn) { try { await fn.validate(value) return @@ -58,7 +58,7 @@ export const yupValidator = const e = _e as YupError const transformErrors = params.transformErrors ? executeParamsTransformErrors(params.transformErrors) - : api === 'form' + : validationSource === 'form' ? defaultFormTransformer : mapIssuesToSingleString diff --git a/packages/zod-form-adapter/src/validator.ts b/packages/zod-form-adapter/src/validator.ts index 905e66859..f7d1b0d19 100644 --- a/packages/zod-form-adapter/src/validator.ts +++ b/packages/zod-form-adapter/src/validator.ts @@ -26,18 +26,18 @@ export const zodValidator = (params: Params = {}): Validator => () => { return { - validate({ value, api }, fn) { + validate({ value, validationSource }, fn) { const transformErrors = params.transformErrors ?? - (api === 'form' ? defaultFormTransformer : mapIssuesToSingleString) + (validationSource === 'form' ? defaultFormTransformer : mapIssuesToSingleString) const result = fn.safeParse(value) if (result.success) return return transformErrors(result.error.issues) }, - async validateAsync({ value, api }, fn) { + async validateAsync({ value, validationSource }, fn) { const transformErrors = params.transformErrors ?? - (api === 'form' ? defaultFormTransformer : mapIssuesToSingleString) + (validationSource === 'form' ? defaultFormTransformer : mapIssuesToSingleString) const result = await fn.safeParseAsync(value) if (result.success) return From 519443b44bf821db41bb8e86f7c885dccd4a6708 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 11:28:26 +0000 Subject: [PATCH 08/16] ci: apply automated fixes --- packages/react-form/src/nextjs/createServerValidate.ts | 5 ++++- packages/react-form/src/start/createServerValidate.tsx | 5 ++++- packages/zod-form-adapter/src/validator.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts index f5993443e..dc45691c7 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -58,7 +58,10 @@ export const createServerValidate = const values = decode(formData, info) as never as TFormData - const onServerError = await runValidator({ value: values, validationSource: 'form' }) + const onServerError = await runValidator({ + value: values, + validationSource: 'form', + }) if (!onServerError) return diff --git a/packages/react-form/src/start/createServerValidate.tsx b/packages/react-form/src/start/createServerValidate.tsx index 764b6e518..b3c0041c7 100644 --- a/packages/react-form/src/start/createServerValidate.tsx +++ b/packages/react-form/src/start/createServerValidate.tsx @@ -64,7 +64,10 @@ export const createServerValidate = const data = decode(formData, info) as never as TFormData - const onServerError = await runValidator({ value: data, validationSource: 'form' }) + const onServerError = await runValidator({ + value: data, + validationSource: 'form', + }) if (!onServerError) return diff --git a/packages/zod-form-adapter/src/validator.ts b/packages/zod-form-adapter/src/validator.ts index f7d1b0d19..71ae5042f 100644 --- a/packages/zod-form-adapter/src/validator.ts +++ b/packages/zod-form-adapter/src/validator.ts @@ -29,7 +29,9 @@ export const zodValidator = validate({ value, validationSource }, fn) { const transformErrors = params.transformErrors ?? - (validationSource === 'form' ? defaultFormTransformer : mapIssuesToSingleString) + (validationSource === 'form' + ? defaultFormTransformer + : mapIssuesToSingleString) const result = fn.safeParse(value) if (result.success) return return transformErrors(result.error.issues) @@ -37,7 +39,9 @@ export const zodValidator = async validateAsync({ value, validationSource }, fn) { const transformErrors = params.transformErrors ?? - (validationSource === 'form' ? defaultFormTransformer : mapIssuesToSingleString) + (validationSource === 'form' + ? defaultFormTransformer + : mapIssuesToSingleString) const result = await fn.safeParseAsync(value) if (result.success) return From 79f5261c6ecff115509dea7665d89b4a06a37fa3 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Sep 2024 22:17:05 -0700 Subject: [PATCH 09/16] feat: support form validaton in Valibot --- .../valibot-form-adapter/src/validator.ts | 60 ++++++++++++++----- packages/yup-form-adapter/src/validator.ts | 2 +- packages/zod-form-adapter/src/validator.ts | 10 ++-- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/packages/valibot-form-adapter/src/validator.ts b/packages/valibot-form-adapter/src/validator.ts index 604e2a30c..42bd5c841 100644 --- a/packages/valibot-form-adapter/src/validator.ts +++ b/packages/valibot-form-adapter/src/validator.ts @@ -1,31 +1,63 @@ -import { safeParse, safeParseAsync } from 'valibot' -import type { GenericIssue, GenericSchema, GenericSchemaAsync } from 'valibot' +import { getDotPath, safeParse, safeParseAsync } from 'valibot' +import { setBy } from '@tanstack/form-core' import type { Validator, ValidatorAdapterParams } from '@tanstack/form-core' +import type { + BaseIssue, + GenericIssue, + GenericSchema, + GenericSchemaAsync, +} from 'valibot' type Params = ValidatorAdapterParams +export function prefixSchemaToErrors(errors: Array>) { + let schema = {} as object + for (const valibotError of errors) { + schema = setBy(schema, getDotPath(valibotError), () => valibotError.message) + } + return schema +} + +export function defaultFormTransformer(errors: Array>) { + return { + form: mapIssuesToSingleString(errors), + fields: prefixSchemaToErrors(errors), + } +} + +export const mapIssuesToSingleString = (errors: Array>) => + errors.map((error) => error.message).join(', ') + export const valibotValidator = ( params: Params = {}, ): Validator => () => { return { - validate({ value }, fn) { + validate({ value, validationSource }, fn) { if (fn.async) return - const result = safeParse(fn, value) + const result = safeParse(fn, value, { + abortPipeEarly: false, + }) if (result.success) return - if (params.transformErrors) { - return params.transformErrors(result.issues) - } - return result.issues.map((i) => i.message).join(', ') + const transformErrors = params.transformErrors + ? params.transformErrors + : validationSource === 'form' + ? defaultFormTransformer + : mapIssuesToSingleString + return transformErrors(result.issues) }, - async validateAsync({ value }, fn) { - const result = await safeParseAsync(fn, value) + async validateAsync({ value, validationSource }, fn) { + const result = await safeParseAsync(fn, value, { + abortPipeEarly: false, + }) if (result.success) return - if (params.transformErrors) { - return params.transformErrors(result.issues) - } - return result.issues.map((i) => i.message).join(', ') + const transformErrors = params.transformErrors + ? params.transformErrors + : validationSource === 'form' + ? defaultFormTransformer + : mapIssuesToSingleString + return transformErrors(result.issues) }, } } diff --git a/packages/yup-form-adapter/src/validator.ts b/packages/yup-form-adapter/src/validator.ts index 5f1bdace0..a26700828 100644 --- a/packages/yup-form-adapter/src/validator.ts +++ b/packages/yup-form-adapter/src/validator.ts @@ -52,7 +52,7 @@ export const yupValidator = }, async validateAsync({ value, validationSource }, fn) { try { - await fn.validate(value) + await fn.validate(value, { abortEarly: false }) return } catch (_e) { const e = _e as YupError diff --git a/packages/zod-form-adapter/src/validator.ts b/packages/zod-form-adapter/src/validator.ts index 71ae5042f..b31e144c3 100644 --- a/packages/zod-form-adapter/src/validator.ts +++ b/packages/zod-form-adapter/src/validator.ts @@ -27,24 +27,26 @@ export const zodValidator = () => { return { validate({ value, validationSource }, fn) { + const result = fn.safeParse(value) + if (result.success) return const transformErrors = params.transformErrors ?? (validationSource === 'form' ? defaultFormTransformer : mapIssuesToSingleString) - const result = fn.safeParse(value) - if (result.success) return + return transformErrors(result.error.issues) }, async validateAsync({ value, validationSource }, fn) { + const result = await fn.safeParseAsync(value) + if (result.success) return + const transformErrors = params.transformErrors ?? (validationSource === 'form' ? defaultFormTransformer : mapIssuesToSingleString) - const result = await fn.safeParseAsync(value) - if (result.success) return return transformErrors(result.error.issues) }, } From bf0d94e297c8c222dbc503b9b7140c98d6441bde Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 13 Sep 2024 03:52:47 -0700 Subject: [PATCH 10/16] chore: fix tests for createServerValidate --- packages/solid-form/tests/createForm.test.tsx | 3 ++- .../tests/createServerValidate.spec.ts | 18 +++++++++--------- packages/yup-form-adapter/src/validator.ts | 7 ++----- .../tests/createServerValidate.spec.ts | 2 +- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/solid-form/tests/createForm.test.tsx b/packages/solid-form/tests/createForm.test.tsx index 8fec7003e..b8f1944b9 100644 --- a/packages/solid-form/tests/createForm.test.tsx +++ b/packages/solid-form/tests/createForm.test.tsx @@ -2,8 +2,9 @@ import { describe, expect, it, vi } from 'vitest' import { render, screen, waitFor } from '@solidjs/testing-library' import { userEvent } from '@testing-library/user-event' import { Show, createSignal, onCleanup } from 'solid-js' -import { type FormValidationErrorMap, createForm } from '../src/index' +import { createForm } from '../src/index' import { sleep } from './utils' +import type { FormValidationErrorMap } from '../src/index' const user = userEvent.setup() diff --git a/packages/valibot-form-adapter/tests/createServerValidate.spec.ts b/packages/valibot-form-adapter/tests/createServerValidate.spec.ts index bb60bf86b..22e7a4a70 100644 --- a/packages/valibot-form-adapter/tests/createServerValidate.spec.ts +++ b/packages/valibot-form-adapter/tests/createServerValidate.spec.ts @@ -49,15 +49,15 @@ describe('valibot createServerValidate api', () => { it('should run v.string async validation', async () => { const serverValidate = createServerValidate({ validatorAdapter: valibotValidator(), - onServerValidate: v.pipeAsync( - v.object({ - name: v.string(), - }), - v.checkAsync(async (val) => { - await sleep(1) - return val.name.length > 3 - }, 'Testing 123'), - ), + onServerValidate: v.objectAsync({ + name: v.pipeAsync( + v.string(), + v.checkAsync(async (name) => { + await sleep(1) + return name.length > 3 + }, 'Testing 123'), + ), + }), }) const formData1 = new FormData() diff --git a/packages/yup-form-adapter/src/validator.ts b/packages/yup-form-adapter/src/validator.ts index a26700828..0a8fff40c 100644 --- a/packages/yup-form-adapter/src/validator.ts +++ b/packages/yup-form-adapter/src/validator.ts @@ -1,8 +1,5 @@ -import { - type Validator, - type ValidatorAdapterParams, - setBy, -} from '@tanstack/form-core' +import { setBy } from '@tanstack/form-core' +import type { Validator, ValidatorAdapterParams } from '@tanstack/form-core' import type { AnySchema, ValidationError as YupError } from 'yup' type Params = ValidatorAdapterParams diff --git a/packages/yup-form-adapter/tests/createServerValidate.spec.ts b/packages/yup-form-adapter/tests/createServerValidate.spec.ts index 79fec11a0..801aacad1 100644 --- a/packages/yup-form-adapter/tests/createServerValidate.spec.ts +++ b/packages/yup-form-adapter/tests/createServerValidate.spec.ts @@ -100,7 +100,7 @@ describe('yup createServerValidate api', () => { formData1.append('name', 'aa') expect( await serverValidate(formData1).catch((e) => e.formState.errors), - ).toEqual(['You must have a length of at least 3']) + ).toEqual(['You must have a length of at least 3, UUID']) const formData2 = new FormData() formData2.append('name', 'aaa') From b83bd8324e3f8f3d2c47f61ad2f69aebccb893ae Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 13 Sep 2024 04:05:04 -0700 Subject: [PATCH 11/16] docs: add docs for validation logic --- docs/framework/angular/guides/validation.md | 32 +++++ docs/framework/react/guides/validation.md | 36 +++++ docs/framework/solid/guides/validation.md | 38 +++++ docs/framework/vue/guides/validation.md | 147 +++++++++++++------- 4 files changed, 199 insertions(+), 54 deletions(-) diff --git a/docs/framework/angular/guides/validation.md b/docs/framework/angular/guides/validation.md index b50560561..d5b2f44b2 100644 --- a/docs/framework/angular/guides/validation.md +++ b/docs/framework/angular/guides/validation.md @@ -469,6 +469,38 @@ export class AppComponent { } ``` +### Form Level Adapter Validation + +You can also use the adapter at the form level: + +```typescript +import { zodValidator } from '@tanstack/zod-form-adapter' +import { z } from 'zod' + +// ... + +const form = injectForm({ + validatorAdapter: zodValidator(), + validators: { + onChange: z.object({ + age: z.number().gte(13, 'You must be 13 to make an account'), + }), + }, +}) +``` + +If you use the adapter at the form level, it will pass the validation to the fields of the same name. + +This means that: + +```html + + + +``` + +Will still display the error message from the form-level validation. + ## Preventing invalid forms from being submitted The `onChange`, `onBlur` etc... callbacks are also run when the form is submitted and the submission is blocked if the form is invalid. diff --git a/docs/framework/react/guides/validation.md b/docs/framework/react/guides/validation.md index e5bfdc295..0f3a9833d 100644 --- a/docs/framework/react/guides/validation.md +++ b/docs/framework/react/guides/validation.md @@ -477,6 +477,42 @@ These adapters also support async operations using the proper property names: /> ``` +### Form Level Adapter Validation + +You can also use the adapter at the form level: + +```tsx +import { zodValidator } from '@tanstack/zod-form-adapter' +import { z } from 'zod' + +// ... + +const form = useForm({ + validatorAdapter: zodValidator(), + validators: { + onChange: z.object({ + age: z.number().gte(13, 'You must be 13 to make an account'), + }), + }, +}) +``` + +If you use the adapter at the form level, it will pass the validation to the fields of the same name. + +This means that: + +```tsx + + { + return <>{/* ... */} + }} +/> +``` + +Will still display the error message from the form-level validation. + ## Preventing invalid forms from being submitted The `onChange`, `onBlur` etc... callbacks are also run when the form is submitted and the submission is blocked if the form is invalid. diff --git a/docs/framework/solid/guides/validation.md b/docs/framework/solid/guides/validation.md index 354b721bb..50ef40a7a 100644 --- a/docs/framework/solid/guides/validation.md +++ b/docs/framework/solid/guides/validation.md @@ -376,6 +376,44 @@ These adapters also support async operations using the proper property names: /> ``` +### Form Level Adapter Validation + + +You can also use the adapter at the form level: + +```tsx +import { zodValidator } from '@tanstack/zod-form-adapter' +import { z } from 'zod' + +// ... + +const form = createForm(() => ({ + validatorAdapter: zodValidator(), + validators: { + onChange: z.object({ + age: z.number().gte(13, 'You must be 13 to make an account'), + }), + }, +})) +``` + +If you use the adapter at the form level, it will pass the validation to the fields of the same name. + +This means that: + +```tsx + + { + return <>{/* ... */} + }} +/> +``` + +Will still display the error message from the form-level validation. + + ## Preventing invalid forms from being submitted The `onChange`, `onBlur` etc... callbacks are also run when the form is submitted and the submission is blocked if the form is invalid. diff --git a/docs/framework/vue/guides/validation.md b/docs/framework/vue/guides/validation.md index 6819a2d6f..a9aea2c5f 100644 --- a/docs/framework/vue/guides/validation.md +++ b/docs/framework/vue/guides/validation.md @@ -18,20 +18,20 @@ Here is an example: ```vue