angular
angular copied to clipboard
Injected ngControl doesn't contain control property in Angular 9
🐞 bug report
Affected Package
import { NgControl } from '@angular/forms';
Is this a regression?
I believe that this is an Ivy compiler issue
Description
Injected ngControl
doesn't contain control
property in Angular 9.
🔬 Minimal Reproduction
<mat-checkbox formControlName="locked" name="locked" class="m-t-5"
[opDisabled]="!(isEditing && isLocked)">
<span>{{ 'User.Locked' | translate }}</span>
</mat-checkbox>
This doesn't work:
import { Directive, Input } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[opDisabled]'
})
export class DisabledDirective {
@Input()
set opDisabled(condition: boolean) {
const action = condition ? 'disable' : 'enable';
this.ngControl.control[action]();
}
constructor(private ngControl: NgControl) {}
}
This works:
import { Directive, Input } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[opDisabled]'
})
export class DisabledDirective {
@Input()
set opDisabled(condition: boolean) {
const action = condition ? 'disable' : 'enable';
setTimeout(() => this.ngControl.control[action]());
}
constructor(private ngControl: NgControl) {}
}
🔥 Exception or Error
core.js:5828 ERROR TypeError: Cannot read property 'disable' of undefined
The control
property is undefined
and It looks like the it gets appended asynchronously to the injected ngControl
, that's why the setTimeout(() => {...})
workaround seems to work. Is this by design or is it a bug?
There is a similar issue reported #32522, but locked due to inactivity, without any solutions mentioned in the comments.
🌍 Your Environment
Angular Version:
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 9.0.1
Node: 13.3.0
OS: darwin x64
Angular: 9.0.0
... animations, cdk, common, compiler, compiler-cli, core, forms
... language-service, material, platform-browser
... platform-browser-dynamic, router
Ivy Workspace: Yes
Package Version
-----------------------------------------------------------
@angular-devkit/architect 0.900.1
@angular-devkit/build-angular 0.900.1
@angular-devkit/build-optimizer 0.900.1
@angular-devkit/build-webpack 0.900.1
@angular-devkit/core 9.0.1
@angular-devkit/schematics 9.0.1
@angular/cli 9.0.1
@angular/flex-layout 9.0.0-beta.29
@ngtools/webpack 9.0.1
@schematics/angular 9.0.1
@schematics/update 0.900.1
rxjs 6.5.4
typescript 3.7.5
webpack 4.41.2
Listen to opDisabled input binding change inside ngOnChanges, In this way you can fix this issue.
@Input() opDisabled;
ngOnChanges(changes) {
if (changes['opDisabled']) {
const action = this.opDisabled ? 'disable' : 'enable';
this.ngControl.control[action]();
}
}
For More Information Check this thread: https://twitter.com/yurzui/status/1225050458097704960
@ChellappanRajan thanks. This looks awkward, nevertheless.
So will this issue be fixed, or is the ngOnChanges workaround going to be required for Ivy permanently?
So which solution is actually better? the setTimeout or ngOnChanges solution?
There are more missing properties/values when injecting ngModel
instead of ngControl
.
With directive like:
@Directive({
selector: '[appTest]',
providers: [NgModel]
})
export class TestDirective implements OnInit{
constructor(
private ngModel: NgModel
) { }
ngOnInit(): void {
console.log(this.ngModel);
}
}
Used like this:
<form>
<input type="text" name="test-input" [(ngModel)]="title" appTest>
</form>
On Angular 9 console.log
prints something like:
NgModel {
path: (...)
formDirective: (...)
...
name: null
}
On Angular 8 console.log
prints something like:
NgModel {
path: (...)
formDirective: (...)
...
name: "test-input"
model: "angular8app"
viewModel: "angular8app"
}
In other words, at least name
is null and model
and viewModel
properties are missing when injecting NgModel
.
Disabling Ivy in tsconfig.app.json
will give the same result as in Angular 8:
"angularCompilerOptions": {
"enableIvy": false
}
Minimal reproduce scenario without using Material components: https://stackblitz.com/edit/angular-duxmqt?file=src%2Fapp%2Fapp.module.ts
So which solution is actually better? the setTimeout or ngOnChanges solution? @dafranklin
ngOnChanges
, by far (avoid another change detection + render cycle!
Here is the example impl: https://stackblitz.com/edit/angular-xeaqyy?file=src/app/app.module.ts
So will this issue be fixed, or is the ngOnChanges workaround going to be required for Ivy permanently?
So this is a tricky one. The current confusing behaviour is ivy is a combination of 2 implementations choices in forms and Angular core:
- in forms the
FormControlName
creates acontrol
instance inngOnChanges
- with ivy inputs are set before
ngOnChanges
hooks are executed on every directive
The above explains the error but this explanation only "make sense" if ones is familiar with the internal implementation and can be quite confusing for users.
We are still debatting the best approach here, for now the safest option is to go down the ngOnChanges
path (it should work for both VE and ivy).
Is there some documentation that can be updated (maybe even the JSDocs) until this odd behavior is fixed? I forgot to implement this in some new code and had to go looking up this error again, only to find I've commented before 😬
Hi all, got the same issue in a Directive used in a ReactiveForm input that in angular <= 8 used to work but with version 9+ the injected formControlName instance is missing name and control attributes.
fixed using the valueAccessor attribute of the injected FormControlName instance (because name and control are now undefined)
@Directive({
selector: 'input[myInputDirective]',
})
export class MyInputDirective implements OnInit {
inputValue: string; // now required to access the control value instead of this.formControl.control.value
constructor(
private formControl: FormControlName,
private confirmationDialogService: ConfirmationDialogService
) {}
ngOnInit(): void {
this.formControl.valueAccessor.registerOnChange((value) => this.inputValue = value);
}
@HostListener('blur')
__(): void {
this.checkData(this.inputValue); // --> used to be this.checkData(this.formControl.control.value); but now control attribute is undefined
}
...
checkData(inputValue: string): void {
...
HTH GG
Is there any news on this topic?
@zakharenkov @micobarac if you are using formControlName
you may use the ngAfterViewInit
hook to access the control
prop on NgControl
which FormControlName
directive inherits
As I understand as the formControlName is directive in NgOnInit its not yet fully initialized so the control prop is not present, when using it in afterVeiwInit
all is fine