Skip to content

Commit fe25c57

Browse files
cexbrayatthePunderWoman
authored andcommitted
fix(forms): preserve parse errors when parse returns value
Fixes #67170 by keeping the errors even a value is returned from the parse function.
1 parent b9b5c27 commit fe25c57

File tree

2 files changed

+27
-1
lines changed

2 files changed

+27
-1
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ export function createParser<TValue, TRaw>(
4444

4545
const setRawValue = (rawValue: TRaw) => {
4646
const result = parse(rawValue);
47-
errors.set(result.errors ?? []);
4847
if (result.value !== undefined) {
4948
setValue(result.value);
5049
}
50+
// `errors` is a linked signal sourced from the model value; write parse errors after
51+
// model updates so `{value, errors}` results do not get reset by the recomputation.
52+
errors.set(result.errors ?? []);
5153
};
5254

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

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {TestBed} from '@angular/core/testing';
1111
import {
1212
form,
1313
FormField,
14+
maxError,
1415
transformedValue,
1516
validate,
1617
type FormValueControl,
@@ -304,6 +305,25 @@ describe('parse errors', () => {
304305
expect(errors1).toEqual([]);
305306
expect(input1.value).toBe('42');
306307
});
308+
309+
it('should preserve parse errors when transformedValue parse returns both value and errors', async () => {
310+
@Component({
311+
imports: [TestNumberInput, FormField],
312+
template: `<test-number-input [parseMax]="10" [formField]="f" />`,
313+
})
314+
class TestCmp {
315+
state = signal<number | null>(5);
316+
f = form(this.state);
317+
}
318+
319+
const fix = await act(() => TestBed.createComponent(TestCmp));
320+
const comp = fix.componentInstance;
321+
const input: HTMLInputElement = fix.nativeElement.querySelector('input')!;
322+
323+
input.value = '11';
324+
await act(() => input.dispatchEvent(new Event('input')));
325+
expect(comp.f().errors()).toEqual([jasmine.objectContaining({kind: 'max'})]);
326+
});
307327
});
308328

309329
@Component({
@@ -318,6 +338,7 @@ describe('parse errors', () => {
318338
class TestNumberInput implements FormValueControl<number | null> {
319339
readonly value = model.required<number | null>();
320340
readonly errors = input<readonly ValidationError[]>([]);
341+
readonly parseMax = input<number | undefined>(undefined);
321342

322343
protected readonly rawValue = transformedValue(this.value, {
323344
parse: (rawValue) => {
@@ -326,6 +347,9 @@ class TestNumberInput implements FormValueControl<number | null> {
326347
if (Number.isNaN(value)) {
327348
return {errors: [{kind: 'parse', message: `${rawValue} is not numeric`}]};
328349
}
350+
if (this.parseMax() != null && value > this.parseMax()!) {
351+
return {value, errors: [maxError(this.parseMax()!)]};
352+
}
329353
return {value};
330354
},
331355
format: (value) => {

0 commit comments

Comments
 (0)