Angular Material 自定义 MatFormFieldControl - 如何管理错误状态
Angular Material custom MatFormFieldControl - How to manage error state
我正在尝试制作自定义 MatFormFieldControl,版本为 Angular Material 和 Angular 6。自定义输入是一个权重输入,它有一个值 (input type="number ") 和单位 (select "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>
体重-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;
}
体重-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;
}
}
这是它必须去的地方:
app.component.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>
(比索在意大利语中表示重量,单位是海关单位,因此您将它们绑定在输入 [units] 中)
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;
}
所以我需要的是:
- 这是 MatFormFieldControl 和 ControlValueAccessor 的良好实现吗?有问题,错误吗?
- 主要是:如何管理输入的 errorState,使其表现为普通的 mat 表单字段控件以及如何 detect/relate 它与外部表单控件验证器一起使用? (例如,如果控件“peso”具有 Validators.required 如果自定义输入为空,则 errorState 为真,否则为假,与最终的自定义验证器相同)
更新:我从这个
中更正了空方法
(!value.value && value.value != 0) || !!!value.unit
对此
(!value.value && value.value != 0) && !!!value.unit
我用 mat-select 输入更改了 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>
可能应该使用 Validator 接口,但不幸的是,它会产生讨厌的循环错误依赖性。因此,只需向您的自定义组件添加一个 errorState
属性 来检查注入到构造函数中的 ngControl
,如下所示:
get errorState() {
return this.ngControl.errors !== null && !!this.ngControl.touched;
}
这应该尊重父组件中的正常 Angular 验证器,例如 formGroup 中的这一行:
peso: [ null, Validators.required ],
我正在尝试制作自定义 MatFormFieldControl,版本为 Angular Material 和 Angular 6。自定义输入是一个权重输入,它有一个值 (input type="number ") 和单位 (select "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>
体重-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;
}
体重-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;
}
}
这是它必须去的地方:
app.component.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>
(比索在意大利语中表示重量,单位是海关单位,因此您将它们绑定在输入 [units] 中)
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;
}
所以我需要的是:
- 这是 MatFormFieldControl 和 ControlValueAccessor 的良好实现吗?有问题,错误吗?
- 主要是:如何管理输入的 errorState,使其表现为普通的 mat 表单字段控件以及如何 detect/relate 它与外部表单控件验证器一起使用? (例如,如果控件“peso”具有 Validators.required 如果自定义输入为空,则 errorState 为真,否则为假,与最终的自定义验证器相同)
更新:我从这个
中更正了空方法
(!value.value && value.value != 0) || !!!value.unit
对此
(!value.value && value.value != 0) && !!!value.unit
我用 mat-select 输入更改了 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>
可能应该使用 Validator 接口,但不幸的是,它会产生讨厌的循环错误依赖性。因此,只需向您的自定义组件添加一个 errorState
属性 来检查注入到构造函数中的 ngControl
,如下所示:
get errorState() {
return this.ngControl.errors !== null && !!this.ngControl.touched;
}
这应该尊重父组件中的正常 Angular 验证器,例如 formGroup 中的这一行:
peso: [ null, Validators.required ],