Skip to content

Commit 709f5a3

Browse files
alxhubleonsenft
authored andcommitted
feat(forms): add FieldState.getError()
Added a `getError(kind: string)` method to `FieldState` that returns the first validation error of a given kind, or `undefined` if no such error exists. This method is reactive and will re-evaluate when errors change. Fixes #63905 Also updated public API goldens and added unit tests.
1 parent ee8d209 commit 709f5a3

File tree

4 files changed

+47
-0
lines changed

4 files changed

+47
-0
lines changed

goldens/public-api/forms/signals/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export type FieldContext<TValue, TPathKind extends PathKind = PathKind.Root> = T
130130
export interface FieldState<TValue, TKey extends string | number = string | number> extends ReadonlyFieldState<TValue, TKey> {
131131
readonly controlValue: WritableSignal<TValue>;
132132
readonly fieldTree: FieldTree<unknown, TKey>;
133+
getError(kind: string): ValidationError.WithFieldTree | undefined;
133134
markAsDirty(): void;
134135
markAsTouched(options?: MarkAsTouchedOptions): void;
135136
reloadValidation(): void;

packages/forms/signals/src/api/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,17 @@ export interface FieldState<
536536
*/
537537
markAsTouched(options?: MarkAsTouchedOptions): void;
538538

539+
/**
540+
* Gets the first validation error of the given kind on this field.
541+
*
542+
* This method is reactive and will re-evaluate when the field's errors change if called
543+
* within a reactive context (e.g. `computed` or `effect`).
544+
*
545+
* @param kind The kind of error (e.g. 'required', 'min').
546+
* @returns The first matching error, or `undefined` if none.
547+
*/
548+
getError(kind: string): ValidationError.WithFieldTree | undefined;
549+
539550
/**
540551
* Resets the {@link touched} and {@link dirty} state of the field and its descendants.
541552
*

packages/forms/signals/src/field/node.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ export class FieldNode implements FieldState<unknown> {
252252
return this.metadataState.get(key);
253253
}
254254

255+
getError(kind: string): ValidationError.WithFieldTree | undefined {
256+
return this.errors().find((e) => e.kind === kind);
257+
}
258+
255259
hasMetadata(key: MetadataKey<any, any, any>): boolean {
256260
return this.metadataState.has(key);
257261
}

packages/forms/signals/test/node/field_node.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,37 @@ describe('FieldNode', () => {
10231023
expect(f.a().valid()).toBe(false);
10241024
});
10251025

1026+
it('should get error by kind', () => {
1027+
const f = form(
1028+
signal({a: 1}),
1029+
(p) => {
1030+
validate(p.a, ({value}) => {
1031+
if (value() > 10) {
1032+
return [{kind: 'too-damn-high'}, {kind: 'bad'}];
1033+
}
1034+
return undefined;
1035+
});
1036+
},
1037+
{injector: TestBed.inject(Injector)},
1038+
);
1039+
1040+
expect(f.a().getError('too-damn-high')).toBeUndefined();
1041+
expect(f.a().getError('bad')).toBeUndefined();
1042+
1043+
f.a().value.set(11);
1044+
const errorHigh = f.a().getError('too-damn-high');
1045+
expect(errorHigh).toBeDefined();
1046+
expect(errorHigh?.kind).toBe('too-damn-high');
1047+
expect(errorHigh?.fieldTree).toBe(f.a);
1048+
1049+
const errorBad = f.a().getError('bad');
1050+
expect(errorBad).toBeDefined();
1051+
expect(errorBad?.kind).toBe('bad');
1052+
expect(errorBad?.fieldTree).toBe(f.a);
1053+
1054+
expect(f.a().getError('non-existent')).toBeUndefined();
1055+
});
1056+
10261057
it('should validate required field', () => {
10271058
const data = signal({first: '', last: ''});
10281059
const f = form(

0 commit comments

Comments
 (0)