From 1e4cb506e2bed75c023440542dd49a2cd15b2836 Mon Sep 17 00:00:00 2001 From: lars-berger Date: Mon, 24 Feb 2025 23:39:13 +0800 Subject: [PATCH] feat: expose all field errors as an array --- README.md | 1 + src/components/field.component.tsx | 2 ++ src/methods/get-field-error.ts | 8 ++++---- src/methods/get-field-errors.ts | 28 ++++++++++++++++++++++++++++ src/methods/index.ts | 1 + src/methods/validate.ts | 15 ++++++++++++++- src/types/field-state.model.ts | 1 + src/types/field-states.model.ts | 2 +- 8 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 src/methods/get-field-errors.ts diff --git a/README.md b/README.md index 53ce096..fd4561d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Field-level properties & methods ```ts getFieldError

>(fieldPath: P): string | null; +getFieldErrors

>(fieldPath: P): string[]; getFieldValue

>(fieldPath: P): FieldValue; isFieldDirty

>(fieldPath: P): boolean; hasFieldError

>(fieldPath: P): boolean; diff --git a/src/components/field.component.tsx b/src/components/field.component.tsx index 1769e08..9aa2181 100644 --- a/src/components/field.component.tsx +++ b/src/components/field.component.tsx @@ -12,6 +12,7 @@ import type { } from '../types'; import { getFieldError, + getFieldErrors, getFieldValue, isFieldDirty, hasFieldError, @@ -81,6 +82,7 @@ export function Field< const fieldState = { error: createMemo(() => getFieldError(formState, fieldPath)), + errors: createMemo(() => getFieldErrors(formState, fieldPath)), isDirty: createMemo(() => isFieldDirty(formState, fieldPath)), hasError: createMemo(() => hasFieldError(formState, fieldPath)), isTouched: createMemo(() => isFieldTouched(formState, fieldPath)), diff --git a/src/methods/get-field-error.ts b/src/methods/get-field-error.ts index c3ef6b6..9be8788 100644 --- a/src/methods/get-field-error.ts +++ b/src/methods/get-field-error.ts @@ -2,7 +2,7 @@ import type { FormValue, FormState, FieldPath } from '../types'; import { isTraversable } from '../utils'; /** - * Get error for a field (if there is one). + * Get the first error for a field (if there is one). */ export function getFieldError>( formState: FormState, @@ -12,14 +12,14 @@ export function getFieldError>( const { errorFieldPaths } = formState.__internal.fieldStates; if (errorFieldPaths.has(fieldPath)) { - return errorFieldPaths.get(fieldPath) ?? null; + return errorFieldPaths.get(fieldPath)?.[0] ?? null; } // No need to check descendants if the value is not an object or array. if (isTraversable(formValue)) { - for (const [errorFieldPath, error] of errorFieldPaths) { + for (const [errorFieldPath, errors] of errorFieldPaths) { if (errorFieldPath.startsWith(fieldPath)) { - return error; + return errors[0] ?? null; } } } diff --git a/src/methods/get-field-errors.ts b/src/methods/get-field-errors.ts new file mode 100644 index 0000000..482b80d --- /dev/null +++ b/src/methods/get-field-errors.ts @@ -0,0 +1,28 @@ +import type { FormValue, FormState, FieldPath } from '../types'; +import { isTraversable } from '../utils'; + +/** + * Get all errors for a field. + */ +export function getFieldErrors< + V extends FormValue, + P extends FieldPath, +>(formState: FormState, fieldPath: P): string[] { + const { value: formValue } = formState; + const { errorFieldPaths } = formState.__internal.fieldStates; + + if (errorFieldPaths.has(fieldPath)) { + return errorFieldPaths.get(fieldPath) ?? []; + } + + // No need to check descendants if the value is not an object or array. + if (isTraversable(formValue)) { + for (const [errorFieldPath, errors] of errorFieldPaths) { + if (errorFieldPath.startsWith(fieldPath)) { + return errors; + } + } + } + + return []; +} diff --git a/src/methods/index.ts b/src/methods/index.ts index 049971a..da0209f 100644 --- a/src/methods/index.ts +++ b/src/methods/index.ts @@ -1,4 +1,5 @@ export * from './get-field-error'; +export * from './get-field-errors'; export * from './get-field-value'; export * from './has-error'; export * from './has-field-error'; diff --git a/src/methods/validate.ts b/src/methods/validate.ts index a128465..298cb87 100644 --- a/src/methods/validate.ts +++ b/src/methods/validate.ts @@ -18,8 +18,21 @@ export function validate( const result = options.schema.safeParse(formValue); + // Clear existing errors. + fieldStates.errorFieldPaths.clear(); + + // Aggregate errors by field path. A single field can have multiple + // errors. for (const error of result.error?.errors ?? []) { - fieldStates.errorFieldPaths.set(error.path.join('.'), error.message); + const fieldPath = error.path.join('.'); + + const existingErrors = + fieldStates.errorFieldPaths.get(fieldPath) ?? []; + + fieldStates.errorFieldPaths.set(fieldPath, [ + ...existingErrors, + error.message, + ]); } return result.success; diff --git a/src/types/field-state.model.ts b/src/types/field-state.model.ts index 6abd27f..5c285e9 100644 --- a/src/types/field-state.model.ts +++ b/src/types/field-state.model.ts @@ -6,6 +6,7 @@ import type { FormValue } from './form-value.model'; export type FieldState> = { error: Accessor; + errors: Accessor; isDirty: Accessor; hasError: Accessor; isTouched: Accessor; diff --git a/src/types/field-states.model.ts b/src/types/field-states.model.ts index 6a9587e..02e9287 100644 --- a/src/types/field-states.model.ts +++ b/src/types/field-states.model.ts @@ -1,5 +1,5 @@ export interface FieldStates { dirtyFieldPaths: Set; touchedFieldPaths: Set; - errorFieldPaths: Map; + errorFieldPaths: Map; }