Components: form-field: support swapping various child components with ngIf

Created on 12 Oct 2017  路  9Comments  路  Source: angular/components

Bug, feature request, or proposal:

I recently ran into a bug that mat-hint threw an error when its corresponding matInput is inserted conditionally. E.g. take the following template:

<mat-form-field>
    <input *ngIf="isFile()" matInput placeholder="Enter file name" formControlName="filename">
    <input *ngIf="isDirectory()" matInput placeholder="Enter directory name" formControlName="filename">
    <mat-hint>
      Some hint.
    </mat-hint>
</mat-form-field>

Note that the only reason inputs are inserted conditionally here is because the placeholder text is different depending on the condition.

This will throw the following error:

app.component.html:7 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'null'. Current value: 'mat-hint-0'.
    at new d (VM8254 zone.min.js:1)
    at viewDebugError (VM7606 core.umd.js:8466)
    at expressionChangedAfterItHasBeenCheckedError (VM7606 core.umd.js:8444)
    at checkBindingNoChanges (VM7606 core.umd.js:8608)
    at checkNoChangesNodeDynamic (VM7606 core.umd.js:12515)
    at checkNoChangesNode (VM7606 core.umd.js:12464)
    at debugCheckNoChangesNode (VM7606 core.umd.js:13241)
    at debugCheckRenderNodeFn (VM7606 core.umd.js:13181)
    at Object.eval [as updateRenderer] (VM7993 AppComponent.ngfactory.js:78)
    at Object.debugUpdateRenderer [as updateRenderer] (VM7606 core.umd.js:13163)
    at checkNoChangesView (VM7606 core.umd.js:12283)
    at callViewAction (VM7606 core.umd.js:12650)
    at execEmbeddedViewsAction (VM7606 core.umd.js:12628)
    at checkNoChangesView (VM7606 core.umd.js:12282)
    at callViewAction (VM7606 core.umd.js:12650)

This is not a problem when the input is not inserted conditionally but rather placed in the template right away. I'm not sure if that is by design or not but it caused me some hours of debugging until I realised it was the combination of mat-hint inside a mat-form-field whom\s matInput is rendered using *ngIf.

What is the expected behavior?

I expected this to just work, but again, maybe this is by design but then we should document somewhere that matInputs aren't allowed to be inserted using structural directives.

What is the current behavior?

Described above.

What are the steps to reproduce?

Here's a plunk that reproduces the error: http://plnkr.co/edit/MrUvFVKIW1BoX9d96eeu?p=preview

Simply check the console.

What is the use-case or motivation for changing an existing behavior?

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

I'm using @angular/material version 2.0.0-beta.11 but the plunk uses the latest version. Unfortunately I can't tell if this error has been thrown in older versions as well.

Is there anything else we should know?

Yeah, you're all doing an amazing job. Thanks for that.

P3 materiaform-field feature

Most helpful comment

I am having this issue too, but it is with mat-error instead of mat-hint:

<input *ngIf="!structure[key].options" matInput [formControlName]="key" 
    [placeholder]="structure[key].label">

<mat-select *ngIf="structure[key].options" 
    [placeholder]="structure[key].label" [formControlName]="key">
        <mat-option *ngFor="let value of structure[key].options" 
            [value]="value.value">{{value.label}}</mat-option>
</mat-select>

All 9 comments

Rather than conditional <input> elements, I suggest just setting up a dynamic placeholder binding:

html:

<input [placeholder]="myPlaceholder"

typescript:

myPlaceholder = 'dynamic placeholder' 

Though I was planning on looking into making form-field pieces more swappable

@mmalerba while your suggestion will work with a normal string, but I don't see it usable with the current i18n implementation of angular for different languages. 馃槥 Or am I missing something?

(while you are thinking about "form-field pieces that are more swappable" it would be really useful for me that the <mat-form-field> recognizes matInputs that are placed via <ng-content> in it. Here a stackblitz with an example of what I mean...)

I am having this issue too, but it is with mat-error instead of mat-hint:

<input *ngIf="!structure[key].options" matInput [formControlName]="key" 
    [placeholder]="structure[key].label">

<mat-select *ngIf="structure[key].options" 
    [placeholder]="structure[key].label" [formControlName]="key">
        <mat-option *ngFor="let value of structure[key].options" 
            [value]="value.value">{{value.label}}</mat-option>
</mat-select>

You can localize a placeholder using:

<mat-form-field>
    <input *ngIf="isFile()" matInput i18n-placeholder placeholder="Enter file name" formControlName="filename">
    <input *ngIf="isDirectory()" matInput i18n-placeholder placeholder="Enter directory name" formControlName="filename">
</mat-form-field>

But not the most DRY approach. According to the documentation:

_"The placeholder can be specified either via a placeholder attribute on the input or a element in the same form field as the matInput."_
https://material.angular.io/components/input/overview

So a better way of localizing multiple types of placeholders would be:

<mat-form-field>
    <mat-placeholder i18n *ngIf="isFile()">Enter file name</mat-placeholder>
    <mat-placeholder i18n *ngIf="isDirectory()">Enter directory name</mat-placeholder>
    <input matInput formControlName="filename" />
</mat-form-field>

And that might solve your errors too as you will only have the single input?

Hitting the same issue with mat-hint and ngSwitch:

<mat-form-field
  *ngFor="let field of schema.fields"
  floatLabel="always"
  [hintLabel]="field.hint">

  <ng-container [ngSwitch]="field.type || 'text'">

    <input matInput
      *ngSwitchCase="'text'"
      [placeholder]="field.placeholder"
      [formControlName]="field.name"
      [errorStateMatcher]="matcher"
      [required]="field.required">

    <mat-select
      *ngSwitchCase="'select'"
      [placeholder]="field.placeholder"
      [formControlName]="field.name"
      [errorStateMatcher]="matcher"
      [required]="field.required">

    </mat-select>

  </ng-container>

</mat-form-field>

Tried switching to using <mat-hint> element instead to no avail:

<mat-form-field
  *ngFor="let field of schema.fields"
  floatLabel="always">

  <mat-hint
    align="start">
    {{ field.hint }}
  </mat-hint>

  <ng-container [ngSwitch]="field.type || 'text'">

    <input matInput
      *ngSwitchCase="'text'"
      [placeholder]="field.placeholder"
      [formControlName]="field.name"
      [errorStateMatcher]="matcher"
      [required]="field.required">

    <mat-select
      *ngSwitchCase="'select'"
      [placeholder]="field.placeholder"
      [formControlName]="field.name"
      [errorStateMatcher]="matcher"
      [required]="field.required">

    </mat-select>

  </ng-container>

</mat-form-field>

Would definitely love to see "more swappable form-field pieces" 馃檹

P.S. thanks to the Angular team for the good work they've done so far 鉂わ笍 馃憦

__Update__: was able to workaround by grouping the all form components together into a single conditional render -- basically render the whole mat-form-field and its children together (not DRY but it works):

<ng-container *ngFor="let field of schema.fields">

  <ng-container [ngSwitch]="field.type || 'text'">
    <ng-container *ngSwitchCase="'text'">

      <mat-form-field floatLabel="always">

        <input matInput
          [placeholder]="field.placeholder"
          [formControlName]="field.name"
          [errorStateMatcher]="matcher"
          [required]="field.required">

        <mat-hint>
          {{ field.hint }}
        </mat-hint>

      </mat-form-field>

    </ng-container>
    <ng-container *ngSwitchCase="'select'">

      <mat-form-field floatLabel="always">

        <mat-select
          [placeholder]="field.placeholder"
          [formControlName]="field.name"
          [errorStateMatcher]="matcher"
          [required]="field.required">

        </mat-select>

        <mat-hint>
          {{ field.hint }}
        </mat-hint>

      </mat-form-field>

    </ng-container>
  </ng-container>

</ng-container>

Also suffering hard from this issue, requiring me to have a lot of redundant code, as errors and hints cannot be build together dynamically!

Any input on this issue from the Angular team?

I have an open question on stackoverflow where I encountered the same issue. In my case I need to dynamically swap two inputs: one that does and one that does not have have a custom errorStateMatcher.

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

<mat-hint [id]="field.name">No error</mat-hint>
Was this page helpful?
0 / 5 - 0 ratings

Related issues

alanpurple picture alanpurple  路  3Comments

Miiekeee picture Miiekeee  路  3Comments

crutchcorn picture crutchcorn  路  3Comments

shlomiassaf picture shlomiassaf  路  3Comments

RoxKilly picture RoxKilly  路  3Comments