Angular Material 自定义 MatFormFieldControl - 如何管理错误状态

Eub*_*per 11 angular-material2 angular angular6

我正在尝试使用 Angular Material 和 Angular 6 的第 7 版制作自定义 MatFormFieldControl。自定义输入是一个重量输入,它有一个值(输入类型 =“数字”)和一个单位(选择“kg”,“ G”,...)。它必须放置在 mat-form-field-control 中,使用反应式表单 (formControlName="weight") 并支持错误状态 ( <mat-error *ngIf="weightControl.hasError('required')">error<...>),甚至使用自定义验证器。

我写了这个实现:

重量-input.component.html

<div [formGroup]="weightForm">
  <input fxFlex formControlName="value" type="number" placeholder="Valore" min="0" #value>
  <select formControlName="unit" [style.color]="getUnselectedColor()" (change)="setUnselected(unit)" #unit>
    <option value="" selected> Unità </option>
    <option *ngFor="let unit of units" style="color: black;">{{ unit }}</option>
  </select>
</div>
Run Code Online (Sandbox Code Playgroud)

重量-input.component.css

.container {
  display: flex;
}

input, select {
  border: none;
  background: none;
  padding: 0;
  opacity: 0;
  outline: none;
  font: inherit;
  transition: 200ms opacity ease-in-out;
}

:host.weight-floating input {
  opacity: 1;
}

:host.weight-floating select {
  opacity: 1;
}
Run Code Online (Sandbox Code Playgroud)

重量-input.component.ts

import { Component, OnInit, Input, OnDestroy, HostBinding, ElementRef, forwardRef, Optional, Self } from '@angular/core';
import { FormGroup, FormBuilder, ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatFormFieldControl } from '@angular/material';
import { Subject } from 'rxjs';
import { FocusMonitor } from '@angular/cdk/a11y';

export class Weight {
  constructor(public value: number, public unit: string) { };
}

@Component({
  selector: 'weight-input',
  templateUrl: './weight-input.component.html',
  styleUrls: ['./weight-input.component.css'],
  providers: [
    { provide: MatFormFieldControl, useExisting: WeightInput }
  ],
})
export class WeightInput implements OnInit, OnDestroy, MatFormFieldControl<Weight>, ControlValueAccessor {

  stateChanges = new Subject<void>();

  @Input() 
  get units(): string[] {
    return this._units;
  }
  set units(value: string[]) {
    this._units = value;
    this.stateChanges.next();
  }
  private _units: string[];

  unselected = true;
  weightForm: FormGroup;

  @Input()
  get value(): Weight | null {
    const value: Weight = this.weightForm.value;
    return ((value.value || value.value == 0) && !!value.unit) ? value : null;
  }
  set value(value: Weight | null) {
    value = value || new Weight(null, '');
    this.weightForm.setValue({ value: value.value, unit: value.unit });
    if(this._onChange) this._onChange(value);
    this.stateChanges.next();
  }

  static nextId = 0;
  @HostBinding() id = `weight-input-${WeightInput.nextId++}`;

  @Input()
  get placeholder() {
    return this._placeholder;
  }
  set placeholder(placeholder) {
    this._placeholder = placeholder;
    this.stateChanges.next();
  }
  private _placeholder: string;

  focused = false;

  get empty() {
    const value = this.weightForm.value as Weight;
    return (!value.value && value.value != 0) || !!!value.unit;
  }

  @HostBinding('class.weight-floating')
  get shouldLabelFloat() {
    return this.focused || !this.empty;
  }

  @Input()
  get required(): boolean {
    return this._required;
  }
  set required(required: boolean) {
    const temp: any = required;
    required = (temp != "true");
    this._required = required;
    this.stateChanges.next();
  }
  private _required = false;

  @Input()
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(disabled: boolean) {
    const temp: any = disabled;
    disabled = (temp != "true");
    this._disabled = disabled;
    this.setDisable();
    this.stateChanges.next();
  }
  private _disabled = false;

  errorState = false;
  controlType = 'weight-input';

  @HostBinding('attr.aria-describedby') describedBy = '';
  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  onContainerClick(event: MouseEvent) {
    if(!this.disabled) {
      this._onTouched();
    }
   }

  constructor(
    @Optional() @Self() public ngControl: NgControl, 
    private fb: FormBuilder, 
    private fm: FocusMonitor,
    private elRef: ElementRef<HTMLElement>
  ) {
    if(this.ngControl != null) { 
      this.ngControl.valueAccessor = this; 
    }
    fm.monitor(elRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });
  }

  ngOnInit() {
    this.weightForm = this.fb.group({
      value: null,
      unit: ''
    });
    this.setDisable();
    this.weightForm.valueChanges.subscribe(
      () => {
        const value = this.value;
        if(this._onChange) this._onChange(value);
        this.stateChanges.next();
      }
    );
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this.fm.stopMonitoring(this.elRef.nativeElement);
  }

  writeValue(value: Weight): void {
    if(value instanceof Weight) {
      this.weightForm.setValue(value);
    }
  }

  _onChange: (_: any) => void;
  registerOnChange(fn: (_: any) => void): void {
    this._onChange = fn;
  }

  _onTouched: () => void;
  registerOnTouched(fn: () => void): void {
    this._onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  private setDisable(): void {
    if(this.disabled && this.weightForm) {
      this.weightForm.disable();
    }
    else if(this.weightForm) {
      this.weightForm.enable();
    }
  }

  getUnselectedColor(): string {
    return this.unselected ? '#999' : '#000';
  }

  setUnselected(select): void {
    this.unselected = !!!select.value;
  }

}
Run Code Online (Sandbox Code Playgroud)

这是它必须去的地方:

应用程序组件.html

<mat-form-field fxFlexAlign="stretch">
        <weight-input formControlName="peso" [units]="units" placeholder="Peso" required></weight-input>
        <mat-error *ngIf="peso.invalid">errore</mat-error>
      </mat-form-field>
Run Code Online (Sandbox Code Playgroud)

(比索在意大利语中表示重量,单位是海关,因此您将它们绑定在输入 [单位] 中)

app.component.ts(部分)

units = [ 'Kg', 'g', 'T', 'hg' ];
ngOnInit() {
    this.initForm();
  } 

private initForm(): void {
    this.scheda = this.fb.group({
      diametro: [ null, Validators.required ],
      peso: [ null, Validators.required ], //There will be custom validators, for instance for unit control (Validators.unitsIn(units: string[]))
      contorno: [ null, Validators.required ],
      fornitore: null,
      note: null
    });
  }

get diametro(): FormControl | undefined {
    return this.scheda.get('diametro') as FormControl;
  }
  get peso(): FormControl | undefined {
    return this.scheda.get('peso') as FormControl;
  }
Run Code Online (Sandbox Code Playgroud)

所以我需要的是: -

这是 MatFormFieldControl 和 ControlValueAccessor 的一个很好的实现吗?它有问题,错误吗?

- 主要是:如何管理输入的 errorState,使其表现为正常的 mat 表单字段控制,以及如何检测/关联它与外部表单控件验证器?(例如,如果控件“比索”具有 Validators.required errorState 如果自定义输入为空,则为 true,否则为 false,与最终的自定义验证器相同)

更新:我将空方法从这个更正(!value.value && value.value != 0) || !!!value.unit到这个(!value.value && value.value != 0) && !!!value.unit

我用 mat-select 输入更改了选择输入,但它在功能上仍然相同

<div [formGroup]="weightForm">
 <input fxFlex formControlName="value" type="number" placeholder="Valore" min="0" #value>
  <mat-select fxFlex="10" id="mat-select" formControlName="unit">
    <mat-option value="" selected> Unità </mat-option>  
    <mat-option *ngFor="let unit of units" [value]="unit">
        {{ unit }}
      </mat-option>
    </mat-select>
</div>
Run Code Online (Sandbox Code Playgroud)

Phi*_* J. 9

可能应该使用 Validator 接口,但不幸的是,它创建了那种讨厌的循环错误依赖。因此,相反,只需errorState向您的自定义组件添加一个属性,以检查ngControl注入构造函数的 ,如下所示:

get errorState() {
  return this.ngControl.errors !== null && !!this.ngControl.touched;
}
Run Code Online (Sandbox Code Playgroud)

这应该尊重您在父组件中的普通 Angular 验证器,就像 formGroup 中的这一行:

peso: [ null, Validators.required ],
Run Code Online (Sandbox Code Playgroud)

  • 当然,这只是为了打字。`errorState` 应该返回一个布尔值,但 `this.ngControl.touched` 有时可能是未定义的。您也可以检查是否“this.ngControl.touched === true”。 (2认同)