angular icon indicating copy to clipboard operation
angular copied to clipboard

Injected ngControl doesn't contain control property in Angular 9

Open micobarac opened this issue 4 years ago • 10 comments

🐞 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

micobarac avatar Feb 11 '20 12:02 micobarac

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 avatar Feb 11 '20 12:02 ChellappanRajan

@ChellappanRajan thanks. This looks awkward, nevertheless.

micobarac avatar Feb 11 '20 12:02 micobarac

So will this issue be fixed, or is the ngOnChanges workaround going to be required for Ivy permanently?

allout58 avatar Apr 07 '20 20:04 allout58

So which solution is actually better? the setTimeout or ngOnChanges solution?

dafranklin avatar Apr 08 '20 12:04 dafranklin

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
}

Nitue avatar Apr 29 '20 09:04 Nitue

Minimal reproduce scenario without using Material components: https://stackblitz.com/edit/angular-duxmqt?file=src%2Fapp%2Fapp.module.ts

pkozlowski-opensource avatar Jun 04 '20 13:06 pkozlowski-opensource

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 a control instance in ngOnChanges
  • 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).

pkozlowski-opensource avatar Jun 04 '20 13:06 pkozlowski-opensource

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 😬

allout58 avatar Jan 04 '21 20:01 allout58

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

pinoatrome avatar Feb 26 '21 00:02 pinoatrome

Is there any news on this topic?

zakharenkov avatar Jan 18 '22 20:01 zakharenkov