如何正确使用具有反应形式、ControlValueAccessor 和 errorMatcher 的角度材料

时间:2021-04-13 16:01:26

标签: angular angular-material angular-reactive-forms

我最近花了几个小时浏览了很多资源,但似乎没有一个能像预期的那样开箱即用,所以我尽我所能在 stackblitz 上编写了这个示例。

有人可以告诉我这是否是完成以下任务的最佳方法?

  1. 使用 angular material、errorStateMatcher 和 ngControl 创建自定义表单控件。
  2. 让这个组件“开箱即用”,或者尽可能在本地将自定义 formControl 上的任何父验证以及自定义表单控件内的自定义验证交织在一起。
  3. 目标:请帮助删除或添加任何使此功能更好所需的代码。提前致谢。

链接到stackblitz: https://stackblitz.com/edit/angular-mat-reactive-form-control-ddssy1

自定义控件 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>

自定义控件 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
    }   
  }
}

父 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>

家长 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
    }   
  }
}

1 个答案:

答案 0 :(得分:0)

你所做的很好,但你可能还想添加这些:

  /** 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);
    }
  }

在 HTML 中,您可以像这样绑定它们:

<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>