1

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.

Yong Shun
  • 35,286
  • 4
  • 24
  • 46
LeO
  • 4,238
  • 4
  • 48
  • 88
  • https://stackoverflow.com/questions/56887035/custom-controls-with-reactive-forms/56893298#56893298 – Eliseo May 05 '22 at 07:00

2 Answers2

2

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

Yong Shun
  • 35,286
  • 4
  • 24
  • 46
  • 1
    Thanx - this works great. Especially the `inUrl` doesn't need to be manually injected (I have seen samples of this). But this means basically that my CustomControl is a wrapper around another Control (e.g. I could set an internal error state and an external one). Anyway thats interesting from a technical point of view. I'm happy with the provided sample :-) – LeO Nov 05 '21 at 08:49
0

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.

LeO
  • 4,238
  • 4
  • 48
  • 88