Reputation: 4306
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?
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
}
}
}
Upvotes: 1
Views: 721
Reputation: 1384
what you did is good, but you may want to add these too:
/** Adding these just to update the component a bit */
@Input() name: string;
@Input() readOnly: boolean;
@Input() type: string;
@Input() required: boolean;
@Input() maxLength: number;
@Input() hint: string;
@Input() errMessage: string;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
onBlur(event) {
if (event && event.target && event.target.value) {
this.value = event.target.value;
this.blur.emit(event);
}
}
In the HTML, you can bind them like this:
<input
matInput
[id]="id"
#input
[formControl]="control"
[placeholder]="placeholder"
[name]="formControlName"
[readonly]="readOnly"
[type]="type"
[required]="required"
[maxLength]="maxLength"
(blur)="onBlur($event)"
/>
<mat-hint>{{ hint ? hint : 'Required' }}</mat-hint>
<mat-error>{{ errMessage ? errMessage : errorMessage }}</mat-error>
Upvotes: 0