-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Description
Which @angular/* package(s) are the source of the bug?
forms
Is this a regression?
No
Description
Signal forms break the one-way ControlValueAccessor contract by calling writeValue back on a CVA component after that component called onChange, directly contradicting Angular's own documentation.
The documented contract
Angular's official documentation at angular.dev states clearly:
writeValue(value: any): void
Writes a new value to the element.
This method is called by the forms API to write to the view when programmatic changes from model to view are requested.
This defines a strict one-way contract:
writeValue(value)— called by the form for programmatic, external, model-to-view changes onlyonChange(value)— called by the component when the user changes the value (view-to-model)
These two directions must never form a loop. Angular's classic FormControl correctly respects this — calling onChange updates the FormControl's internal value but does not trigger writeValue back on the CVA. Signal forms do not respect this.
What Signal forms does wrong
When a CVA component bound via [formField] calls onChange(value), the Signal form updates its internal signal. Because Signal form has no concept of value origin — it does not distinguish between a programmatic external set() and a value that just arrived from the CVA via onChange — it reactively pushes the value back to writeValue on the same CVA that just sent it:
User types in CVA component
→ onChange("hello") called
→ Signal form signal updates
→ writeValue("hello") called back on CVA ← violates documented contract
The same issue applies to FormValueControl, the Signal form equivalent of CVA. There is no mechanism to indicate who submitted the change — whether it originated from inside the component or from an external model update. Without that distinction, any reactive form implementation will produce this loopback.
Real-world impact
We implemented a CodeMirror editor as a ControlValueAccessor. CodeMirror manages its own internal document state. Our contract is:
writeValue= programmatic value from outside, push into CodeMirroronChange= user edited the document, notify the form
When Signal form calls writeValue back after our onChange, we are forced to either:
- Compare the incoming
writeValuevalue against CodeMirror's current document to detect the loopback — an O(n) string comparison on every keystroke. For large documents exceeding 1MB this causes severe and measurable performance degradation. - Use a dirty flag to suppress the loopback
writeValue— a workaround that should not be necessary and compensates for a broken contract.
Neither workaround is acceptable. FormControl does not require either. The behaviour difference between FormControl and Signal form for the same CVA implementation is a clear regression in the Signal form design.
Expected behaviour
Signal form must not call writeValue on a CVA in response to a value that was just received from that same CVA's onChange. This is the documented contract and the behaviour FormControl has always correctly implemented.
Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run ng version)
Angular CLI: 21.2.3
Node: 22.21.1
Package Manager: npm 10.9.4
OS: Windows 11 x64
Angular: 21.2.5
... animations, cli, common, compiler, core, forms, localize, platform-browser, platform-browser-dynamic, router, sdk
Anything else?
No response
Metadata
Metadata
Assignees
Labels
Type
Projects
Status