diff --git a/packages/forms/signals/src/directive/control_cva.ts b/packages/forms/signals/src/directive/control_cva.ts index 38f95eb4632..5857d68452f 100644 --- a/packages/forms/signals/src/directive/control_cva.ts +++ b/packages/forms/signals/src/directive/control_cva.ts @@ -88,12 +88,12 @@ export function cvaControlCreate( return () => { const fieldState = parent.state(); - const value = fieldState.value(); + const controlValue = fieldState.controlValue(); - if (bindingUpdated(bindings, 'controlValue', value)) { + if (bindingUpdated(bindings, 'controlValue', controlValue)) { // We don't know if the interop control has underlying signals, so we must use `untracked` to // prevent writing to a signal in a reactive context. - untracked(() => parent.controlValueAccessor!.writeValue(value)); + untracked(() => parent.controlValueAccessor!.writeValue(controlValue)); } for (const name of CONTROL_BINDING_NAMES) { diff --git a/packages/forms/signals/test/web/interop.spec.ts b/packages/forms/signals/test/web/interop.spec.ts index 45328911030..c2789874040 100644 --- a/packages/forms/signals/test/web/interop.spec.ts +++ b/packages/forms/signals/test/web/interop.spec.ts @@ -607,6 +607,62 @@ describe('ControlValueAccessor', () => { expect(field().value()).toBe('initial'); }); + it('should not write stale model values back to a CVA while debounce is pending', () => { + let writeValues: string[] = []; + + @Component({ + selector: 'custom-control-writeback-test', + template: ``, + }) + class CustomControlWritebackTest implements ControlValueAccessor { + value = ''; + + private onChangeFn?: (value: string) => void; + + writeValue(newValue: string): void { + writeValues.push(newValue); + this.value = newValue; + } + + registerOnChange(fn: (value: string) => void): void { + this.onChangeFn = fn; + } + + registerOnTouched(fn: () => void): void {} + + onInput(newValue: string) { + this.value = newValue; + this.onChangeFn?.(newValue); + } + } + + @Component({ + imports: [CustomControlWritebackTest, FormField], + template: ``, + }) + class TestCmp { + readonly f = form(signal('initial'), (p) => { + debounce(p, 'blur'); + }); + } + + const fixture = act(() => TestBed.createComponent(TestCmp)); + + const debugEl = fixture.debugElement.query( + (el) => el.componentInstance instanceof CustomControlWritebackTest, + ); + + const cvaInstance = debugEl.componentInstance as CustomControlWritebackTest; + + writeValues = []; + + act(() => cvaInstance.onInput('updated')); + + expect(cvaInstance.value).toBe('updated'); + + expect(writeValues).toEqual([]); + }); + describe('properties', () => { describe('disabled', () => { it('should bind to directive input', () => {