Skip to content

Commit 23fd8fa

Browse files
mmalerbathePunderWoman
authored andcommitted
fix(forms): use consistent error format returned from parse
Aligns the errors returned from the `parse` function in `transformedValue` to use the same convention as the rest of signal forms (a property called `error` that can contain a single error or list of errors)
1 parent 163dd8e commit 23fd8fa

File tree

6 files changed

+25
-8
lines changed

6 files changed

+25
-8
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ export type OneOrMany<T> = T | readonly T[];
436436

437437
// @public
438438
export interface ParseResult<TValue> {
439-
readonly errors?: readonly ValidationError.WithoutFieldTree[];
439+
readonly error?: OneOrMany<ValidationError.WithoutFieldTree>;
440440
readonly value?: TValue;
441441
}
442442

packages/forms/signals/src/api/rules/validation/util.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,17 @@ export function isEmpty(value: unknown): boolean {
5858
}
5959
return value === '' || value === false || value == null;
6060
}
61+
62+
/**
63+
* Normalizes validation errors (which can be a single error, an array of errors, or undefined)
64+
* into a list of errors.
65+
*/
66+
export function normalizeErrors<T>(error: OneOrMany<T> | undefined): readonly T[] {
67+
if (error === undefined) {
68+
return [];
69+
}
70+
if (Array.isArray(error)) {
71+
return error as readonly T[];
72+
}
73+
return [error as T];
74+
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import {FORM_FIELD_PARSE_ERRORS} from '../directive/parse_errors';
1717
import {createParser} from '../util/parser';
1818
import type {ValidationError} from './rules';
19+
import type {OneOrMany} from './types';
1920

2021
/**
2122
* Result of parsing a raw value into a model value.
@@ -28,7 +29,7 @@ export interface ParseResult<TValue> {
2829
/**
2930
* Errors encountered during parsing, if any.
3031
*/
31-
readonly errors?: readonly ValidationError.WithoutFieldTree[];
32+
readonly error?: OneOrMany<ValidationError.WithoutFieldTree>;
3233
}
3334

3435
/**
@@ -42,7 +43,7 @@ export interface TransformedValueOptions<TValue, TRaw> {
4243
*
4344
* Should return an object containing the parsed result, which may contain:
4445
* - `value`: The parsed model value. If `undefined`, the model will not be updated.
45-
* - `errors`: Any parse errors encountered. If `undefined`, no errors are reported.
46+
* - `error`: Any parse errors encountered. If `undefined`, no errors are reported.
4647
*/
4748
parse: (rawValue: TRaw) => ParseResult<TValue>;
4849

@@ -93,7 +94,7 @@ export interface TransformedValueSignal<TRaw> extends WritableSignal<TRaw> {
9394
* if (val === '') return {value: null};
9495
* const num = Number(val);
9596
* if (Number.isNaN(num)) {
96-
* return {errors: [{kind: 'parse', message: `${val} is not numeric`}]};
97+
* return {error: {kind: 'parse', message: `${val} is not numeric`}};
9798
* }
9899
* return {value: num};
99100
* },

packages/forms/signals/src/directive/native.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function getNativeControlValue(
7070

7171
if (element.validity.badInput) {
7272
return {
73-
errors: [new NativeInputParseError() as WithoutFieldTree<NativeInputParseError>],
73+
error: new NativeInputParseError() as WithoutFieldTree<NativeInputParseError>,
7474
};
7575
}
7676

packages/forms/signals/src/util/parser.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {type Signal, linkedSignal} from '@angular/core';
1010
import type {ValidationError} from '../api/rules';
11+
import {normalizeErrors} from '../api/rules/validation/util';
1112
import type {ParseResult} from '../api/transformed_value';
1213

1314
/**
@@ -44,12 +45,13 @@ export function createParser<TValue, TRaw>(
4445

4546
const setRawValue = (rawValue: TRaw) => {
4647
const result = parse(rawValue);
48+
errors.set(normalizeErrors(result.error));
4749
if (result.value !== undefined) {
4850
setValue(result.value);
4951
}
5052
// `errors` is a linked signal sourced from the model value; write parse errors after
5153
// model updates so `{value, errors}` results do not get reset by the recomputation.
52-
errors.set(result.errors ?? []);
54+
errors.set(normalizeErrors(result.error));
5355
};
5456

5557
return {errors: errors.asReadonly(), setRawValue};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,10 @@ class TestNumberInput implements FormValueControl<number | null> {
345345
if (rawValue === '') return {value: null};
346346
const value = Number(rawValue);
347347
if (Number.isNaN(value)) {
348-
return {errors: [{kind: 'parse', message: `${rawValue} is not numeric`}]};
348+
return {error: {kind: 'parse', message: `${rawValue} is not numeric`}};
349349
}
350350
if (this.parseMax() != null && value > this.parseMax()!) {
351-
return {value, errors: [maxError(this.parseMax()!)]};
351+
return {value, error: [maxError(this.parseMax()!)]};
352352
}
353353
return {value};
354354
},

0 commit comments

Comments
 (0)