Dynamically create a <mat-error> component and have it be projected into the parent <mat-form-field> component properly

Viewed 4293

Using the ComponentFactoryResolver to dynamically create a component is straight forward, but it doesn't seem to project the created component into the parent when I try and do this for the Angular Material <mat-error> component within a <mat-form-field>. I've been using Netanel Basal's example with Bootstrap components, which works great for generating form control errors, but due to the content projection used in Angular Material with <mat-error> I can't seem to get it to work for Angular Material v8.2.1.

Is it possible to dynamically create a <mat-error> in a <mat-form-field> and have it content project properly?

3 Answers

I've tried this before - MatFormField picks up MatErrors via ContentChildren. Since it's content projection, dynamically adding or removing elements like MatError doesn't work -- the content template is compiled and handled differently than other templates.

You mentioned in a comment that you're trying to handle consistent error messaging across templates...why not add a directive to the <mat-error> element that controls the error messaging based on the parent control's form validation state?

If you're looking to control templates, though, you can't use a directive. You can, however, create a component and use it like a directive:

Create new file mat-error-messages.component.ts, this is full code of file:

import {AfterViewInit, Component, Injector} from '@angular/core';
import {MatFormField, MatFormFieldControl} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';

@Component({
    selector: '[matErrorMessages]',
    template: '{{ error }}'
})
export class MatErrorMessagesComponent implements AfterViewInit {

    public error = '';
    private inputRef: MatFormFieldControl<MatInput>;

    constructor(private _inj: Injector) {
    }

    public ngAfterViewInit(): void {
        // grab reference to MatFormField directive, where form control is accessible.
        const container = this._inj.get(MatFormField);
        this.inputRef = container._control;

        // sub to the control's status stream
        this.inputRef.ngControl.statusChanges.subscribe(this.updateErrors);
    }


    private updateErrors = (state: 'VALID' | 'INVALID'): void => {
        if (state === 'INVALID') {
            // active errors on the FormControl
            const controlErrors = this.inputRef.ngControl.errors;

            // just grab one error
            const firstError = Object.keys(controlErrors)[0];

            if (firstError === 'required')
                {this.error = 'This field is required.';}

            if (firstError === 'minlength')
                {this.error = 'This field should be longer.';}

            if (firstError === 'error from my own custom validator')
                {this.error = 'You get the point.';}
            // .....
        }
    };
}

then in template....

<mat-error matErrorMessages></mat-error>

This way, you let the MatFormField control the presence of the MatError as it's supposed to do, but you control the content of the error element in a localized, clean way.

stackblitz of the above: https://stackblitz.com/edit/angular-sjkfft

You have to dig in to the Material components' private members, which is questionable practice, and can break with library updates, but it works.

I actually have a repo for this type of error messaging that was written for a much older version of @angular/material, but I was also able to get a hold of the Validator itself for better error messaging, and was injecting a whole list of custom validators/errors for that validator into the module: https://github.com/joh04667/material-error-messages

this just another alternative solution ,inspired by ngx-valdemort to create a component for handle validation error message of control , it is just required to be child in the formGroup body and pass the name of the form control and it 's not restricted to angular material component.

@Component({
  selector: 'error-summary',
  templateUrl: './error-summary.component.html',
  styleUrls: ['./error-summary.component.css']
})
export class ErrorSummaryComponent  {

  @Input() control:string;

  @Input() visible:any;

  constructor(private controlContainer: ControlContainer) { }

  get form():FormGroup {
    return this.controlContainer.control  as FormGroup;
  }

  get formControl() :AbstractControl{
    return this.form.get(this.control) as AbstractControl;
  }

  get isNotValid() {
    return this.formControl.invalid && (this.formControl.touched || this.formControl.dirty)
  }

}

template

<ng-container *ngIf="isNotValid"  >
  <ng-container *ngIf="formControl.hasError('required')"> this is required </ng-container>
  <ng-container *ngIf="formControl.hasError('email')">this not valid email format</ng-container>
  <ng-container *ngIf="formControl.hasError('maxlength')">max ...</ng-container>
  <ng-container *ngIf="formControl.hasError('minlength')">min ...</ng-container>
</ng-container>

and can be use like this

<form [formGroup]="form">

  <mat-form-field>
        <input matInput placeholder="Enter your email" formControlName="email">
  </mat-form-field>
    <error-summary control="email" ></error-summary>

  ...

</form>

demo

Accepted answer worked only when user interacts with the field. What if user never interacts with fields and directly tries to submit the form?

So here i will improve answer:

Note: here i used transloco to translate texts, you can remove it and use your way to do that.

1 - Create new file mat-error-messages.component.ts with this code:

import {AfterViewInit, Component, Injector} from '@angular/core';
import {MatFormField, MatFormFieldControl} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input';
import {TranslocoService} from '@ngneat/transloco';

@Component({
    selector: '[matErrorMessages]',
    template: '{{ error }}'
})
export class MatErrorMessagesComponent implements AfterViewInit {

    public error = '';
    private inputRef: MatFormFieldControl<MatInput>;

    constructor(
        private _inj: Injector,
        private _translocoService: TranslocoService
    ) {
    }

    public ngAfterViewInit(): void {
        // grab reference to MatFormField directive, where form control is accessible.
        const container = this._inj.get(MatFormField);
        this.inputRef = container._control;

        // sub to the control's status stream
        this.inputRef.ngControl.statusChanges.subscribe(this.updateErrors);
    }


    private updateErrors = (state: 'VALID' | 'INVALID'): void => {
        if (state === 'INVALID') {
            // active errors on the FormControl
            const controlErrors = this.inputRef.ngControl.errors;
            // just grab one error
            const firstError = Object.keys(controlErrors)[0];
            const varList = Object.values(controlErrors)[0]; // variables of error
            switch (firstError) {
                case 'required':
                case 'email':
                case 'minlength':
                case 'maxlength':
                case 'pattern':
                    this.error = this._translocoService.translate(`validation.${ firstError }`, varList);
                    break;
            }
        }
    };
}

2- In your submit function, at first of function should be like this:

handleSubmit(): void {
    if (this.formGroup.invalid) {
      Object.keys(this.formGroup.controls).forEach(key => {
        const value: AbstractControl = this.formGroup.get(key);
        if (value instanceof FormGroup) {
          value.markAsDirty();
          value.markAllAsTouched();
          value.updateValueAndValidity({ onlySelf: true });
        } else {
          value.markAsDirty();
          value.markAsTouched();
          value.updateValueAndValidity({ onlySelf: true });
        }
      });
      return;
    }

    ................ your code
}

Note: To avoid code duplication you can set it in function in other class and just call it with pass formGroup variable.

3- In all mat-form-field of forms add:

<mat-error matErrorMessages></mat-error>

like this:

<mat-form-field>
   <mat-label>Label</mat-label>
   <textarea matInput [formControlName]="'name'"></textarea>
   <mat-error matErrorMessages></mat-error>
</mat-form-field>

4- My translation texts:

{
    "validation": {
        "required" : "This field is required.",
        "email": "Please enter a valid email address.",
        "pattern": "This value has syntax error.",
        "minlength": "Please enter at least {{requiredLength}} characters, you have entered only {{actualLength}}.",
        "maxlength": "Please enter no more than {{requiredLength}} characters, you have entered only {{actualLength}}."
    }
}
Related