I recently spent hours going over alot of resources which none seems to work exactly out of the box as expected so I cam,e up with this example on stackblitz to the best of my ability.
Could some one please tell me if this is the best way to accomplish the following?
- Create a custom form control using angular material, errorStateMatcher, and ngControl.
- Have this component work as "out of the box" or natively as possible intertwining any parent validations put on the custom formControl as well as custom validations inside the custom form control.
- GOAL: please help remove or add any code needed to make this function better. Thanks in advance.
Link to stackblitz: https://stackblitz.com/edit/angular-mat-reactive-form-control-ddssy1
Custom Control Html:
<mat-form-field appearance="outline" [floatLabel]="'always'" class="example-full-width">
<mat-label>{{label}}</mat-label>
<input matInput [id]="id" #input [formControl]="control" [placeholder]="placeholder"/>
<mat-hint>Required</mat-hint>
<mat-error>{{errorMessage}}</mat-error>
</mat-form-field>
Custom Control TS:
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
Component,
ViewChild,
HostBinding,
Input,
ChangeDetectionStrategy,
Optional,
Self,
DoCheck,
OnInit,
} from "@angular/core";
import {
ControlValueAccessor,
NgControl,
FormControlName,
FormControl,
} from "@angular/forms";
import {
MatFormFieldControl,
ErrorStateMatcher, MatInput
} from "@angular/material";
import { Subject } from "rxjs";
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null): boolean {
return !!(control && control.invalid && (control.dirty || control.touched));
}
}
@Component({
host: {
'(focusout)': 'onTouch()',
"[id]": "id",
"[attr.aria-describedby]": "describedBy"
},
selector: "custom-input",
templateUrl: "./custom-select.component.html",
styleUrls: ["./custom-select.component.scss"],
providers: [
{
provide: MatFormFieldControl,
useExisting: CustomSelectComponent
}
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomSelectComponent
implements ControlValueAccessor, OnInit, DoCheck {
static nextId = 0;
@HostBinding() id = `input-${CustomSelectComponent.nextId++}`;
@HostBinding("attr.aria-describedby") describedBy = "";
@ViewChild("Input") input: MatInput;
@Input() placeholder: string;
@Input() label: string;
@Input() disabled: boolean;
@Input('value') _value: any
get value() {
return this._value || null;
}
set value(val) {
this._value = val;
}
public control: FormControl;
public errorMessage: string;
get errorState(){
console.log('error state!');
return this.errorMatcher.isErrorState(this.ngControl.control as FormControl, null);
}
onChange: (value: any) => void;
onTouch: () => void;
constructor(
@Optional() @Self() public ngControl: NgControl,
@Optional() private _controlName: FormControlName,
private errorMatcher: ErrorStateMatcher,
) {
if (ngControl) {
ngControl.valueAccessor = this;
}
}
ngOnInit(): void {
this.control = this._controlName.control;
this.control.valueChanges.subscribe(res=>{
if(res){
this.validate();
}
})
this.control.markAsTouched();
this.validate();
}
ngDoCheck(): void {
if(this.control){
this.validate();
}
}
writeValue(obj: any): void {
this._value = obj;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
validate(){
console.log(this.control);
this.errorMessage = null;
if(this.control.errors && this.control.errors.required && !this.control.value){
this.errorMessage = "Required";
return;
}
if(this.control?.value?.length < 3){
this.control.setErrors({ invalid: true});
this.errorMessage = 'Length must be at least 3 characters.';
return
}
}
}
PARENT HTML:
<div style="text-align:center">
<form class="example-form" [formGroup]="myForm" (submit)="submitForm()">
<custom-input placeholder="Favorite Food" label="Food" formControlName="food" [required]="true"></custom-input>
<button>Submit</button>
</form>
</div>
<div>
Form is valid? {{myForm.valid}}
</div>
PARENT TS:
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
Component,
ViewChild,
HostBinding,
Input,
ChangeDetectionStrategy,
Optional,
Self,
DoCheck,
OnInit,
} from "@angular/core";
import {
ControlValueAccessor,
NgControl,
FormControlName,
FormControl,
} from "@angular/forms";
import {
MatFormFieldControl,
ErrorStateMatcher, MatInput
} from "@angular/material";
import { Subject } from "rxjs";
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null): boolean {
return !!(control && control.invalid && (control.dirty || control.touched));
}
}
@Component({
host: {
'(focusout)': 'onTouch()',
"[id]": "id",
"[attr.aria-describedby]": "describedBy"
},
selector: "custom-input",
templateUrl: "./custom-select.component.html",
styleUrls: ["./custom-select.component.scss"],
providers: [
{
provide: MatFormFieldControl,
useExisting: CustomSelectComponent
}
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomSelectComponent
implements ControlValueAccessor, OnInit, DoCheck {
static nextId = 0;
@HostBinding() id = `input-${CustomSelectComponent.nextId++}`;
@HostBinding("attr.aria-describedby") describedBy = "";
@ViewChild("Input") input: MatInput;
@Input() placeholder: string;
@Input() label: string;
@Input() disabled: boolean;
@Input('value') _value: any
get value() {
return this._value || null;
}
set value(val) {
this._value = val;
}
public control: FormControl;
public errorMessage: string;
get errorState(){
console.log('error state!');
return this.errorMatcher.isErrorState(this.ngControl.control as FormControl, null);
}
onChange: (value: any) => void;
onTouch: () => void;
constructor(
@Optional() @Self() public ngControl: NgControl,
@Optional() private _controlName: FormControlName,
private errorMatcher: ErrorStateMatcher,
) {
if (ngControl) {
ngControl.valueAccessor = this;
}
}
ngOnInit(): void {
this.control = this._controlName.control;
this.control.valueChanges.subscribe(res=>{
if(res){
this.validate();
}
})
this.control.markAsTouched();
this.validate();
}
ngDoCheck(): void {
if(this.control){
this.validate();
}
}
writeValue(obj: any): void {
this._value = obj;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
validate(){
console.log(this.control);
this.errorMessage = null;
if(this.control.errors && this.control.errors.required && !this.control.value){
this.errorMessage = "Required";
return;
}
if(this.control?.value?.length < 3){
this.control.setErrors({ invalid: true});
this.errorMessage = 'Length must be at least 3 characters.';
return
}
}
}