diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts index 8ffa3fc02..e372e3064 100644 --- a/examples/react/next-server-actions/src/app/action.ts +++ b/examples/react/next-server-actions/src/app/action.ts @@ -10,7 +10,16 @@ const serverValidate = createServerValidate({ ...formOpts, onServerValidate: ({ value }) => { if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' + return { + fields: { + age: 'Field level error: You must be at least 12 to sign up', + }, + form: '', // Can be omitted or be a string for form-level errors + } + } + + if (value.score <= 90) { + return 'Form level error: Score must be over 90' // Also valid; Form-level } }, }) diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index 380455588..d9f81bf5c 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -49,6 +49,33 @@ export const ClientComp = () => { ) }} + + + value <= 80 + ? 'Client validation: Score must be over 80' + : undefined, + }} + > + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors.map((error) => ( +

{error}

+ ))} +
+ ) + }} +
+ [formState.canSubmit, formState.isSubmitting]} > diff --git a/examples/react/next-server-actions/src/app/shared-code.ts b/examples/react/next-server-actions/src/app/shared-code.ts index 31d9980f3..acf80e361 100644 --- a/examples/react/next-server-actions/src/app/shared-code.ts +++ b/examples/react/next-server-actions/src/app/shared-code.ts @@ -2,7 +2,7 @@ import { formOptions } from '@tanstack/react-form/nextjs' export const formOpts = formOptions({ defaultValues: { - firstName: '', age: 0, + score: 0, }, }) diff --git a/examples/react/remix/app/routes/_index/route.tsx b/examples/react/remix/app/routes/_index/route.tsx index 81764938c..9afeea417 100644 --- a/examples/react/remix/app/routes/_index/route.tsx +++ b/examples/react/remix/app/routes/_index/route.tsx @@ -15,6 +15,7 @@ const formOpts = formOptions({ defaultValues: { firstName: '', age: 0, + score: 0, }, }) @@ -22,7 +23,16 @@ const serverValidate = createServerValidate({ ...formOpts, onServerValidate: ({ value }) => { if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' + return { + fields: { + age: 'Field level error: You must be at least 12 to sign up', + }, + form: 'Form level error: You must be at least 12 to sign up', // Can be omitted or be a string for form-level errors + } + } + + if (value.score <= 90) { + return 'Form level error: Score must be over 90' // Also valid; Form-level } }, }) @@ -79,7 +89,32 @@ export default function Index() { return (
field.handleChange(e.target.valueAsNumber)} + /> + {field.state.meta.errors.map((error) => ( +

{error}

+ ))} +
+ ) + }} + + + value < 80 + ? 'Client validation: You must be at least 80' + : undefined, + }} + > + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 926be4eef..8954ffb7b 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1287,6 +1287,35 @@ export class FormApi< this.options.transform?.fn(newObj) state = newObj.state this.prevTransformArray = transformArray + + const serverErrorMap = this.store.state.errorMap['onServer'] as + | Record + | undefined + + if (!serverErrorMap) return state + + batch(() => { + void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( + (field) => { + const fieldInstance = field.instance + + if (!fieldInstance) return + + if (fieldInstance.name in serverErrorMap) { + fieldInstance.setMeta((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + onServer: serverErrorMap[fieldInstance.name], + }, + })) + fieldInstance.mount() + } + + this.validateField(fieldInstance.name, 'server') + }, + ) + }) } return state @@ -2239,6 +2268,7 @@ export class FormApi< // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition ...prev?.errorMap, onMount: undefined, + onServer: undefined, }, })) } diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts index 903a6f520..8976ffea7 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -106,18 +106,36 @@ export const createServerValidate = if (!onServerError) return values - const onServerErrorVal = ( - isGlobalFormValidationError(onServerError) - ? onServerError.form - : onServerError - ) as UnwrapFormAsyncValidateOrFn + let onServerErrorVal = undefined + let onServerErrorValFields = undefined + // ^ if fields is defined, this object is { fieldName: "Error Message" } + + // checks if it's an object with 'fields' + if (isGlobalFormValidationError(onServerError)) { + onServerErrorVal = + onServerError.form as UnwrapFormAsyncValidateOrFn + onServerErrorValFields = + onServerError.fields as UnwrapFormAsyncValidateOrFn + } else { + onServerErrorVal = onServerError as UnwrapFormAsyncValidateOrFn + } + + // Extract string values from errors if they're in object format + let errorsArray: any[] = [] + if (onServerErrorVal) { + if (Array.isArray(onServerErrorVal)) { + errorsArray = onServerErrorVal.map((err) => { + return err + }) + } else errorsArray = [onServerErrorVal] + } const formState: ServerFormState = { errorMap: { - onServer: onServerError, + onServer: onServerErrorValFields, }, values, - errors: onServerErrorVal ? [onServerErrorVal] : [], + errors: errorsArray, } throw new ServerValidateError({ diff --git a/packages/react-form/src/remix/createServerValidate.ts b/packages/react-form/src/remix/createServerValidate.ts index 903a6f520..8976ffea7 100644 --- a/packages/react-form/src/remix/createServerValidate.ts +++ b/packages/react-form/src/remix/createServerValidate.ts @@ -106,18 +106,36 @@ export const createServerValidate = if (!onServerError) return values - const onServerErrorVal = ( - isGlobalFormValidationError(onServerError) - ? onServerError.form - : onServerError - ) as UnwrapFormAsyncValidateOrFn + let onServerErrorVal = undefined + let onServerErrorValFields = undefined + // ^ if fields is defined, this object is { fieldName: "Error Message" } + + // checks if it's an object with 'fields' + if (isGlobalFormValidationError(onServerError)) { + onServerErrorVal = + onServerError.form as UnwrapFormAsyncValidateOrFn + onServerErrorValFields = + onServerError.fields as UnwrapFormAsyncValidateOrFn + } else { + onServerErrorVal = onServerError as UnwrapFormAsyncValidateOrFn + } + + // Extract string values from errors if they're in object format + let errorsArray: any[] = [] + if (onServerErrorVal) { + if (Array.isArray(onServerErrorVal)) { + errorsArray = onServerErrorVal.map((err) => { + return err + }) + } else errorsArray = [onServerErrorVal] + } const formState: ServerFormState = { errorMap: { - onServer: onServerError, + onServer: onServerErrorValFields, }, values, - errors: onServerErrorVal ? [onServerErrorVal] : [], + errors: errorsArray, } throw new ServerValidateError({