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)

(cherry picked from commit 23fd8fa586)
This commit is contained in:
Miles Malerba 2026-02-22 09:51:04 -08:00 committed by Jessica Janiuk
parent e99b92a1f0
commit bdfb60f3e3
6 changed files with 25 additions and 8 deletions

View file

@ -435,7 +435,7 @@ export type OneOrMany<T> = T | readonly T[];
// @public
export interface ParseResult<TValue> {
readonly errors?: readonly ValidationError.WithoutFieldTree[];
readonly error?: OneOrMany<ValidationError.WithoutFieldTree>;
readonly value?: TValue;
}

View file

@ -58,3 +58,17 @@ export function isEmpty(value: unknown): boolean {
}
return value === '' || value === false || value == null;
}
/**
* Normalizes validation errors (which can be a single error, an array of errors, or undefined)
* into a list of errors.
*/
export function normalizeErrors<T>(error: OneOrMany<T> | undefined): readonly T[] {
if (error === undefined) {
return [];
}
if (Array.isArray(error)) {
return error as readonly T[];
}
return [error as T];
}

View file

@ -16,6 +16,7 @@ import {
import {FORM_FIELD_PARSE_ERRORS} from '../directive/parse_errors';
import {createParser} from '../util/parser';
import type {ValidationError} from './rules';
import type {OneOrMany} from './types';
/**
* Result of parsing a raw value into a model value.
@ -28,7 +29,7 @@ export interface ParseResult<TValue> {
/**
* Errors encountered during parsing, if any.
*/
readonly errors?: readonly ValidationError.WithoutFieldTree[];
readonly error?: OneOrMany<ValidationError.WithoutFieldTree>;
}
/**
@ -42,7 +43,7 @@ export interface TransformedValueOptions<TValue, TRaw> {
*
* Should return an object containing the parsed result, which may contain:
* - `value`: The parsed model value. If `undefined`, the model will not be updated.
* - `errors`: Any parse errors encountered. If `undefined`, no errors are reported.
* - `error`: Any parse errors encountered. If `undefined`, no errors are reported.
*/
parse: (rawValue: TRaw) => ParseResult<TValue>;
@ -93,7 +94,7 @@ export interface TransformedValueSignal<TRaw> extends WritableSignal<TRaw> {
* if (val === '') return {value: null};
* const num = Number(val);
* if (Number.isNaN(num)) {
* return {errors: [{kind: 'parse', message: `${val} is not numeric`}]};
* return {error: {kind: 'parse', message: `${val} is not numeric`}};
* }
* return {value: num};
* },

View file

@ -70,7 +70,7 @@ export function getNativeControlValue(
if (element.validity.badInput) {
return {
errors: [new NativeInputParseError() as WithoutFieldTree<NativeInputParseError>],
error: new NativeInputParseError() as WithoutFieldTree<NativeInputParseError>,
};
}

View file

@ -8,6 +8,7 @@
import {type Signal, linkedSignal} from '@angular/core';
import type {ValidationError} from '../api/rules';
import {normalizeErrors} from '../api/rules/validation/util';
import type {ParseResult} from '../api/transformed_value';
/**
@ -44,12 +45,13 @@ export function createParser<TValue, TRaw>(
const setRawValue = (rawValue: TRaw) => {
const result = parse(rawValue);
errors.set(normalizeErrors(result.error));
if (result.value !== undefined) {
setValue(result.value);
}
// `errors` is a linked signal sourced from the model value; write parse errors after
// model updates so `{value, errors}` results do not get reset by the recomputation.
errors.set(result.errors ?? []);
errors.set(normalizeErrors(result.error));
};
return {errors: errors.asReadonly(), setRawValue};

View file

@ -345,10 +345,10 @@ class TestNumberInput implements FormValueControl<number | null> {
if (rawValue === '') return {value: null};
const value = Number(rawValue);
if (Number.isNaN(value)) {
return {errors: [{kind: 'parse', message: `${rawValue} is not numeric`}]};
return {error: {kind: 'parse', message: `${rawValue} is not numeric`}};
}
if (this.parseMax() != null && value > this.parseMax()!) {
return {value, errors: [maxError(this.parseMax()!)]};
return {value, error: [maxError(this.parseMax()!)]};
}
return {value};
},