Since I’m getting more and more on the Angular frontend train, also some annoying problems arise, which I want to address in this blog. This post describes one of them…
Given you are using @angular/material
in combination with Angular Reactive Forms. It has a nice functionality to show errors on mat-form-field
components, if their state is invalid, by using mat-error
. It’s really working well, except when you are using a custom ControlValueAccessor
for your input fields. In this case the mat-error
is just not shown 🙁
Example
I’ll give you an example: I have the following custom ControlValueAccessor
named inputSuffix
, which formats the value of an input field with a custom suffix, when the input is not focussed:
import { Directive, ElementRef, HostListener, Input, Renderer2, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Directive({ selector: '[inputSuffix]', providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputSuffixDirective), multi: true }] }) export class InputSuffixDirective implements ControlValueAccessor { @Input() suffix: string; @HostListener('focusin', ['$event.target.value']) onFocusIn; @HostListener('blur', ['$event.target.value']) onBlur; @HostListener('keyup', ['$event.target.value']) onKeyUp; constructor( private readonly renderer: Renderer2, private readonly elementRef: ElementRef) { } writeValue(numVal: string): void { const fmtVal = this.formatValue(numVal); this.setDomValue(fmtVal); } registerOnChange(changeModelValueCallback: (_: any) => void): void { this.onFocusIn = (inputVal) => { const val = inputVal ? this.getBlankValue(inputVal) : ''; this.setDomValue(val); }; this.onKeyUp = (inputVal) => { const val = inputVal ? this.getBlankValue(inputVal) : ''; changeModelValueCallback(val); }; } registerOnTouched(): void { this.onBlur = (val: string) => { const fmtVal = this.formatValue(val); this.setDomValue(fmtVal); this.elementRef.nativeElement.blur(); }; } private setDomValue(domVal: string) { this.renderer.setProperty(this.elementRef.nativeElement, 'value', domVal ); } private getBlankValue(val: string): string { return (val && this.suffix) ? val.replace(this.suffix, '') : val; } private formatValue(val: string): string { return val + (this.suffix || ''); } }
input-suffix.directive.ts
Don’t be afraid of this code, in general the mat-error
problem arises for any kind of ControlValueAccessor
, this is just an example… what the code does is to attach the defined suffix
input parameter to the underlying value whenever the input is not in focus and on focus it removes the suffix to let the user input the blank value.
This value accessor can now be used in a form in combination with mat-form-field
and matInput
:
<form [formGroup]="inputForm"> <mat-form-field> <mat-label>Deposit</mat-label> <input matInput inputSuffix suffix=" €" formControlName="deposit"> <mat-error>Deposit should be at least 100€</mat-error> </mat-form-field> </form>
form.component.html
import { Component } from '@angular/core'; import { FormGroup, FormBuilder, FormControl, ValidatorFn, AbstractControl, ValidationErrors, Validators } from '@angular/forms'; @Component({ selector: 'app-form', templateUrl: './form.component.html' }) export class FormComponent { readonly inputForm: FormGroup = this.formBuilder.group({ deposit: [ 123, [ this.minValidator(100) ] ] }); constructor(private readonly formBuilder: FormBuilder) { } private minValidator(min: number): ValidatorFn { return (control: AbstractControl): ValidationErrors => { return (control.value && min) ? Validators.min(min)(control) : null; }; } }
form.component.ts
The problem
As we can see, the FormComponent
defines a min value validator on the form field deposit
. What we expect now is this validation to kick in and show the mat-error
when the form field gets invalid, e.g. when the user changes the value to 10
. Instead, nothing happens:
The solution
I was fiddling around, trying this and that (including handling the validation for myself without using mat-error
), until I found the solution.
matInput
has a property errorStateMatcher
of type ErrorStateMatcher
, which comes for the rescue. It normally allows to change when an error message is shown. It turns out for our scenario with a custom ControlValueAccessor
that’s it has to be defined to get the mat-error
to be shown.
So you can define a property in your component code:
... readonly errorStateMatcher: ErrorStateMatcher = { isErrorState: (ctrl: FormControl) => (ctrl && ctrl.invalid) }; ...
form.component.ts
… which then can be used in the component template:
... <input matInput [errorStateMatcher]="errorStateMatcher" inputSuffix suffix=" €" formControlName="deposit"> ...
form.component.html
And it works as expected:
In this case the defined errorStateMatcher
will show the mat-error
directly when the input form control gets invalid. Of course other logic is possible here which fits your needs.