fix(forms): warn in dev mode when ngModel cannot reach parent NgForm across component boundary#69585
fix(forms): warn in dev mode when ngModel cannot reach parent NgForm across component boundary#69585arturovt wants to merge 1 commit into
Conversation
1438c12 to
4bd9084
Compare
| await timeout(); | ||
| expect(warnSpy).not.toHaveBeenCalledWith(jasmine.stringContaining('viewProviders')); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
What happens if we don't have a NgForm as parent but a FormGroup ?
There was a problem hiding this comment.
Valid concert, updated the code.
4bd9084 to
1bf6c87
Compare
JeanMeche
left a comment
There was a problem hiding this comment.
AGENT : Here are two prominent cases where the change would erroneously log a warning (false positives):
1. Explicitly standalone ngModel with [ngModelOptions]="{standalone: true}"
If a developer explicitly opts out of form registration using [ngModelOptions]="{standalone: true}", the warning will still be logged.
This occurs because the warning logic evaluates synchronously inside the NgModel constructor. During instantiation, @Host() parent resolves to null, and the constructor uses injector?.get(ControlContainer) to find the parent form and logs the warning. However, Angular @Input() bindings (such as ngModelOptions) are not initialized until after the constructor runs. The directive has no way of knowing the developer explicitly passed {standalone: true}, leading to a frustrating developer experience where the warning instructs them to apply a fix they have already applied.
2. Internal ngModel usage within a custom ControlValueAccessor
A very common pattern is creating a custom form component that implements ControlValueAccessor and using [(ngModel)] internally in its template to easily bind to the native input element.
If a user drops this custom component inside a <form>, the inner ngModel's constructor will run. Its @Host() boundary correctly stops at the custom component (parent === null), but injector.get(ControlContainer) traverses up the element injector tree and successfully finds the NgForm attached to the parent's <form> tag. The warning fires, assuming the developer accidentally disconnected the control, but the developer intentionally kept the inner ngModel isolated as an internal implementation detail of their custom component. Furthermore, because of the flaw in Case 1, the component author cannot even suppress this warning by adding standalone: true to the inner template.
…across component boundary NgModel injects ControlContainer with @host(), which stops the injector at the component host element boundary. When NgForm lives in a parent component and ngModel lives in a child component, the injection returns null silently and the control acts standalone — never registering with the form. To surface this invisible failure, emit a dev-mode warning (NG01354) when ngModel's @host() injection finds nothing but the element Injector can still reach a ControlContainer further up the hierarchy. The warning identifies the cross-boundary issue and points developers to the viewProviders fix or the standalone option. Adds the NG01354 reference page explaining why the warning fires and providing two remediation paths: bridging ControlContainer via viewProviders, or opting out with [ngModelOptions]="{standalone: true}". Fixes angular#47580
NgModel injects ControlContainer with @host(), which stops the injector at the component host element boundary. When NgForm lives in a parent component and ngModel lives in a child component, the injection returns null silently and the control acts standalone — never registering with the form.
To surface this invisible failure, emit a dev-mode warning (NG01354) when ngModel's @host() injection finds nothing but the element Injector can still reach a ControlContainer further up the hierarchy. The warning identifies the cross-boundary issue and points developers to the viewProviders fix or the standalone option.
Adds the NG01354 reference page explaining why the warning fires and providing two remediation paths: bridging ControlContainer via viewProviders, or opting out with [ngModelOptions]="{standalone: true}".
Fixes #47580