Angular Material: mat-error with custom ControlValueAccessor

Angular Material: mat-error with custom ControlValueAccessor

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=" &euro;"
      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.

Ich bin freiberuflicher Senior Full-Stack Web-Entwickler (Angular, TypeScript, C#/.NET) im Raum Frankfurt/Main. Mit Leidenschaft für Software-Design, Clean Code, moderne Technologien und agile Vorgehensmodelle.

0 Kommentare

Eine Antwort hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.