Skip to content

Commit

Permalink
feat: expose all field errors as an array
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-berger committed Feb 24, 2025
1 parent 7c0901d commit 1e4cb50
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Field-level properties & methods

```ts
getFieldError<P extends FieldPath<V>>(fieldPath: P): string | null;
getFieldErrors<P extends FieldPath<V>>(fieldPath: P): string[];
getFieldValue<P extends FieldPath<V>>(fieldPath: P): FieldValue<V, P>;
isFieldDirty<P extends FieldPath<V>>(fieldPath: P): boolean;
hasFieldError<P extends FieldPath<V>>(fieldPath: P): boolean;
Expand Down
2 changes: 2 additions & 0 deletions src/components/field.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from '../types';
import {
getFieldError,
getFieldErrors,
getFieldValue,
isFieldDirty,
hasFieldError,
Expand Down Expand Up @@ -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)),
Expand Down
8 changes: 4 additions & 4 deletions src/methods/get-field-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<V extends FormValue, P extends FieldPath<V>>(
formState: FormState<V>,
Expand All @@ -12,14 +12,14 @@ export function getFieldError<V extends FormValue, P extends FieldPath<V>>(
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;
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions src/methods/get-field-errors.ts
Original file line number Diff line number Diff line change
@@ -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<V>,
>(formState: FormState<V>, 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 [];
}
1 change: 1 addition & 0 deletions src/methods/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
15 changes: 14 additions & 1 deletion src/methods/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,21 @@ export function validate<V extends FormValue>(

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;
Expand Down
1 change: 1 addition & 0 deletions src/types/field-state.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { FormValue } from './form-value.model';

export type FieldState<V extends FormValue, P extends FieldPath<V>> = {
error: Accessor<string | null>;
errors: Accessor<string[]>;
isDirty: Accessor<boolean>;
hasError: Accessor<boolean>;
isTouched: Accessor<boolean>;
Expand Down
2 changes: 1 addition & 1 deletion src/types/field-states.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface FieldStates {
dirtyFieldPaths: Set<string>;
touchedFieldPaths: Set<string>;
errorFieldPaths: Map<string, string>;
errorFieldPaths: Map<string, string[]>;
}

0 comments on commit 1e4cb50

Please sign in to comment.