components: Mat-hint causing an ExpressionChangedAfterItHasBeenCheckedError if respective input has a *ngIf directive

Reproduction

Minimal repro on StackBlitz. Open the console to see the following error message:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'aria-describedby: null'. Current value: 'aria-describedby: mat-hint-0'.

This always happens if the respective input has a *ngIf-directive. I couldn’t observe any subsequent errors caused by this. Using the hintLabel property instead of a <mat-hint> results in the same error.

Expected Behavior

No ExpressionChangedAfterItHasBeenCheckedError when using *ngIf on an input

Actual Behavior

ExpressionChangedAfterItHasBeenCheckedError

Environment

  • Angular: ^7.1.4
  • CDK/Material: ^7.2.0
  • Browser(s): Chrome (Most recent @ Version 75.0.3770.80)
  • Operating System (e.g. Windows, macOS, Ubuntu): macOS

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 12
  • Comments: 22 (3 by maintainers)

Commits related to this issue

Most upvoted comments

Try use Unique ID for the hint. A simple hack to make the error go away stackblitz

<mat-hint [id]="null">No error</mat-hint>

This should not be marked as closed. While true that running in production mode won’t raise this error, that’s just because production is no longer running with this safety check on (for efficiency). The bug still exists and should still be addressed.

This is because Angular is running in development mode, and in this mode change detection adds an additional turn after every regular change detection run to check if the model has changed.

If you run this example locally on you machine in production mode, everything will be ok. Screen Shot 2019-06-05 at 10 43 12

Hello

I am having this error using mat-form-field with several inputs with a *ngIf each:

<mat-form-field> <input *ngIf="displayLanguage === 1" matInput type="text" formControlName="city" name="city" required> <input *ngIf="displayLanguage === 2" matInput type="text" formControlName="city2" name="city2"> <input *ngIf="displayLanguage === 3" matInput type="text" formControlName="city3" name="city3"> </mat-form-field>

But when I create a mat-form-field for each input and apply the *ngIf to them, the error disappears:

<mat-form-field *ngIf="displayLanguage === 1"> <input matInput type="text" formControlName="city" name="city" required> </mat-form-field> <mat-form-field *ngIf="displayLanguage === 2"> <input matInput type="text" formControlName="city2" name="city2"> </mat-form-field> <mat-form-field *ngIf="displayLanguage === 3"> <input matInput type="text" formControlName="city3" name="city3"> </mat-form-field>

Hope this helps.

I’m also having this same issue. I’m using it the same way as @ritxweb was however I believe using it this way should not raise this error.

Mainly because I’ve made a dynamic form builder and every generated component shared the name <mat-error> component, and if I were to put one <mat-form-field> on each field condition, I would also have to put one <mat-error>, copying and pasting the same code over and over.

We are building a component library which is very inspired by angular material 😃 we use the concept of the formfield with some modifications but run into the same issue.

What I found out is that with the ngIf on the input the instantiation changes.

With ngIf the cycle is like:

  • formfield gets instantiated
  • input gets instantiated
  • because of the ngIf the inputs view gets already initialized and checked, so the describedby is null
  • formfield content init here the described by ids get set now
  • formfield view init + checked -> error because the aria-described by of the input has changed

Without ngif the cycle is:

  • formfield gets instantiated
  • input gets instantiated
  • formfield content init -> ids get set on the input
  • input view init + checked -> no error

A possible solution I found is to defer all calls of _syncDescribedByIds to the next CD cycle. The downside I see is additional CD cycles especially during the initialisation of the formfield because it is called in the subscription of control.stateChanges, hintChildren.changes and errorChildren.changes which all are emitted during the first initialisation of the formfield. But maybe that can be mitigated somehow.

Would be wonderful for this to be addressed.

A use-case where an *ngIf would be needed is in a case dealing with Countries and States --where a text-entry of State is allowed but then switches to a drop-down for certain countries (e.g. US) as follows:

 <mat-form-field appearance="outline">
        <mat-label>State/Province/Region</mat-label>
        <input matInput type="text" required formControlName="state_province" *ngIf="stateProvinceRegions.length == 0; else stateProvinceRegionSelector"/>
        <ng-template #stateProvinceRegionSelector>
          <mat-select required formControlName="state_province">
            <mat-option *ngFor="let entry of stateProvinceRegions" [value]="entry.key" >
              {{entry.value}}
            </mat-option>
          </mat-select>
        </ng-template>
        <!-- [id]="null" is a hack (https://github.com/angular/components/issues/16209)-->
        <mat-error [id]="null" *ngIf="purchaseForm.get('address.state_province')?.errors?.required">Region is required.</mat-error>
 </mat-form-field>

“Avoid using *ngIf” is not a solution at all. It’s a pretty essential feature of Angular, you can’t just avoid it everywhere above your mat-error/mat-hint.

I think its not about avoiding ngIf its about learning when it is pertinent or not.

when an ngIf statement is false, the component ceases to Exist in DOM. and if it doesn’t exist when it is needed then the library will crash (although this could be treated better internally), using a <ng-template> for the mat-error component is better, because the component will always be there available, and can be referenced by the library.

Every time a ngIf statement goes from false to true the component will be created form scratch, which sometimes can lead to performance issues. Thats why its better to learn when it is ok or not to use it.

Hello

I am having this error using mat-form-field with several inputs with a *ngIf each:

<mat-form-field> <input *ngIf="displayLanguage === 1" matInput type="text" formControlName="city" name="city" required> <input *ngIf="displayLanguage === 2" matInput type="text" formControlName="city2" name="city2"> <input *ngIf="displayLanguage === 3" matInput type="text" formControlName="city3" name="city3"> </mat-form-field>

But when I create a mat-form-field for each input and apply the *ngIf to them, the error disappears:

<mat-form-field *ngIf="displayLanguage === 1"> <input matInput type="text" formControlName="city" name="city" required> </mat-form-field> <mat-form-field *ngIf="displayLanguage === 2"> <input matInput type="text" formControlName="city2" name="city2"> </mat-form-field> <mat-form-field *ngIf="displayLanguage === 3"> <input matInput type="text" formControlName="city3" name="city3"> </mat-form-field>

Hope this helps.

Error:

<mat-hint *ngIf="dropdownOptions.hint">{{dropdownOptions.hint}}</mat-hint>

No Error using @ZloDeeV 's answer.

<mat-hint *ngIf="dropdownOptions.hint" [id]="null">{{dropdownOptions.hint}}</mat-hint>

The same problem appears with both mat-hint and mat-error elements. To avoid ExpressionChangedAfterItHasBeenCheckedError beeing thrown you should avoid using *ngIf directives with matInput elements

@joaodforce Thank you so much, it has actually led me to a much better solution!

However, I’d still say there is a but in angular-material here, and I’m not sure if avoiding ngIf/ngFor is always possible. I could not get it to work within mat-stepper, which actually relies on step components being in DOM while I need to dynamically control what steps there are.

Here’s another workaround:

@Component()
class MyComponentWithInputField implements AfterViewInit {
  myControl: AbstractControl;

  constructor(private cdr: ChangeDetectorRef) { }

  ngAfterViewInit() {
    if (this.myControl.errors) {
      this.cdr.detectChanges();
    }
  }
}

“Avoid using *ngIf” is not a solution at all. It’s a pretty essential feature of Angular, you can’t just avoid it everywhere above your mat-error/mat-hint.

That a bug,

im running into the same problem

<mat-form-field style="width: 100%;" [ngClass]="{myInputNoWrapper: options.noWrapper}">  
  <mat-label *ngIf="options.label">{{options.label}}</mat-label>
  <span *ngIf="options.prefix" matPrefix>{{ options.prefix }} &#160; </span>

  <input *ngIf="options.type != 'textarea'; else ta" matInput [formControl]="myInputControl"
  (input)="onChange($event)"
  (blur)="onBlur($event)"
  [type]="options.type" [placeholder]="options.placeholder"
  [required]="options.required">

  <ng-template #ta>
    <textarea [ngStyle]="{'height': options.textareaH}" matInput [formControl]="myInputControl"
    (input)="onChange($event)"
    (blur)="onBlur($event)"
    [placeholder]="options.placeholder"
    [required]="options.required"
    ></textarea>
  </ng-template>  

  <mat-hint *ngIf="options.hint">{{options.hint}}</mat-hint>
  <mat-error *ngIf="!options.noValidate" [MYDIRECTIVE]="myInputControl"></mat-error>
</mat-form-field>

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: ‘aria-describedby: null’. Current value: ‘aria-describedby: mat-hint-0’

if the only workaround is code duplication …