'Angular Material custom form field doesn't show mat-error

I thought I have almost the same example but somehow the control tricks me :-/

<form [formGroup]="form">
    <app-ref-urlcheck [maxLen]="20" formControlName="url"></app-ref-urlcheck>
</form>

and the template looks like

<mat-form-field>
  <input matInput #inUrl="ngModel" [(ngModel)]="value" type="url" [attr.maxlength]="maxLen" [errorStateMatcher]="errorStateMatcher"
    (input)="changeInput(inUrl.value)" [disabled]="isDisabled" [value]="strUrl" 
    placeholder="Homepage" />
  <mat-error>test error</mat-error> <!-- doesn't show up - neither the next -->
  <mat-error *ngIf="(inUrl.touched && inUrl.invalid)">This field is required</mat-error>
</mat-form-field>

and the main content

import { Component, HostListener, Input, OnInit } from '@angular/core';
import { AbstractControl, ControlValueAccessor, FormControl, NgControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatFormFieldControl } from '@angular/material/form-field';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-ref-urlcheck',
  templateUrl: './ref-urlcheck.component.html',
  styleUrls: ['./ref-urlcheck.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: RefURLcheckComponent
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: RefURLcheckComponent
    }
  ]
})
export class RefURLcheckComponent implements OnInit, ControlValueAccessor, MatFormFieldControl<any>, Validator {
  @Input() maxLen = 254;
  strUrl: string;

  onChange = (changedUrl) => { };
  onTouched = () => { };
  isDisabled = false;
  touched = false;

  @HostListener('focusin', ['$event.target.value']) onFocusIn;

  constructor() { }
  onContainerClick(event: MouseEvent): void {
    throw new Error('Method not implemented.');
  }
  setDescribedByIds(ids: string[]): void {
    throw new Error('Method not implemented.');
  }
  userAriaDescribedBy?: string;
  autofilled?: boolean;
  controlType?: string;
  errorState: boolean;
  disabled: boolean;
  required: boolean;
  shouldLabelFloat: boolean;
  empty: boolean;
  focused: boolean;
  ngControl: NgControl;
  placeholder: string;
  id: string;
  stateChanges: Observable<void>;
  value: any;

  ngOnInit(): void {
  }

  setDisabledState?(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }
  registerOnTouched(onTouched: () => {}): void {
    this.onTouched = onTouched;
  }
  registerOnChange(onChange: (changedValue: string) => {}): void {
    this.onChange = onChange;

    this.onFocusIn = (inputVal) => {
      console.log('focus in', inputVal);
      this.markAsTouched();
    };


  }

  writeValue(value: string): void {
    this.strUrl = value;
  }

  markAsTouched() {
    if (!this.touched) {
      this.onTouched();
      this.touched = true;
    }
  }

  changeInput(inVal: string) {
    this.onChange(inVal);
    this.markAsTouched();
  }



  readonly errorStateMatcher: ErrorStateMatcher = {
    isErrorState: (ctrl: FormControl) => {
      console.log('errorStateMatch...')
      this.errorState = true;
      return (ctrl && ctrl.invalid);
    }
  };

  validate(control: AbstractControl): ValidationErrors | null {
    if (control?.value.length <= 5) {
      this.errorState = true;
      return {
        tooShort: true
      };
    }
    this.errorState = false;
    return null;
  }
}

Same question as in the referred example: How to display <mat-error>? It doesn't even show up anyhow.



Solution 1:[1]

Reused the attached code and suspect that the FormControl didn't update with an error when the validation is failed.

When the validation fails, should set the error to FormControl as below:

this.inUrl.control.setErrors({ tooShort: true });
import { ViewChild } from '@angular/core';

export class RefURLcheckComponent
  implements OnInit, ControlValueAccessor, MatFormFieldControl<any>, Validator
{
  @ViewChild('inUrl', { static: true }) inUrl: NgControl;

  ...

  validate(control: AbstractControl): ValidationErrors | null {
    if (control?.value?.length <= 5) {
      this.errorState = true;
      this.inUrl.control.setErrors({ tooShort: true });
      return {
        tooShort: true,
      };
    }

    this.errorState = false;
    this.inUrl.control.setErrors(null);
    return null;
  }
}

Sample Demo on StackBlitz

Solution 2:[2]

Thanx to @Yong Shun I figured out how to do the management properly. It seems like that one needs to wrap an input field with the regular template driven approach (for the updates of the input field) and use this component as reactive one. So my custom control has inside a control which handles the states.

I removed from my original code all the unnecessary stuff and included some minor hints from the guideline.

So here is my (streamlined) working example - for the sake when somebody needs it again.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Yong Shun
Solution 2 LeO