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

3

9

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?

Scottie answered 22/10, 2019 at 17:28 Comment(11)
Got some code we can look at..?Henka
Why do you need to do that? A mat-form-field only needs to have one mat-error - it is simple to dynamically change the error content.Scad
It'd be helpful to post a stackblitz and the version of @angular/material you're using....MatFormField has gone through some changesKnavery
@G.Tranter we didn't want to add the mat-error to every form field when it could be dynamically generated on demand. We can absolutely have a mat-error and then apply a method {{ getError('formGroup.formControl') }}, and do a look up of the error message, but we'd be adding that method to every component and it just adds template bloat.Scottie
@Knavery we're using Angular Material v8.2.1Scottie
@ShyAgam the code is the Netanel Basal StackBlitz example (stackblitz.com/edit/netanel-control-er) from the indicated article, but replacing the ControlErrorComponent or dynamically loading the mat-error component and have it content project properly.Scottie
@Scottie how is dynamically adding <mat-error>{{ getError('formGroup.formControl') }}</mat-error> to <mat-form-field> better than dynamically adding <span>{{ getError('formGroup.formControl') }}</span> to <mat-error>?Scad
@G.Tranter that example you're referencing is what I was saying I would need to add manually to each form control if I can't do it dynamically.Scottie
@mt@Scottie If you have that many form fields and the rules for computing errors are consistent across them then you can just create your own component based on mat-form-field which handles the error handling.Brigitte
you can "simulate" #55934135. I just updated to take account "blur"Sergent
There is an related discussion.Niello
K
16

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

Knavery answered 25/10, 2019 at 21:39 Comment(5)
So if I were to use a directive on the mat-error instead would I be able to access the FormControl to determine the error? I want to stay away from doing something like <mat-error>{{ getErrorMessage('formControlName') }}</mat-error> and would be quite happy to have <mat-error appErrorMessage></mat-error> for each since it would avoid having a getError method being added to every component. I kind of thought it might not be possible due to content projection esp after experimenting with it, but was hoping I was wrong, but the directive would good alternative.Scottie
Yup! In the code above, the injector grabs the MatFormField. On that MatFormField, the FormControl is the MatFormField._control.ngControl. Come to think of it, you may just be able to inject FormControl directly to get the reference, but I'm not able to test that at the moment.Knavery
Ok, I checked, you can't inject a FormControl or NgControl. However, I did get this stackblitz up and running: stackblitz.com/edit/angular-sjkfft. Check out the repo I linked to see how to get custom Validators and rich data into the errors!Knavery
This way not work when submit form, it only work when change value of input!Ce
Yes, you would need an event to run change detection on form controls before submitting a form. Unblurred inputs are valid by default. You can do this via the FormGroup.Knavery
S
2

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 🚀🚀

Squabble answered 31/10, 2019 at 13:56 Comment(0)
C
1

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}}."
    }
}
Ce answered 24/8, 2022 at 1:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.