From bdfb60f3e33065e047183dc1890c36e527e2b304 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Sun, 22 Feb 2026 09:51:04 -0800 Subject: [PATCH] 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 23fd8fa58660b1a5051f26f40d8f3a673ed78dc9) --- goldens/public-api/forms/signals/index.api.md | 2 +- .../forms/signals/src/api/rules/validation/util.ts | 14 ++++++++++++++ .../forms/signals/src/api/transformed_value.ts | 7 ++++--- packages/forms/signals/src/directive/native.ts | 2 +- packages/forms/signals/src/util/parser.ts | 4 +++- .../forms/signals/test/node/parse_errors.spec.ts | 4 ++-- 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/goldens/public-api/forms/signals/index.api.md b/goldens/public-api/forms/signals/index.api.md index 8f40dae8f3e..2634fc57b21 100644 --- a/goldens/public-api/forms/signals/index.api.md +++ b/goldens/public-api/forms/signals/index.api.md @@ -435,7 +435,7 @@ export type OneOrMany = T | readonly T[]; // @public export interface ParseResult { - readonly errors?: readonly ValidationError.WithoutFieldTree[]; + readonly error?: OneOrMany; readonly value?: TValue; } diff --git a/packages/forms/signals/src/api/rules/validation/util.ts b/packages/forms/signals/src/api/rules/validation/util.ts index b8ac570225c..0befeed29f8 100644 --- a/packages/forms/signals/src/api/rules/validation/util.ts +++ b/packages/forms/signals/src/api/rules/validation/util.ts @@ -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(error: OneOrMany | undefined): readonly T[] { + if (error === undefined) { + return []; + } + if (Array.isArray(error)) { + return error as readonly T[]; + } + return [error as T]; +} diff --git a/packages/forms/signals/src/api/transformed_value.ts b/packages/forms/signals/src/api/transformed_value.ts index 247f8c06269..ab4d67192c9 100644 --- a/packages/forms/signals/src/api/transformed_value.ts +++ b/packages/forms/signals/src/api/transformed_value.ts @@ -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 { /** * Errors encountered during parsing, if any. */ - readonly errors?: readonly ValidationError.WithoutFieldTree[]; + readonly error?: OneOrMany; } /** @@ -42,7 +43,7 @@ export interface TransformedValueOptions { * * 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; @@ -93,7 +94,7 @@ export interface TransformedValueSignal extends WritableSignal { * 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}; * }, diff --git a/packages/forms/signals/src/directive/native.ts b/packages/forms/signals/src/directive/native.ts index a82841a6053..1359c83dfb4 100644 --- a/packages/forms/signals/src/directive/native.ts +++ b/packages/forms/signals/src/directive/native.ts @@ -70,7 +70,7 @@ export function getNativeControlValue( if (element.validity.badInput) { return { - errors: [new NativeInputParseError() as WithoutFieldTree], + error: new NativeInputParseError() as WithoutFieldTree, }; } diff --git a/packages/forms/signals/src/util/parser.ts b/packages/forms/signals/src/util/parser.ts index 01c3c05569f..7eb21364cfb 100644 --- a/packages/forms/signals/src/util/parser.ts +++ b/packages/forms/signals/src/util/parser.ts @@ -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( 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}; diff --git a/packages/forms/signals/test/node/parse_errors.spec.ts b/packages/forms/signals/test/node/parse_errors.spec.ts index 010e40204e4..aa7c36941e4 100644 --- a/packages/forms/signals/test/node/parse_errors.spec.ts +++ b/packages/forms/signals/test/node/parse_errors.spec.ts @@ -345,10 +345,10 @@ class TestNumberInput implements FormValueControl { 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}; },