运行 ControlValueAccessor 在更新和验证 child 个组件后进行验证
Run ControlValueAccessor validate after updating and validating child components
我正在 Angular 处理一个棘手的问题。我想创建一个自定义表单组件,其中 "bundles" 几个 child 输入元素并且在所有 children 都有效时有效。
我以为我会简单地创建一个组件来实现 ControlValueAccessor 和 Validator 接口。我将使用 ViewChildren 装饰器将输入作为 NgModels 注入,这样我就可以遍历它们并在组件的验证方法中收集任何验证错误。这是我制作的地址输入组件的示例实现,它针对两个都需要一个值的输入(街道和街道号码)执行此操作:
import { AfterViewInit, Component, forwardRef, OnInit, QueryList, ViewChildren } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NgModel, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { Address } from './address';
@Component({
selector: `address-input`,
template: `
<label for="street">Street</label>
<input id="street" name="street" type="text" [(ngModel)]="Street" (blur)="onTouchedAField()" required /><br />
<label for="number">Number</label>
<input id="number" name="number" type="number" [(ngModel)]="Number" (blur)="onTouchedAField()" required />`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: AddressInputComponent,
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: AddressInputComponent,
multi: true,
},
],
})
export class AddressInputComponent implements ControlValueAccessor, Validator {
@ViewChildren(NgModel) public validatedFields!: QueryList<NgModel>;
private address: Address = new Address(null, null);
private onFormCtrlChanged!: (_: any) => void;
private onFormCtrlTouched!: (_: any) => void;
public get Street(): string | null {
if (this.address) {
return this.address.Street;
} else {
return null;
}
}
public set Street(val: string) {
this.address = new Address(val, this.address.Number);
this.emitChanged();
}
public get Number(): number | null {
if (this.address) {
return this.address.Number;
} else {
return null;
}
}
public set Number(val: number | null) {
this.address = new Address(this.address.Street, val);
this.emitChanged();
}
public onTouchedAField(): void {
this.emitTouched();
}
public writeValue(newAddress: Address): void {
if (newAddress !== undefined) {
this.address = newAddress;
}
}
public registerOnChange(fn: (_: any) => void): void {
this.onFormCtrlChanged = fn;
}
public registerOnTouched(fn: (_: any) => void): void {
this.onFormCtrlTouched = fn;
}
public validate(control: AbstractControl): ValidationErrors | null {
let validationErrors: ValidationErrors | null = null;
this.validatedFields.forEach((ngm: NgModel) => {
console.log(`ValidationErrs for ${ngm.name} with value ${ngm.value}`, ngm.errors);
if (ngm.errors !== null) {
if (validationErrors === null)
validationErrors = {};
validationErrors[ngm.name] = ngm.errors;
}
});
console.log(`validationErrors `, validationErrors);
return validationErrors;
}
private emitChanged(): void {
this.onFormCtrlChanged(this.address);
}
private emitTouched(): void {
this.onFormCtrlTouched(this.address);
}
}
当我将地址组件嵌入到这样的表单中时,这大部分工作正常:
<form (ngSubmit)="onSubmit()" #myForm="ngForm">
<address-input name="address" [(ngModel)]="address" #addressInput="ngModel"></address-input>
<button type="submit">SUBMIT</button>
</form>
唯一但令人讨厌的问题是,当通过模型绑定正确填写地址输入组件时,地址输入组件将仍然处于无效状态。
即当包含自定义表单组件的表单的应用程序组件定义它绑定到自定义表单组件的地址字段时,如下所示:
public address: Address = new Address('Baker Street', 123);
自定义表单组件应该有效但仍然无效。
例如,当我向字段中添加字母或数字时,自定义表单组件立即生效。
经过一番调查,我发现这是因为Angular在自定义表单组件上调用了ControlValueAccessor接口的writeValue方法后,立即调用了Validator接口的validate方法。这样,当 Angular 调用自定义表单组件上的验证方法时,自定义表单组件的 child 输入值及其验证状态尚未更新。
为了解决我的问题,我想知道是否有任何方法可以强制 Angular 更新 ngModels 的值和验证状态,或者 运行 在 child 时再次验证 inputs/components 已更新。或者如果有任何其他方法可以使这项工作。更广泛地说,我还想知道是否有更好的方法来实现一个在所有 children 都有效的表单组件,因为我觉得解决这个问题需要一些 "hacks" 或低效的代码。
You can find a Stackblitz here 包含问题的简单再现。
提前感谢您的时间和帮助,
约书亚
如果您使用 ReactiveForms,您的代码会简单得多,在 ReactiveForms 中,您的真实来源是模型,并且您始终可以访问单个控件或控件组的状态,而无需使用 @ViewChildren 扫描组件,这里是这是如何从 angular 文档 https://angular.io/guide/form-validation#cross-field-validation
中实现的很好的例子
您可以尝试使用FormGroup控件来嵌套表单:
https://angular.io/guide/reactive-forms#step-1-creating-a-nested-group
感谢如此详细的解释...我有一个类似的问题,即自定义组件上设置的默认值未验证包装组件。调试后意识到 onChangeCb() 仍然是我初始化的那个,而不是 registerOnChange 分配的那个。所以变化没有传播。以下是我尝试过并且似乎有效的更改。请检查
...
public registerOnChange(fn) {
this._onChange = fn;
/* value is model */
this.value && setTimeout(()=>{this._onChange(this.value)},0);
}
...
除了上面一个,我觉得下面的解决方案更好,因为它不涉及任何 'setTimeout'
public writeValue(val) {
// EXISING CODE
...
(val!==this.value) && this._onChange(this.value);
}
我正在 Angular 处理一个棘手的问题。我想创建一个自定义表单组件,其中 "bundles" 几个 child 输入元素并且在所有 children 都有效时有效。
我以为我会简单地创建一个组件来实现 ControlValueAccessor 和 Validator 接口。我将使用 ViewChildren 装饰器将输入作为 NgModels 注入,这样我就可以遍历它们并在组件的验证方法中收集任何验证错误。这是我制作的地址输入组件的示例实现,它针对两个都需要一个值的输入(街道和街道号码)执行此操作:
import { AfterViewInit, Component, forwardRef, OnInit, QueryList, ViewChildren } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NgModel, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { Address } from './address';
@Component({
selector: `address-input`,
template: `
<label for="street">Street</label>
<input id="street" name="street" type="text" [(ngModel)]="Street" (blur)="onTouchedAField()" required /><br />
<label for="number">Number</label>
<input id="number" name="number" type="number" [(ngModel)]="Number" (blur)="onTouchedAField()" required />`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: AddressInputComponent,
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: AddressInputComponent,
multi: true,
},
],
})
export class AddressInputComponent implements ControlValueAccessor, Validator {
@ViewChildren(NgModel) public validatedFields!: QueryList<NgModel>;
private address: Address = new Address(null, null);
private onFormCtrlChanged!: (_: any) => void;
private onFormCtrlTouched!: (_: any) => void;
public get Street(): string | null {
if (this.address) {
return this.address.Street;
} else {
return null;
}
}
public set Street(val: string) {
this.address = new Address(val, this.address.Number);
this.emitChanged();
}
public get Number(): number | null {
if (this.address) {
return this.address.Number;
} else {
return null;
}
}
public set Number(val: number | null) {
this.address = new Address(this.address.Street, val);
this.emitChanged();
}
public onTouchedAField(): void {
this.emitTouched();
}
public writeValue(newAddress: Address): void {
if (newAddress !== undefined) {
this.address = newAddress;
}
}
public registerOnChange(fn: (_: any) => void): void {
this.onFormCtrlChanged = fn;
}
public registerOnTouched(fn: (_: any) => void): void {
this.onFormCtrlTouched = fn;
}
public validate(control: AbstractControl): ValidationErrors | null {
let validationErrors: ValidationErrors | null = null;
this.validatedFields.forEach((ngm: NgModel) => {
console.log(`ValidationErrs for ${ngm.name} with value ${ngm.value}`, ngm.errors);
if (ngm.errors !== null) {
if (validationErrors === null)
validationErrors = {};
validationErrors[ngm.name] = ngm.errors;
}
});
console.log(`validationErrors `, validationErrors);
return validationErrors;
}
private emitChanged(): void {
this.onFormCtrlChanged(this.address);
}
private emitTouched(): void {
this.onFormCtrlTouched(this.address);
}
}
当我将地址组件嵌入到这样的表单中时,这大部分工作正常:
<form (ngSubmit)="onSubmit()" #myForm="ngForm">
<address-input name="address" [(ngModel)]="address" #addressInput="ngModel"></address-input>
<button type="submit">SUBMIT</button>
</form>
唯一但令人讨厌的问题是,当通过模型绑定正确填写地址输入组件时,地址输入组件将仍然处于无效状态。
即当包含自定义表单组件的表单的应用程序组件定义它绑定到自定义表单组件的地址字段时,如下所示:
public address: Address = new Address('Baker Street', 123);
自定义表单组件应该有效但仍然无效。
例如,当我向字段中添加字母或数字时,自定义表单组件立即生效。
经过一番调查,我发现这是因为Angular在自定义表单组件上调用了ControlValueAccessor接口的writeValue方法后,立即调用了Validator接口的validate方法。这样,当 Angular 调用自定义表单组件上的验证方法时,自定义表单组件的 child 输入值及其验证状态尚未更新。
为了解决我的问题,我想知道是否有任何方法可以强制 Angular 更新 ngModels 的值和验证状态,或者 运行 在 child 时再次验证 inputs/components 已更新。或者如果有任何其他方法可以使这项工作。更广泛地说,我还想知道是否有更好的方法来实现一个在所有 children 都有效的表单组件,因为我觉得解决这个问题需要一些 "hacks" 或低效的代码。
You can find a Stackblitz here 包含问题的简单再现。
提前感谢您的时间和帮助, 约书亚
如果您使用 ReactiveForms,您的代码会简单得多,在 ReactiveForms 中,您的真实来源是模型,并且您始终可以访问单个控件或控件组的状态,而无需使用 @ViewChildren 扫描组件,这里是这是如何从 angular 文档 https://angular.io/guide/form-validation#cross-field-validation
中实现的很好的例子您可以尝试使用FormGroup控件来嵌套表单: https://angular.io/guide/reactive-forms#step-1-creating-a-nested-group
感谢如此详细的解释...我有一个类似的问题,即自定义组件上设置的默认值未验证包装组件。调试后意识到 onChangeCb() 仍然是我初始化的那个,而不是 registerOnChange 分配的那个。所以变化没有传播。以下是我尝试过并且似乎有效的更改。请检查
...
public registerOnChange(fn) {
this._onChange = fn;
/* value is model */
this.value && setTimeout(()=>{this._onChange(this.value)},0);
}
...
除了上面一个,我觉得下面的解决方案更好,因为它不涉及任何 'setTimeout'
public writeValue(val) {
// EXISING CODE
...
(val!==this.value) && this._onChange(this.value);
}