Skip to content

Commit 1e4cb50

Browse files
committed
feat: expose all field errors as an array
1 parent 7c0901d commit 1e4cb50

File tree

8 files changed

+52
-6
lines changed

8 files changed

+52
-6
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Field-level properties & methods
1717

1818
```ts
1919
getFieldError<P extends FieldPath<V>>(fieldPath: P): string | null;
20+
getFieldErrors<P extends FieldPath<V>>(fieldPath: P): string[];
2021
getFieldValue<P extends FieldPath<V>>(fieldPath: P): FieldValue<V, P>;
2122
isFieldDirty<P extends FieldPath<V>>(fieldPath: P): boolean;
2223
hasFieldError<P extends FieldPath<V>>(fieldPath: P): boolean;

src/components/field.component.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from '../types';
1313
import {
1414
getFieldError,
15+
getFieldErrors,
1516
getFieldValue,
1617
isFieldDirty,
1718
hasFieldError,
@@ -81,6 +82,7 @@ export function Field<
8182

8283
const fieldState = {
8384
error: createMemo(() => getFieldError(formState, fieldPath)),
85+
errors: createMemo(() => getFieldErrors(formState, fieldPath)),
8486
isDirty: createMemo(() => isFieldDirty(formState, fieldPath)),
8587
hasError: createMemo(() => hasFieldError(formState, fieldPath)),
8688
isTouched: createMemo(() => isFieldTouched(formState, fieldPath)),

src/methods/get-field-error.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { FormValue, FormState, FieldPath } from '../types';
22
import { isTraversable } from '../utils';
33

44
/**
5-
* Get error for a field (if there is one).
5+
* Get the first error for a field (if there is one).
66
*/
77
export function getFieldError<V extends FormValue, P extends FieldPath<V>>(
88
formState: FormState<V>,
@@ -12,14 +12,14 @@ export function getFieldError<V extends FormValue, P extends FieldPath<V>>(
1212
const { errorFieldPaths } = formState.__internal.fieldStates;
1313

1414
if (errorFieldPaths.has(fieldPath)) {
15-
return errorFieldPaths.get(fieldPath) ?? null;
15+
return errorFieldPaths.get(fieldPath)?.[0] ?? null;
1616
}
1717

1818
// No need to check descendants if the value is not an object or array.
1919
if (isTraversable(formValue)) {
20-
for (const [errorFieldPath, error] of errorFieldPaths) {
20+
for (const [errorFieldPath, errors] of errorFieldPaths) {
2121
if (errorFieldPath.startsWith(fieldPath)) {
22-
return error;
22+
return errors[0] ?? null;
2323
}
2424
}
2525
}

src/methods/get-field-errors.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { FormValue, FormState, FieldPath } from '../types';
2+
import { isTraversable } from '../utils';
3+
4+
/**
5+
* Get all errors for a field.
6+
*/
7+
export function getFieldErrors<
8+
V extends FormValue,
9+
P extends FieldPath<V>,
10+
>(formState: FormState<V>, fieldPath: P): string[] {
11+
const { value: formValue } = formState;
12+
const { errorFieldPaths } = formState.__internal.fieldStates;
13+
14+
if (errorFieldPaths.has(fieldPath)) {
15+
return errorFieldPaths.get(fieldPath) ?? [];
16+
}
17+
18+
// No need to check descendants if the value is not an object or array.
19+
if (isTraversable(formValue)) {
20+
for (const [errorFieldPath, errors] of errorFieldPaths) {
21+
if (errorFieldPath.startsWith(fieldPath)) {
22+
return errors;
23+
}
24+
}
25+
}
26+
27+
return [];
28+
}

src/methods/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './get-field-error';
2+
export * from './get-field-errors';
23
export * from './get-field-value';
34
export * from './has-error';
45
export * from './has-field-error';

src/methods/validate.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,21 @@ export function validate<V extends FormValue>(
1818

1919
const result = options.schema.safeParse(formValue);
2020

21+
// Clear existing errors.
22+
fieldStates.errorFieldPaths.clear();
23+
24+
// Aggregate errors by field path. A single field can have multiple
25+
// errors.
2126
for (const error of result.error?.errors ?? []) {
22-
fieldStates.errorFieldPaths.set(error.path.join('.'), error.message);
27+
const fieldPath = error.path.join('.');
28+
29+
const existingErrors =
30+
fieldStates.errorFieldPaths.get(fieldPath) ?? [];
31+
32+
fieldStates.errorFieldPaths.set(fieldPath, [
33+
...existingErrors,
34+
error.message,
35+
]);
2336
}
2437

2538
return result.success;

src/types/field-state.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { FormValue } from './form-value.model';
66

77
export type FieldState<V extends FormValue, P extends FieldPath<V>> = {
88
error: Accessor<string | null>;
9+
errors: Accessor<string[]>;
910
isDirty: Accessor<boolean>;
1011
hasError: Accessor<boolean>;
1112
isTouched: Accessor<boolean>;

src/types/field-states.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface FieldStates {
22
dirtyFieldPaths: Set<string>;
33
touchedFieldPaths: Set<string>;
4-
errorFieldPaths: Map<string, string>;
4+
errorFieldPaths: Map<string, string[]>;
55
}

0 commit comments

Comments
 (0)