From e0bc9a96522b64eff44ecc71db231a61c4e93ef2 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:29:40 +0200 Subject: [PATCH 1/2] chore: remove redundant parseIssues usage in examples --- examples/angular/field-groups/src/bound-field.component.ts | 5 +++-- examples/lit/field-groups/src/field-bounds.ts | 4 ++-- examples/preact/field-groups/src/fieldGroups/fieldBounds.tsx | 4 ++-- examples/react/field-groups/src/fieldGroups/fieldBounds.tsx | 4 ++-- examples/solid/field-groups/src/fieldGroups/fieldBounds.tsx | 4 ++-- .../field-groups/src/fieldGroups/UpperBoundFields.svelte | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/angular/field-groups/src/bound-field.component.ts b/examples/angular/field-groups/src/bound-field.component.ts index 609443897..6f991e8f3 100644 --- a/examples/angular/field-groups/src/bound-field.component.ts +++ b/examples/angular/field-groups/src/bound-field.component.ts @@ -50,9 +50,9 @@ export class BoundFieldComponent { watchFields: this.fields().lowerBound ? [this.fields().lowerBound!] : undefined, - run: ({ value, parseIssues }: any) => { + run: ({ value }: any) => { const valueResult = boundsSchema.safeParse(value) - if (!valueResult.success) return parseIssues(valueResult.error.issues) + if (!valueResult.success) return valueResult.error.issues const lowerName = this.fields().lowerBound if (!lowerName) return const lowerResult = boundsSchema.safeParse( @@ -61,6 +61,7 @@ export class BoundFieldComponent { if (lowerResult.success && valueResult.data < lowerResult.data) { return 'Upper bound must be greater than lower bound' } + return undefined }, }, ]) diff --git a/examples/lit/field-groups/src/field-bounds.ts b/examples/lit/field-groups/src/field-bounds.ts index 9bc4aec3a..ad70124a0 100644 --- a/examples/lit/field-groups/src/field-bounds.ts +++ b/examples/lit/field-groups/src/field-bounds.ts @@ -55,9 +55,9 @@ function upperBoundField(props: { { triggers: ['change'], watchFields: ['lowerBound'], - run: ({ value, parseIssues }) => { + run: ({ value }) => { const upper = boundsSchema.safeParse(value) - if (!upper.success) return parseIssues(upper.error.issues) + if (!upper.success) return upper.error.issues const lower = boundsSchema.safeParse( props.fields.getFieldValue('lowerBound'), ) diff --git a/examples/preact/field-groups/src/fieldGroups/fieldBounds.tsx b/examples/preact/field-groups/src/fieldGroups/fieldBounds.tsx index 196015c3b..0eb10f278 100644 --- a/examples/preact/field-groups/src/fieldGroups/fieldBounds.tsx +++ b/examples/preact/field-groups/src/fieldGroups/fieldBounds.tsx @@ -78,11 +78,11 @@ function UpperBoundFieldComponent(props: UpperBoundFieldProps) { // `watchFields` also uses virtual names. The wrapper rewrites // `lowerBound` to the caller's concrete path, such as `minPrice`. watchFields: ['lowerBound'], - run: ({ value, parseIssues }) => { + run: ({ value }) => { const upperBoundResult = boundsSchema.safeParse(value) if (!upperBoundResult.success) { - return parseIssues(upperBoundResult.error.issues) + return upperBoundResult.error.issues } const lowerBoundResult = boundsSchema.safeParse( diff --git a/examples/react/field-groups/src/fieldGroups/fieldBounds.tsx b/examples/react/field-groups/src/fieldGroups/fieldBounds.tsx index c7d9575b4..d1eb30abd 100644 --- a/examples/react/field-groups/src/fieldGroups/fieldBounds.tsx +++ b/examples/react/field-groups/src/fieldGroups/fieldBounds.tsx @@ -78,11 +78,11 @@ function UpperBoundFieldComponent(props: UpperBoundFieldProps) { // `watchFields` also uses virtual names. The wrapper rewrites // `lowerBound` to the caller's concrete path, such as `minPrice`. watchFields: ['lowerBound'], - run: ({ value, parseIssues }) => { + run: ({ value }) => { const upperBoundResult = boundsSchema.safeParse(value) if (!upperBoundResult.success) { - return parseIssues(upperBoundResult.error.issues) + return upperBoundResult.error.issues } const lowerBoundResult = boundsSchema.safeParse( diff --git a/examples/solid/field-groups/src/fieldGroups/fieldBounds.tsx b/examples/solid/field-groups/src/fieldGroups/fieldBounds.tsx index cc204509f..9c6127e89 100644 --- a/examples/solid/field-groups/src/fieldGroups/fieldBounds.tsx +++ b/examples/solid/field-groups/src/fieldGroups/fieldBounds.tsx @@ -60,11 +60,11 @@ function UpperBoundFieldComponent(props: UpperBoundFieldProps) { { triggers: ['change'], watchFields: ['lowerBound'], - run: ({ value, parseIssues }) => { + run: ({ value }) => { const upperBoundResult = boundsSchema.safeParse(value) if (!upperBoundResult.success) { - return parseIssues(upperBoundResult.error.issues) + return upperBoundResult.error.issues } const lowerBoundResult = boundsSchema.safeParse( diff --git a/examples/svelte/field-groups/src/fieldGroups/UpperBoundFields.svelte b/examples/svelte/field-groups/src/fieldGroups/UpperBoundFields.svelte index 32922db6e..22565e2a1 100644 --- a/examples/svelte/field-groups/src/fieldGroups/UpperBoundFields.svelte +++ b/examples/svelte/field-groups/src/fieldGroups/UpperBoundFields.svelte @@ -17,10 +17,10 @@ { triggers: ['change'], watchFields: ['lowerBound'], - run: ({ value, parseIssues }) => { + run: ({ value }) => { const upperBoundResult = boundsSchema.safeParse(value) if (!upperBoundResult.success) { - return parseIssues(upperBoundResult.error.issues) + return upperBoundResult.error.issues } const lowerBoundResult = boundsSchema.safeParse( fields.getFieldValue('lowerBound'), From 6c2d57702250fb60ee13421f4695f5438a4453d5 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:48:54 +0200 Subject: [PATCH 2/2] BREAKING: deprecate parseIssues in favor of createErrorMap --- .../form-core/src/FormApi/FormApi.public.ts | 57 ++++++++----- .../form-core/src/FormApi/handleSubmit.lib.ts | 23 ++++-- .../src/FormGroupApi/FormGroupApi.lib.ts | 7 +- packages/form-core/src/createErrorMap.lib.ts | 23 ++++++ packages/form-core/src/ssr.lib.ts | 7 +- packages/form-core/src/types.public.ts | 1 + packages/form-core/src/utils.lib.ts | 19 ++++- packages/form-core/src/validation.public.ts | 79 ++++++------------- .../form-core/src/validation/execution.lib.ts | 17 +--- .../form-core/src/validation/mount.lib.ts | 18 +---- .../form-core/src/validation/pipeline.lib.ts | 11 +-- .../tests/FormApi/submission-handling.spec.ts | 4 +- .../tests/FormApi/validation.spec.ts | 6 +- .../tests/FormGroupApi/FormGroupApi.spec.ts | 4 +- .../tests/validation-pipeline.test.ts | 6 +- .../form-core/tests/validation-public.test.ts | 38 --------- packages/form-core/tests/validation.test-d.ts | 51 +++--------- 17 files changed, 145 insertions(+), 226 deletions(-) create mode 100644 packages/form-core/src/createErrorMap.lib.ts diff --git a/packages/form-core/src/FormApi/FormApi.public.ts b/packages/form-core/src/FormApi/FormApi.public.ts index f7824c090..fbc63e1b2 100644 --- a/packages/form-core/src/FormApi/FormApi.public.ts +++ b/packages/form-core/src/FormApi/FormApi.public.ts @@ -9,6 +9,7 @@ import type { ParsedStandardSchemaIssues, ToFormErrorTypes, ToFormOutputs, + ValidationErrorMap, } from '../validation.public' import type { StandardSchemaV1Issue } from '../standardSchema.public' import type { FormListeners } from '../listeners.public' @@ -18,9 +19,9 @@ import type { ServerFormState } from '../ssr.public' import type { SUBMIT_ERROR } from './handleSubmit.lib' /** - * A validation error wrapped for processing as an `onSubmit` failure. + * A validation error recognized as an `onSubmit` failure. * - * Create this value with the `createValidationError` or `parseIssues` helper + * Create this value with the `createValidationError` or `createErrorMap` helper * provided to `onSubmit`, then return it from the callback so its form- and * field-level errors are added to validation state. * @@ -63,17 +64,29 @@ export type CreateValidationErrorFn = < ) => OnSubmitError /** - * Converts Standard Schema issues into an `onSubmit` validation error. + * Creates an error map that can be returned directly from `onSubmit`. * - * Issue paths are mapped to fields in the submitted value. Return the result - * from `onSubmit` to add the parsed errors to validation state. + * Build a mutable map or pass Standard Schema issues to route them using the + * submitted values. Narrow possibly undefined issue lists before calling this + * helper. Empty maps indicate success. * - * @param issues - The Standard Schema issues to convert. * @typeParam TFormData - Library-managed. Do not specify explicitly. */ -export type ParseSubmitIssuesFn = ( - issues: ReadonlyArray, -) => OnSubmitError> +export interface CreateSubmitErrorMapFn { + /** Creates a mutable submission error map with an empty `fields` object. */ + (): ValidationErrorMap & + OnSubmitError> + /** Reuses the supplied map, adding an empty `fields` object if it is missing. */ + ( + initial: Partial>, + ): ValidationErrorMap & + OnSubmitError> + /** Routes issues while preserving Standard Schema issue arrays in the result. */ + ( + issues: ReadonlyArray, + ): ParsedStandardSchemaIssues & + OnSubmitError> +} /** * Context passed to `onSubmit` after submission validation succeeds. @@ -147,7 +160,7 @@ export interface FormSubmitContext< */ createValidationError: CreateValidationErrorFn /** - * Parses Standard Schema issues into an error returnable from `onSubmit`. + * Builds an error map or routes Standard Schema issues for `onSubmit`. * * Issue paths are mapped to fields in the submitted value. Return the parsed * error from `onSubmit` to add those errors to validation state. @@ -156,24 +169,24 @@ export interface FormSubmitContext< * ```ts * { * // ... - * onSubmit: async ({ value, parseIssues }) => { + * onSubmit: async ({ value, createErrorMap }) => { * const result = zodSchema.safeParse(value) * * if (!result.success) { - * return parseIssues(result.error.issues) + * return createErrorMap(result.error.issues) * } * }, * } * ``` */ - parseIssues: ParseSubmitIssuesFn + createErrorMap: CreateSubmitErrorMapFn } /** * Handles a form submission after submission validation succeeds. * * A returned promise is awaited before submission finishes. Return an error - * created by the submission context's `createValidationError` or `parseIssues` + * created by the submission context's `createValidationError` or `createErrorMap` * helper to mark the submission as invalid. * * @typeParam TFormData - Library-managed. Do not specify explicitly. @@ -365,7 +378,7 @@ export interface FormOptions< /** * Called after submission validation succeeds. * - * Return an error created with `createValidationError` or `parseIssues` to + * Return an error created with `createValidationError` or `createErrorMap` to * mark the submission as invalid. A returned promise is awaited before * submission finishes. If the callback throws, `onSubmitInvalid` is called. * @@ -584,15 +597,16 @@ export interface FormResetOptions { * Registered fields are marked touched, field validators run before form * validators, and `onSubmit` is awaited only when validation succeeds. * Validation error results and errors returned by `onSubmit` through - * `createValidationError` are stored as error state. `onSubmitInvalid` is - * awaited after a failed attempt. + * `createValidationError` or `createErrorMap` are stored as error state. + * `onSubmitInvalid` is awaited after a failed attempt. * * Calls made while an attempt is in progress return the same promise instead * of starting another attempt. * * @returns A promise resolving to the error results produced by field and * form validation, plus any validation error returned by `onSubmit` through - * `createValidationError`. The array is empty if none are produced. + * `createValidationError` or `createErrorMap`. The array is empty if none are + * produced. * @typeParam TFormData - Library-managed. Do not specify explicitly. */ export type HandleSubmitFn = () => Promise< @@ -678,15 +692,16 @@ export interface FormApi< * Registered fields are marked touched, field validators run before form * validators, and `onSubmit` is awaited only when validation succeeds. * Validation error results and errors returned by `onSubmit` through - * `createValidationError` are stored as error state. `onSubmitInvalid` is - * awaited after a failed attempt. + * `createValidationError` or `createErrorMap` are stored as error state. + * `onSubmitInvalid` is awaited after a failed attempt. * * Calls made while an attempt is in progress return the same promise instead * of starting another attempt. * * The returned promise resolves to the error results produced by field and * form validation, plus any validation error returned by `onSubmit` through - * `createValidationError`. The array is empty if none are produced. + * `createValidationError` or `createErrorMap`. The array is empty if none are + * produced. */ handleSubmit: HandleSubmitFn /** diff --git a/packages/form-core/src/FormApi/handleSubmit.lib.ts b/packages/form-core/src/FormApi/handleSubmit.lib.ts index 634755d40..3fed6d465 100644 --- a/packages/form-core/src/FormApi/handleSubmit.lib.ts +++ b/packages/form-core/src/FormApi/handleSubmit.lib.ts @@ -4,17 +4,19 @@ import { isValidationErrorMap, parseValidationResult, } from '../validation' -import { parseStandardSchemaIssues } from '../standardSchema.lib' +import { createErrorMapFactory } from '../createErrorMap.lib' +import type { StandardSchemaV1Issue } from '../standardSchema.public' import type { InternalFormApi } from './FormApi.lib' import type { FormValidateResult, FormValidationError, ValidationErrorInput, + ValidationErrorMap, } from '../validation.public' import type { + CreateSubmitErrorMapFn, CreateValidationErrorFn, OnSubmitError, - ParseSubmitIssuesFn, } from './FormApi.public' export const SUBMIT_ERROR = Symbol('SUBMIT_ERROR') @@ -39,12 +41,17 @@ function createSubmitError>( return { [SUBMIT_ERROR]: error } } -function createParseIssues( +function createSubmitErrorMapFactory( value: TFormData, -): ParseSubmitIssuesFn { - return (issues) => { - return createSubmitError(parseStandardSchemaIssues(issues, value, 'form')) - } +): CreateSubmitErrorMapFn { + const createErrorMap = createErrorMapFactory(value) + + return ((initial) => { + const map = createErrorMap(initial as never) + // Read the map itself so edits made after creation are included. + Object.defineProperty(map, SUBMIT_ERROR, { value: map }) + return map + }) as CreateSubmitErrorMapFn } export async function runSubmissionProcess( @@ -168,7 +175,7 @@ export async function runSubmissionProcess( validatorOutputs, value, createValidationError, - parseIssues: createParseIssues(value), + createErrorMap: createSubmitErrorMapFactory(value), }) if (hasResettedFormDuringSubmit()) { diff --git a/packages/form-core/src/FormGroupApi/FormGroupApi.lib.ts b/packages/form-core/src/FormGroupApi/FormGroupApi.lib.ts index b8f563b00..9d3d7b5d6 100644 --- a/packages/form-core/src/FormGroupApi/FormGroupApi.lib.ts +++ b/packages/form-core/src/FormGroupApi/FormGroupApi.lib.ts @@ -18,8 +18,7 @@ import { deriveFromBaseFieldMeta, hasFieldMetaErrors, } from '../FieldApi/fieldState.lib' -import { parseStandardSchemaIssues } from '../standardSchema.lib' -import { createErrorMap } from '../validation.public' +import { createErrorMapFactory } from '../createErrorMap.lib' import { reconcileValidatorInstances } from '../ValidatorInstance.lib' import { resolveDefaultOptions } from '../defaultOptions.lib' import type { FormApi } from '../FormApi/FormApi.public' @@ -685,10 +684,8 @@ export class InternalFormGroupApi< triggerFieldApi: opts?.triggerFieldApi, signal: ctx.signal, value: this.value, - createErrorMap, + createErrorMap: createErrorMapFactory(this.value), createOutput, - parseIssues: (issues) => - parseStandardSchemaIssues(issues, this.value, 'form'), }), onResult: (result) => { this._processValidationResult(result, signal) diff --git a/packages/form-core/src/createErrorMap.lib.ts b/packages/form-core/src/createErrorMap.lib.ts new file mode 100644 index 000000000..1326592cc --- /dev/null +++ b/packages/form-core/src/createErrorMap.lib.ts @@ -0,0 +1,23 @@ +import { parseStandardSchemaIssues } from './standardSchema.lib' +import { isReadonlyArray } from './utils.lib' +import type { CreateErrorMapFn } from './validation.public' + +type TupleNarrower = (arg: any) => arg is ReadonlyArray + +/** + * Standard schema needs path parsing, so the error map function needs context of form values. + */ +export function createErrorMapFactory( + value: TFormData, +): CreateErrorMapFn { + const createErrorMap = ((initial) => { + if (isReadonlyArray(initial)) { + return parseStandardSchemaIssues(initial, value, 'form') + } + const map = initial ?? {} + map.fields ??= {} + return map + }) as CreateErrorMapFn + + return createErrorMap +} diff --git a/packages/form-core/src/ssr.lib.ts b/packages/form-core/src/ssr.lib.ts index ea105f8b1..456fcddc3 100644 --- a/packages/form-core/src/ssr.lib.ts +++ b/packages/form-core/src/ssr.lib.ts @@ -2,10 +2,9 @@ import { batch } from '@tanstack/store' import { createOutput } from './validationOutput.lib' import { defaultInternalBaseFieldMeta } from './FieldApi/fieldState.lib' import { visitAllFormFields } from './FieldApi/fieldTraversal.lib' -import { parseStandardSchemaIssues } from './standardSchema.lib' import { evaluate } from './utils.lib' import { runValidatorPipeline } from './validation' -import { createErrorMap } from './validation.public' +import { createErrorMapFactory } from './createErrorMap.lib' import { devtools } from './devtoolsBridge.lib' import { reconcileValidatorInstances } from './ValidatorInstance.lib' import type { FormOptions } from './FormApi/FormApi.public' @@ -155,10 +154,8 @@ export async function validateServerValues< signal: ctx.signal, formApi: undefined, value: values, - createErrorMap, + createErrorMap: createErrorMapFactory(values), createOutput, - parseIssues: (issues) => - parseStandardSchemaIssues(issues, values, 'form'), }), scope: 'form', }) diff --git a/packages/form-core/src/types.public.ts b/packages/form-core/src/types.public.ts index 0a3f0e913..066f6c910 100644 --- a/packages/form-core/src/types.public.ts +++ b/packages/form-core/src/types.public.ts @@ -31,6 +31,7 @@ export interface FieldUpdateOptions { } export type OneOrMany = TValue | Array +export type ReadonlyOneOrMany = TValue | ReadonlyArray /** * A field API that preserves its value type while erasing its name, error, and diff --git a/packages/form-core/src/utils.lib.ts b/packages/form-core/src/utils.lib.ts index d1e0005cb..60f7efa72 100644 --- a/packages/form-core/src/utils.lib.ts +++ b/packages/form-core/src/utils.lib.ts @@ -1,7 +1,11 @@ import type { AnyInternalFieldApi } from './FieldApi/FieldApi.lib' // type -import type { FieldUpdateOptions, OneOrMany, Updater } from './types.public' +import type { + FieldUpdateOptions, + ReadonlyOneOrMany, + Updater, +} from './types.public' import type { InternalFieldUpdateOptions, ResolvedInternalFieldUpdateOptions, @@ -199,11 +203,11 @@ export function callUpdater(updater: Updater, object: T): T { } export function normalizeToArray( - input: null | undefined | OneOrMany, -): Array { + input: null | undefined | ReadonlyOneOrMany, +): ReadonlyArray { if (Array.isArray(input)) return input if (input === null || input === undefined) return [] - return [input] + return [input as T] } // TODO remove and import from store: @@ -322,3 +326,10 @@ export function isPromiseLike( typeof value.then === 'function' ) } + +/** + * Just `Array.isArray`, but better typed. We *could* make this with declaration merging, + * but I'd prefer to avoid introducing edge cases. + */ +export const isReadonlyArray: (arg: any) => arg is ReadonlyArray = + Array.isArray diff --git a/packages/form-core/src/validation.public.ts b/packages/form-core/src/validation.public.ts index d4651aa69..02e3ce3ae 100644 --- a/packages/form-core/src/validation.public.ts +++ b/packages/form-core/src/validation.public.ts @@ -6,7 +6,7 @@ import type { StandardSchemaV1, StandardSchemaV1Issue, } from './standardSchema.public' -import type { OneOrMany } from './types.public' +import type { OneOrMany, ReadonlyOneOrMany } from './types.public' import type { VALIDATION_OUTPUT } from './validationOutput.lib' /** @@ -585,7 +585,7 @@ export type ValidationErrorValue = ValidationIssue | string /** One or more issue objects after string errors have been normalized. */ export type ValidationError = OneOrMany /** One or more errors accepted from a validator, including string messages. */ -export type ValidationErrorInput = OneOrMany +export type ValidationErrorInput = ReadonlyOneOrMany /** * Errors owned by a form or group and errors routed to its fields. @@ -600,34 +600,23 @@ export interface ValidationErrorMap { } /** - * Creates a mutable map of form errors and errors for individual field paths. + * Creates an error map with field paths inferred from the callback's value shape. * - * Pass the completed map back from a form or group validator to report the errors. - * - * @param initial - An existing map to reuse. A missing `fields` object is added to it. - * @returns The initial map, if supplied, or a new map with an empty `fields` object. - * @typeParam TFormData - The form or group value shape whose field paths are - * accepted by the map. Specify it when calling this helper directly to restrict - * `fields` to that shape's paths. Without a known value shape, field paths are - * unrestricted. The validator context's `createErrorMap` already uses the form - * or group shape and needs no type argument. - */ -export function createErrorMap( - initial?: Partial>, -): ValidationErrorMap { - if (!initial) return { fields: {} } - - initial.fields ??= {} - return initial as ValidationErrorMap -} - -/** - * Creates a mutable error map with field paths inferred from the validator's value shape. + * Return the map from a form, group, or server validator to report its errors. + * Pass Standard Schema issues to retain all issues in `form` and also route + * issues with paths to `fields`. Narrow possibly undefined issue lists first. * * @typeParam TFormData - Library-managed. Do not specify explicitly. */ -export type CreateErrorMapFn = - typeof createErrorMap +export interface CreateErrorMapFn { + ( + initial?: Partial>, + ): ValidationErrorMap + /** Converts issues into a map containing only Standard Schema issue arrays. */ + ( + issues: ReadonlyArray, + ): ParsedStandardSchemaIssues +} /** * Standard Schema issues reported at form or group scope and routed to field paths. @@ -641,25 +630,6 @@ export interface ParsedStandardSchemaIssues { fields: Partial, Array>> } -/** - * Converts Standard Schema issues into errors owned by the field being validated. - * - * @returns A copy of the issue array. Issue paths do not route errors to other fields. - */ -export type ParseFieldIssuesFn = ( - issues: ReadonlyArray, -) => Array - -/** - * Converts Standard Schema issues into a form or group error map to return from a validator. - * - * @returns All issues in `form`, with issues that have paths also grouped in `fields`. - * @typeParam TFormData - Library-managed. Do not specify explicitly. - */ -export type ParseFormIssuesFn = ( - issues: ReadonlyArray, -) => ParsedStandardSchemaIssues - /** * A successful validation result that carries data for `validatorOutputs`. * @@ -719,9 +689,7 @@ export interface FormValidatorContext { triggerFieldApi?: AnyFieldApi /** The complete form values being validated. */ value: TFormData - /** Converts Standard Schema issues into form and field errors to return from this validator. */ - parseIssues: ParseFormIssuesFn - /** Creates an error map with field paths inferred from the form values. */ + /** Builds an error map or routes Standard Schema issues using the form values. */ createErrorMap: CreateErrorMapFn /** Creates a successful result to return data in this validator's `validatorOutputs` entry. */ createOutput: CreateOutputFn @@ -743,9 +711,7 @@ export interface ServerFormValidatorContext { triggerFieldApi?: AnyFieldApi /** The complete form values passed to server validation. */ value: TFormData - /** Converts Standard Schema issues into form and field errors to return from this validator. */ - parseIssues: ParseFormIssuesFn - /** Creates an error map with field paths inferred from the form values. */ + /** Builds an error map or routes Standard Schema issues using the server values. */ createErrorMap: CreateErrorMapFn /** Creates a successful result to return data in this validator's server `validatorOutputs` entry. */ createOutput: CreateOutputFn @@ -877,9 +843,7 @@ export interface FormGroupValidatorContext { triggerFieldApi?: AnyFieldApi /** The group's current value, without the surrounding form values. */ value: TGroupValue - /** Converts Standard Schema issues into group errors and errors at group-relative field paths. */ - parseIssues: ParseFormIssuesFn - /** Creates an error map whose field paths are relative to this group. */ + /** Builds an error map or routes Standard Schema issues at group-relative field paths. */ createErrorMap: CreateErrorMapFn /** Creates a successful result to return data in this validator's `validatorOutputs` entry. */ createOutput: CreateOutputFn @@ -954,8 +918,6 @@ export interface FieldValidatorContext< fieldApi: FieldApi /** The current value of the field being validated. */ value: TFieldValue - /** Converts Standard Schema issues into errors for this field, without routing their paths. */ - parseIssues: ParseFieldIssuesFn } /** A field validator's success result or errors. Field validators do not provide output data. */ @@ -964,6 +926,9 @@ export type FieldValidateResult = ValidationResult /** * Validates a field's value and reports errors owned by that field. * + * Standard Schema issue arrays can be returned directly, including readonly + * arrays. Issue paths do not route errors to other fields. + * * @typeParam TFormData - Library-managed. Do not specify explicitly. * @typeParam TFieldName - Library-managed. Do not specify explicitly. * @typeParam TFieldValue - Library-managed. Do not specify explicitly. @@ -1118,7 +1083,7 @@ type ValidationErrorValueFromType = unknown extends TError type ValidationErrorInputFromType = [TError] extends [never] ? never - : OneOrMany> + : ReadonlyOneOrMany> /** * Form validation results constrained to inferred issue types, as stored in server form state. diff --git a/packages/form-core/src/validation/execution.lib.ts b/packages/form-core/src/validation/execution.lib.ts index a7e1ba92f..1cb82f269 100644 --- a/packages/form-core/src/validation/execution.lib.ts +++ b/packages/form-core/src/validation/execution.lib.ts @@ -1,8 +1,4 @@ -import { - isStandardSchema, - parseStandardSchema, - parseStandardSchemaIssues, -} from '../standardSchema.lib' +import { isStandardSchema, parseStandardSchema } from '../standardSchema.lib' import { VALIDATION_OUTPUT, isValidationOutput } from '../validationOutput.lib' import type { AnyInternalValidatorInstance } from '../ValidatorInstance.lib' import type { InternalFormApi } from '../FormApi/FormApi.lib' @@ -38,11 +34,11 @@ type ServerPipelineValidateContext = { } type FieldValidateContext = Omit< FieldValidatorContext, - 'value' | 'parseIssues' + 'value' > & { scope: 'field' } type FormGroupValidateContext = Omit< FormGroupValidatorContext, - 'value' | 'parseIssues' | 'createErrorMap' | 'createOutput' + 'value' | 'createErrorMap' | 'createOutput' > & { scope: 'group' } export type FormInputContext = Omit type ServerFormInputContext = Omit @@ -148,13 +144,6 @@ function getPredicateContext( } } -/** Parses Standard Schema issues as errors owned directly by a field. */ -export function parseFieldIssues( - issues: Parameters['parseIssues']>[0], -) { - return parseStandardSchemaIssues(issues, undefined, 'field') -} - export const ABORTED_CALL = Symbol('ABORTED_CALL') export const THROWN_ERROR = Symbol('THROWN_ERROR') diff --git a/packages/form-core/src/validation/mount.lib.ts b/packages/form-core/src/validation/mount.lib.ts index b914809e5..65cb22ce5 100644 --- a/packages/form-core/src/validation/mount.lib.ts +++ b/packages/form-core/src/validation/mount.lib.ts @@ -1,16 +1,11 @@ import { createOutput } from '../validationOutput.lib' -import { createErrorMap } from '../validation.public' -import { - isStandardSchema, - parseStandardSchema, - parseStandardSchemaIssues, -} from '../standardSchema.lib' +import { createErrorMapFactory } from '../createErrorMap.lib' +import { isStandardSchema, parseStandardSchema } from '../standardSchema.lib' import { isPromiseLike } from '../utils.lib' import { isErrorResult } from './errors.lib' import { createValidatorAbortContext, normalizeValidatorResult, - parseFieldIssues, } from './execution.lib' import type { AnyInternalValidatorInstance } from '../ValidatorInstance.lib' import type { InternalFormApi } from '../FormApi/FormApi.lib' @@ -287,10 +282,8 @@ export function runFormMountValidatorPipeline({ signal, formApi, value: formApi.state.values, - createErrorMap, + createErrorMap: createErrorMapFactory(formApi.state.values), createOutput, - parseIssues: (issues) => - parseStandardSchemaIssues(issues, formApi.state.values, 'form'), }), scope: 'form', onResult, @@ -317,7 +310,6 @@ export function runFieldMountValidatorPipeline({ formApi: fieldApi.form as never, fieldApi: fieldApi as never, value: fieldApi.value, - parseIssues: parseFieldIssues, }), scope: 'field', onResult, @@ -347,10 +339,8 @@ export function runGroupMountValidatorPipeline({ groupApi: groupApi as never, triggerFieldApi: undefined, value: groupApi.value, - createErrorMap, + createErrorMap: createErrorMapFactory(groupApi.value), createOutput, - parseIssues: (issues) => - parseStandardSchemaIssues(issues, groupApi.value, 'form'), }), scope: 'form', onResult, diff --git a/packages/form-core/src/validation/pipeline.lib.ts b/packages/form-core/src/validation/pipeline.lib.ts index e90bc5038..1e4682a0d 100644 --- a/packages/form-core/src/validation/pipeline.lib.ts +++ b/packages/form-core/src/validation/pipeline.lib.ts @@ -1,6 +1,5 @@ import { createOutput } from '../validationOutput.lib' -import { createErrorMap } from '../validation.public' -import { parseStandardSchemaIssues } from '../standardSchema.lib' +import { createErrorMapFactory } from '../createErrorMap.lib' import { isNotNil } from '../utils.lib' import { isErrorResult } from './errors.lib' import { @@ -9,7 +8,6 @@ import { executeValidator, isFieldValidateContext, isServerValidateContext, - parseFieldIssues, runMaybeDebouncedValidator, shouldRunValidator, } from './execution.lib' @@ -258,12 +256,8 @@ export function runFormValidatorPipeline({ formApi: ctx.formApi, signal: ctx.signal, value: ctx.formApi.state.values, - createErrorMap, + createErrorMap: createErrorMapFactory(ctx.formApi.state.values), createOutput, - parseIssues: ( - issues: Parameters[0], - ) => - parseStandardSchemaIssues(issues, ctx.formApi.state.values, 'form'), } if (!isFieldValidateContext(ctx)) { @@ -337,7 +331,6 @@ export function runFieldValidatorPipeline({ signal: ctx.signal, fieldApi: context.fieldApi, value: context.fieldApi.value, - parseIssues: parseFieldIssues, } }, scope: 'field', diff --git a/packages/form-core/tests/FormApi/submission-handling.spec.ts b/packages/form-core/tests/FormApi/submission-handling.spec.ts index b66e2bd3e..5540773a6 100644 --- a/packages/form-core/tests/FormApi/submission-handling.spec.ts +++ b/packages/form-core/tests/FormApi/submission-handling.spec.ts @@ -198,11 +198,11 @@ describe('form - submission handling', () => { }) const form = new InternalFormApi({ defaultValues: { name: '' }, - onSubmit: ({ value, parseIssues }) => { + onSubmit: ({ value, createErrorMap }) => { const result = schema.safeParse(value) if (!result.success) { - return parseIssues(result.error.issues) + return createErrorMap(result.error.issues) } return null diff --git a/packages/form-core/tests/FormApi/validation.spec.ts b/packages/form-core/tests/FormApi/validation.spec.ts index 2ca600b9a..2a033fa39 100644 --- a/packages/form-core/tests/FormApi/validation.spec.ts +++ b/packages/form-core/tests/FormApi/validation.spec.ts @@ -95,7 +95,7 @@ describe('form - validation', () => { const result = schema.safeParse(context.value) if (!result.success) { - return context.parseIssues(result.error.issues) + return result.error.issues } return null @@ -122,11 +122,11 @@ describe('form - validation', () => { defaultValues: { user: { name: '' } }, validators: [ { - run: ({ value, parseIssues }) => { + run: ({ value, createErrorMap }) => { const result = schema.safeParse(value) if (!result.success) { - return parseIssues(result.error.issues) + return createErrorMap(result.error.issues) } return null diff --git a/packages/form-core/tests/FormGroupApi/FormGroupApi.spec.ts b/packages/form-core/tests/FormGroupApi/FormGroupApi.spec.ts index 74c906f0f..d0b66e010 100644 --- a/packages/form-core/tests/FormGroupApi/FormGroupApi.spec.ts +++ b/packages/form-core/tests/FormGroupApi/FormGroupApi.spec.ts @@ -2030,11 +2030,11 @@ describe('FormGroupApi', () => { validators: [ { triggers: [], - run: ({ value, parseIssues }) => { + run: ({ value, createErrorMap }) => { const result = schema.safeParse(value) if (!result.success) { - return parseIssues(result.error.issues) + return createErrorMap(result.error.issues) } return null diff --git a/packages/form-core/tests/validation-pipeline.test.ts b/packages/form-core/tests/validation-pipeline.test.ts index dbff7c12b..3a7e08295 100644 --- a/packages/form-core/tests/validation-pipeline.test.ts +++ b/packages/form-core/tests/validation-pipeline.test.ts @@ -122,10 +122,10 @@ describe('runFormValidatorPipeline', () => { const issues = [ { message: 'Invalid name', path: ['users', 0, 'name'] as const }, ] - const run = vi.fn(({ triggerFieldApi, value, parseIssues }) => { + const run = vi.fn(({ triggerFieldApi, value, createErrorMap }) => { expect(triggerFieldApi).toBe(field) expect(value).toBe(formApi.state.values) - expect(parseIssues(issues)).toEqual({ + expect(createErrorMap(issues)).toEqual({ form: issues, fields: { 'users[0].name': issues }, }) @@ -154,7 +154,7 @@ describe('runFormValidatorPipeline', () => { expect(fieldContext.fieldApi).toBe(field) expect(fieldContext).not.toHaveProperty('triggerFieldApi') expect(fieldContext.value).toBe(formApi.state.values) - expect(fieldContext.parseIssues(issues)).toEqual({ + expect(fieldContext.createErrorMap(issues)).toEqual({ form: issues, fields: { 'users[0].name': issues }, }) diff --git a/packages/form-core/tests/validation-public.test.ts b/packages/form-core/tests/validation-public.test.ts index be414c1ec..149b7bb05 100644 --- a/packages/form-core/tests/validation-public.test.ts +++ b/packages/form-core/tests/validation-public.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest' import z from 'zod' import { - createErrorMap, createErrorVisibility, createValidator, createValidators, @@ -62,41 +61,4 @@ describe('validation public helpers', () => { expect(createErrorVisibility(visibility)).toBe(visibility) }) - - it('creates mutable validation error maps', () => { - const errors = createErrorMap<{ name: string; age: number }>() - - expect(errors).toEqual({ fields: {} }) - errors.fields.name = undefined - errors.fields.age = 'Age is required' - errors.form = 'Form is invalid' - - expect(errors).toEqual({ - form: 'Form is invalid', - fields: { name: undefined, age: 'Age is required' }, - }) - }) - - it('returns the prefilled validation error map', () => { - const initial = { - form: 'Form is invalid', - fields: { name: 'Name is required' }, - } - - const errors = createErrorMap(initial) - - expect(errors).toBe(initial) - }) - - it('preserves falsy form errors in the initial error map', () => { - const initial = { - form: '', - fields: {}, - } - - const errors = createErrorMap(initial) - - expect(errors).toBe(initial) - expect(errors).toHaveProperty('form', '') - }) }) diff --git a/packages/form-core/tests/validation.test-d.ts b/packages/form-core/tests/validation.test-d.ts index fd5f63026..c14596435 100644 --- a/packages/form-core/tests/validation.test-d.ts +++ b/packages/form-core/tests/validation.test-d.ts @@ -6,10 +6,10 @@ import { createValidator, createValidators, formOptions, - createErrorMap as importCreateErrorMap, } from '../src' import type { AnyFieldApi, + CreateSubmitErrorMapFn, ErrorVisibilityFieldState, FieldApiOptions, FieldErrors, @@ -23,7 +23,6 @@ import type { FormSubmitInvalidContext, FormValidators, OnSubmitError, - ParseSubmitIssuesFn, ReusableErrorVisibilityState, StandardSchemaV1Issue, ToFieldError, @@ -31,7 +30,6 @@ import type { ToFormGroupErrorTypes, ToFormGroupOutputs, ToFormOutputs, - ValidationErrorMap, ValidationIssue, } from '../src' @@ -501,35 +499,6 @@ describe('ErrorVisibility', () => { }) describe('createErrorMap', () => { - it('creates a typed error map shape', () => { - type ErrorMapFormData = { - name: string - user: { - age: number - } - } - - const errors = importCreateErrorMap() - const initial: ValidationErrorMap = { - form: 'Initial form error', - fields: { name: 'Initial name error' }, - } - - expectTypeOf(errors).toEqualTypeOf>() - expectTypeOf(importCreateErrorMap(initial)).toEqualTypeOf< - ValidationErrorMap - >() - expectTypeOf( - importCreateErrorMap({ form: 'Partial form error' }), - ).toEqualTypeOf>() - - errors.form = 'Form error' - errors.fields.name = 'Name is required' - errors.fields['user.age'] = { message: 'Age is required' } - // @ts-expect-error Error map fields must use valid form keys. - errors.fields.missing = 'Missing' - }) - it('infers the error map shape from form validator context', () => { const validators = defineFormValidators([ { @@ -980,7 +949,7 @@ describe('FormErrors', () => { it('should infer form errors from parsed Standard Schema issues', () => { const validators = defineFormValidators([ { - run: ({ parseIssues }) => parseIssues([{ message: '' }]), + run: ({ createErrorMap }) => createErrorMap([{ message: '' }]), triggers: [], }, ]) @@ -1100,7 +1069,7 @@ describe('FormErrors', () => { }) it('should infer submit errors from parsed Standard Schema issues', () => { - type SubmitReturn = ReturnType> + type SubmitReturn = ReturnType> expectTypeOf>().toEqualTypeOf< Array @@ -1108,8 +1077,8 @@ describe('FormErrors', () => { new InternalFormApi({ defaultValues: { name: '' }, - onSubmit: ({ parseIssues }) => { - return parseIssues([{ message: '' }]) + onSubmit: ({ createErrorMap }) => { + return createErrorMap([{ message: '' }]) }, }) }) @@ -1316,15 +1285,15 @@ describe('FieldErrors', () => { it('should infer field errors from parsed Standard Schema issues', () => { const fieldValidators = defineFieldValidators([ { - run: ({ parseIssues }) => parseIssues([{ message: '' }]), + run: (): ReadonlyArray => [{ message: '' }], triggers: [], }, ]) const formValidators = defineFormValidators([ { - run: ({ parseIssues }) => - parseIssues([{ message: '', path: ['name'] }]), + run: ({ createErrorMap }) => + createErrorMap([{ message: '', path: ['name'] }]), triggers: [], }, ]) @@ -1353,8 +1322,8 @@ describe('FieldErrors', () => { it('should infer field errors from group parsed Standard Schema issues', () => { const groupValidators = defineGroupValidators([ { - run: ({ parseIssues }) => - parseIssues([{ message: '', path: ['name'] }]), + run: ({ createErrorMap }) => + createErrorMap([{ message: '', path: ['name'] }]), triggers: [], }, ])