Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Injected ngControl doesn't contain control property in Angular 9 #35330

Open
micobarac opened this issue Feb 11, 2020 · 11 comments
Open

Injected ngControl doesn't contain control property in Angular 9 #35330

micobarac opened this issue Feb 11, 2020 · 11 comments
Labels
area: forms P4 A relatively minor issue that is not relevant to core functions
Milestone

Comments

@micobarac
Copy link

micobarac commented Feb 11, 2020

🐞 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
@ChellappanRajan
Copy link
Contributor

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

@micobarac
Copy link
Author

@ChellappanRajan thanks. This looks awkward, nevertheless.

@allout58
Copy link

allout58 commented Apr 7, 2020

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

@dafranklin
Copy link

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

@Nitue
Copy link

Nitue commented Apr 29, 2020

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
}

@pkozlowski-opensource
Copy link
Member

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

@pkozlowski-opensource
Copy link
Member

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).

@allout58
Copy link

allout58 commented Jan 4, 2021

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 😬

@pinoatrome
Copy link

pinoatrome commented Feb 26, 2021

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

@zakharenkov
Copy link

Is there any news on this topic?

@LemonyPie
Copy link

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: forms P4 A relatively minor issue that is not relevant to core functions
Projects
None yet
Development

No branches or pull requests