Skip to content

form-field: support swapping various child components with ngIf #7737

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

Open
0x-r4bbit opened this issue Oct 12, 2017 · 11 comments
Open

form-field: support swapping various child components with ngIf #7737

0x-r4bbit opened this issue Oct 12, 2017 · 11 comments
Labels
area: material/form-field feature This issue represents a new feature or feature request rather than a bug or bug fix P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent

Comments

@0x-r4bbit
Copy link

0x-r4bbit commented Oct 12, 2017

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.

@mmalerba mmalerba added the P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent label Oct 13, 2017
@mmalerba
Copy link
Contributor

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 mmalerba changed the title bug: mat-hint causes change detection error when input is inserted conditionally form-field: support swapping various child components with ngIf Oct 13, 2017
@mmalerba mmalerba added the feature This issue represents a new feature or feature request rather than a bug or bug fix label Oct 13, 2017
@bboehm86
Copy link

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

@csbenjamin
Copy link

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>

@kmturley
Copy link

kmturley commented Feb 6, 2018

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?

@keego
Copy link

keego commented Oct 9, 2018

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>

@tommueller
Copy link

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?

@brettwp
Copy link

brettwp commented Mar 5, 2019

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.

@konrad-garus
Copy link

It seems to be the same as #16209

@zlodeev
Copy link

zlodeev commented May 13, 2020

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>

@jonathanturcotte
Copy link

I'm also having this issue, except the solution keego posted above didn't work for me. I've tried conditionally rendering a whole new mat-form-field for each input, or just conditionally rendering the inputs inside of it with ng-content/template wrappers - but no matter what I do I keep hitting mat-form-field must contain a MatFormFieldControl whenever angular goes to show the second element. If I don't wrap the problematic elements in <mat-form-field> (and ignore how hints/labels no longer appear properly) I no longer hit the error and the form behaves as expected.

Here's my code:

  <ng-container [ngSwitch]="type">
    <ng-container *ngSwitchCase="'boolean'">
      <mat-form-field>
        <mat-label>Value</mat-label>
        <mat-hint>{{example}}</mat-hint>
        <mat-checkbox [formControl]="formControl.get('__value')"></mat-checkbox>
      </mat-form-field>
    </ng-container>
    <ng-container *ngSwitchDefault>
      <mat-form-field>
        <mat-label>Value</mat-label>
        <mat-hint>{{example}}</mat-hint>
        <input matInput [formControl]="formControl.get('__value')">
      </mat-form-field>
    </ng-container>
  </ng-container>
  <mat-form-field>
    <mat-label>Type</mat-label>
    <mat-select
      (selectionChange)="setType()"
      [formControl]="formControl.get('__type')">
      <mat-option *ngFor="let option of this.types"
        [value]="option.value">
        {{option.label}}
        </mat-option>
    </mat-select>
  </mat-form-field>

Any help with this would be greatly appreciated!

@idimash
Copy link

idimash commented Jun 22, 2022

I have the same problem, the placeholder does not update if the control has been changed inside the mat-form-field template. MatFormField requires a call chageDetector after view update. Found two solutions:
1:

@ViewChild('formField', { read: MatFormField })
private _formField: MatFormField;

public ngAfterViewChecked(): void {		
	this._formField?.ngAfterViewInit();
}

2:

private _formFieldCdr: ChangeDetectorRef;

@ViewChild('formField', { read: ViewContainerRef })
private _formFieldVcr: ViewContainerRef;

public ngDoCheck(): void {		
	this._formFieldCdr?.markForCheck();
}

public ngAfterViewInit(): void {		
	this._formFieldCdr = this._formFieldVcr?.injector.get(ChangeDetectorRef);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: material/form-field feature This issue represents a new feature or feature request rather than a bug or bug fix P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
Projects
None yet
Development

No branches or pull requests