fix(forms): ignore stale value accessor callbacks after control is rebound#69747
Open
MedOussemaNjimi wants to merge 1 commit into
Open
fix(forms): ignore stale value accessor callbacks after control is rebound#69747MedOussemaNjimi wants to merge 1 commit into
MedOussemaNjimi wants to merge 1 commit into
Conversation
…bound `cleanUpControl` disconnects a directive from its previous control by registering noop `onChange`/`onTouched` callbacks on the value accessor, which assumes the accessor replaces previously registered callbacks. Value accessors that accumulate callbacks instead (e.g. by subscribing each one to an inner control's `valueChanges`) keep invoking the stale callback, so rebinding `[formControl]` (or swapping a control inside a `FormGroup`) overwrites the previously bound control's value with the new control's value as soon as `writeValue` runs. Guard the view-to-model and blur pipelines so that callbacks registered for a control the directive is no longer attached to become inert. Fixes angular#68744
This comment was marked as outdated.
This comment was marked as outdated.
Author
|
@googlebot I signed it! |
Member
I think the assumption is valid and should just be documented as interface contract. The provided CVA example has a memory leak by not unsubscribing, so that is a real problem that shouldn't be masked. |
Author
|
Fair point, thanks for the review! I agree the guard would mask the subscription leak in the accessor rather than surface it. Happy to rework this PR. Which direction would you prefer?
I'll update the PR accordingly. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: #68744
When the
FormControlbound via[formControl](or resolved byformControlNameafter aFormGroupswap) is replaced with a different instance, the previously bound control can besilently overwritten with the new control's value.
cleanUpControldisconnects the old view-to-model pipeline by callingvalueAccessor.registerOnChange(noop)/registerOnTouched(noop), which assumes the valueaccessor replaces the previously registered callback. Custom
ControlValueAccessorimplementations commonly accumulate callbacks instead — e.g.:
With such an accessor, the stale callback registered for the old control stays alive. During
re-setup,
setUpControlValueAccessorcallswriteValue(newControl.value), the inner controlemits, the stale callback fires, and
updateControlwrites the new control's value into theold control (also marking it dirty). See the minimal reproduction in the issue.
What is the new behavior?
The callbacks registered by
setUpViewChangePipelineandsetUpBlurPipelinenow check that thedirective is still attached to the control they were created for (
dir.control !== control) andbecome inert otherwise. Swapping the bound control no longer corrupts the previously bound
control's value, dirty, or touched state, regardless of how the value accessor implements
registerOnChange/registerOnTouched.Directives affected were verified for correct sequencing:
FormControlDirective:this.formalready points at the new control whenngOnChangesruns.FormControlName:_setupWithFormassignsthis.controlbefore setting up the pipelines.NgModel: its control never changes instance, so behavior is unchanged.A regression test reproducing the issue's scenario was added to
reactive_integration_spec.ts(fails without the fix, passes with it).
Does this PR introduce a breaking change?
Other information
An alternative considered was to document that
registerOnChangemust replace previouslyregistered callbacks, but the accumulating pattern is widespread in existing applications and the
guard makes the cleanup mechanism robust for both semantics at the cost of a single identity
check.
Closes #68744