Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 289
mtpultz Avatar asked Dec 13 '22 10:12

mtpultz


1 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

like image 127
joh04667 Avatar answered May 12 '23 00:05

joh04667