Skip to content

Commit 27397b3

Browse files
mmalerbamattrbeck
authored andcommitted
fix(forms): clear parse errors when model updates (#66917)
Changes `parsedErrors` to a `linkedSignal` based on the model value. This ensures that the parse errors are reset if the model changes from outside the control. PR Close #66917
1 parent 2d99878 commit 27397b3

File tree

2 files changed

+34
-2
lines changed

2 files changed

+34
-2
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
linkedSignal,
1212
type ModelSignal,
1313
type Signal,
14-
signal,
1514
type WritableSignal,
1615
} from '@angular/core';
1716
import {FORM_FIELD_PARSE_ERRORS} from '../directive/parse_errors';
@@ -94,7 +93,10 @@ export function transformedValue<TValue, TRaw>(
9493
): TransformedValueSignal<TRaw> {
9594
const {parse, format} = options;
9695

97-
const parseErrors = signal<readonly ValidationError.WithoutFieldTree[]>([]);
96+
const parseErrors = linkedSignal({
97+
source: value,
98+
computation: () => [] as readonly ValidationError.WithoutFieldTree[],
99+
});
98100
const rawValue = linkedSignal(() => format(value()));
99101

100102
const formFieldParseErrors = inject(FORM_FIELD_PARSE_ERRORS, {self: true, optional: true});

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,36 @@ describe('parse errors', () => {
274274
expect(comp.model()).toBe(null);
275275
expect(comp.f().errors().length).toBe(0);
276276
});
277+
278+
it('should clear parse errors on one control when another control for the same field updates the model', async () => {
279+
@Component({
280+
imports: [TestNumberInput, FormField],
281+
template: `
282+
<test-number-input id="input1" [formField]="f" />
283+
<test-number-input id="input2" [formField]="f" />
284+
`,
285+
})
286+
class TestCmp {
287+
state = signal<number | null>(5);
288+
f = form(this.state);
289+
}
290+
291+
const testEl = (await act(() => TestBed.createComponent(TestCmp))).nativeElement as HTMLElement;
292+
const input1: HTMLInputElement = testEl.querySelector('#input1 input')!;
293+
const input2: HTMLInputElement = testEl.querySelector('#input2 input')!;
294+
295+
input1.value = 'joe';
296+
await act(() => input1.dispatchEvent(new Event('input')));
297+
let errors1 = [...testEl.querySelectorAll('#input1 .error')].map((el) => el.textContent);
298+
expect(errors1).toEqual(['joe is not numeric']);
299+
300+
input2.value = '42';
301+
await act(() => input2.dispatchEvent(new Event('input')));
302+
303+
errors1 = [...testEl.querySelectorAll('#input1 .error')].map((el) => el.textContent);
304+
expect(errors1).toEqual([]);
305+
expect(input1.value).toBe('42');
306+
});
277307
});
278308

279309
@Component({

0 commit comments

Comments
 (0)