19

I am following this tutorial from the Angular Material website about creating a custom form control. There are no examples in the tutorial regarding how to respect form control errors when there are validation errors.

custom-text.component.html

<mat-form-field class="example-full-width">
    <mat-select [placeholder]="placeholder" #select [formControl]="control">
        <mat-option *ngFor="let food of foods" [value]="food">
            {{food}}
        </mat-option>
    </mat-select>
    <mat-error>This is required.</mat-error>
</mat-form-field>

custom-text.component.ts

import { Component, ViewChild, HostBinding, Input, ChangeDetectionStrategy, Optional, Self, DoCheck, OnInit, NgZone } from '@angular/core';
import { ControlValueAccessor, NgControl, NgForm, FormGroupDirective, FormControlDirective, FormControlName, FormControl, FormBuilder } from '@angular/forms';
import { MatFormFieldControl, MatSelect, CanUpdateErrorState, ErrorStateMatcher } from '@angular/material';

@Component({
    selector: 'custom-text',
    templateUrl: './custom-text.component.html',
    styleUrls: [
        './custom-text.component.scss'
    ],
    providers: [
        {
            provide: MatFormFieldControl,
            useExisting: CustomTextComponent
        }
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomTextComponent implements ControlValueAccessor, OnInit, DoCheck {

    @Input()
    foods: string[];

    @Input()
    get errorStateMatcher(): ErrorStateMatcher {
        return this.select.errorStateMatcher;
    }
    set errorStateMatcher(val) {
        this.select.errorStateMatcher = val;
    }

    @Input()
    get placeholder() {
        return this.select.placeholder;
    }
    set placeholder(plh) {
        this.select.placeholder = plh;
        this.stateChanges.next();
    }

    @Input()
    get value() {
        return this.select.value;
    }
    set value(val) {
        this.select.value = val;
        this.stateChanges.next();
    }

    @ViewChild('select')
    select: MatSelect;

    control: FormControl;

    constructor(
        @Optional() @Self() ngControl: NgControl,
        @Optional() private _controlName: FormControlName) {
        if (ngControl) {
            ngControl.valueAccessor = this;
        }
    }

    ngOnInit(): void {
        this.control = this._controlName.control;
    }

    ngDoCheck(): void {
        this.select.updateErrorState();
    }

    writeValue(obj: any): void {
        this.value = obj;
    }
    registerOnChange(fn: any): void {
        this.select.registerOnChange(fn);
    }
    registerOnTouched(fn: any): void {
        this.select.registerOnTouched(fn);
    }
    setDisabledState?(isDisabled: boolean): void {
        this.select.setDisabledState(isDisabled);
    }


}

app.component.html

<div style="text-align:center">
  <form class="example-form" [formGroup]="myForm" (submit)="submitForm()">
    <custom-text [foods]="[null, 'burger', 'spaghetti', 'fries']" 
      formControlName="selectedFood" 
      [errorStateMatcher]="matcher"></custom-text>
    <button>Submit</button>
  </form>
</div>

Basically, I injected FormControlName instance into the custom control.

constructor(
    @Optional() private _controlName: FormControlName) {
....
ngOnInit(): void {
    this.control = this._controlName.control;
}

I then bind it's control property into the inner mat-select control.

<mat-select [placeholder]="placeholder" #select [formControl]="control">

I then call this.select.updateErrorState inside ngDoCheck.

Here's the link to StackBlitz: https://stackblitz.com/edit/angular-c4ufpp

Is there is a better or more standard way of this?

1 Answer 1

7

Instead of using the Validator interface, which create the cyclic error dependency, add errorState in your custom component that checks the ngControl that was injected into the constructor, like this:

get errorState() {
  return this.ngControl.errors !== null && !!this.ngControl.touched;
}

This makes DoCheck unnecessary. In your parent component, instead of using errorMatcher, use normal Angular validators, like this:

selectedFood = new FormControl('burger', [Validators.required]);
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.