Angular 的自定义表单数组的验证对于异步验证器保持为 PENDING
Validation for Angular's custom form array stays as PENDING for async validators
我有一个自定义邮政编码异步验证器,它也适用于模板驱动表单。
@Directive({
selector: '[appAsyncPostalCode]',
providers: [
{ provide: NG_ASYNC_VALIDATORS, useExisting: AsyncPostalCodeValidatorDirective, multi: true }
]
})
export class AsyncPostalCodeValidatorDirective implements AsyncValidator {
public validate(
control: AbstractControl
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return this.postalCodeValidator()(control);
}
public postalCodeValidator(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const postalCodePattern = /^\d{5}$/;
if (control.value && postalCodePattern.test(control.value)) {
return checkPostalCodeXHR(control.value).pipe(
map(name => null),
catchError(() => {
if (control.value) {
control.markAsTouched();
}
return of({ postalCodeInvalid: true });
})
);
} else {
return of(control.value ? { postalCode: true } : null);
}
};
}
}
以及在模板驱动表单中使用表单数组的自定义实现。
@Component({
selector: 'app-array-input',
templateUrl: './array-input.component.html',
styleUrls: ['./array-input.component.scss'],
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: ArrayInputComponent, multi: true },
{ provide: NG_ASYNC_VALIDATORS, useExisting: ArrayInputComponent, multi: true }
]
})
export class ArrayInputComponent implements ControlValueAccessor, AsyncValidator {
@Input() public name!: string;
@ContentChild(ArrayItemDirective, { static: true })
public itemTemplate!: ArrayItemDirective;
public get value() {
return this.array.value;
}
private array = new FormArray([]);
public controlIds: symbol[] = [];
constructor(
@Optional() @Host() parent: ControlContainer,
private changeDetector: ChangeDetectorRef
) {
if (parent) {
this.array.setParent(parent.control as any);
}
this.array.valueChanges.subscribe(value => {
this.onChange(value);
this.onTouch();
});
}
public validate() {
return combineLatest(this.array.controls.map(c => c.statusChanges)).pipe(
startWith(this.array.controls.map(c => c.status)),
filter(() => !this.array.controls.some(c => c.pending)),
map(() => (this.array.valid ? null : { array: true })),
take(1)
);
}
private onChange: (val?: any) => void = () => {};
private onTouch: () => void = () => {};
public registerOnChange(fn: any): void {
this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouch = fn;
}
public add() {
this.controlIds.push(Symbol());
this.changeDetector.detectChanges();
}
public removeAt(index: number) {
this.controlIds.splice(index, 1);
this.array.removeAt(index);
this.changeDetector.detectChanges();
}
public bindItem(item: ArrayValueDirective) {
const isNew = item.index >= this.array.length;
if (!isNew) {
const value = this.array.at(item.index).value;
item.setValue(value);
}
this.array.setControl(item.index, item.control, { emitEvent: isNew });
}
public writeValue(newArray: any[] | null): void {
if (!equal(this.value, newArray)) {
this.array.clear({ emitEvent: false });
this.controlIds.splice(0);
if (newArray) {
for (const val of newArray) {
this.array.push(new FormControl(val), { emitEvent: false });
this.controlIds.push(Symbol());
}
}
}
this.changeDetector.detectChanges();
}
}
问题是 ArrayInputComponent > validate
在值更改时被调用,但控件的 statusChanges
永远不会触发,即使状态从 PENDING
更改为 VALID
。因此,数组控件保留在 PENDING
中,整个表单无效。
目前我能找到的唯一解决方法是在异步验证器完成后的下一个 tick 中检查父组件的有效性
...
return checkPostalCodeXHR(control.value).pipe(
map(name => {
setTimeout(() => control.parent?.updateValueAndValidity());
return null;
}),
catchError(() => {
if (control.value) {
control.markAsTouched();
}
setTimeout(() => control.parent?.updateValueAndValidity());
return of({ postalCodeInvalid: true });
})
);
...
我知道在某些情况下 Angular 将 emitEvent
设置为 false
以进行更新,但也做了一些更改以针对异步验证器修复此问题(https://github.com/profanis/angular/commit/bae96682e7d5ea1942c72583e7d68a24507c1a5a).
有人对此有解决方案吗?
可能是我声明不正确,或者我应该以其他方式检查数组的有效性?
提前谢谢你。
编辑:我针对这个问题发起了一次 Stackblitz。 https://stackblitz.com/edit/angular-template-form-array-jf9h5v.
最好检查浏览器的控制台,而不是 Stackblitz 控制台。您可以看到第一个控件的 VALID 状态是如何不发出的,但是如果您检查控制台中的控件日志,它在扩展中被评估,状态是 VALID。
添加的 Stackblitz 的这个分支解决了这个问题。
https://stackblitz.com/edit/angular-template-form-array-jf9h5v-uh3t4x
对于 FormArray 的控件,statusChanges
似乎没有发出新的状态。但是数组的 statusChanges
可以。这可用于正确验证数组。
public validate() {
return this.array.statusChanges.pipe(
startWith(this.array.status),
filter(() => !this.array.pending),
map(() => (this.array.valid ? null : { array: true })),
take(1)
);
}
我有一个自定义邮政编码异步验证器,它也适用于模板驱动表单。
@Directive({
selector: '[appAsyncPostalCode]',
providers: [
{ provide: NG_ASYNC_VALIDATORS, useExisting: AsyncPostalCodeValidatorDirective, multi: true }
]
})
export class AsyncPostalCodeValidatorDirective implements AsyncValidator {
public validate(
control: AbstractControl
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return this.postalCodeValidator()(control);
}
public postalCodeValidator(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
const postalCodePattern = /^\d{5}$/;
if (control.value && postalCodePattern.test(control.value)) {
return checkPostalCodeXHR(control.value).pipe(
map(name => null),
catchError(() => {
if (control.value) {
control.markAsTouched();
}
return of({ postalCodeInvalid: true });
})
);
} else {
return of(control.value ? { postalCode: true } : null);
}
};
}
}
以及在模板驱动表单中使用表单数组的自定义实现。
@Component({
selector: 'app-array-input',
templateUrl: './array-input.component.html',
styleUrls: ['./array-input.component.scss'],
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: ArrayInputComponent, multi: true },
{ provide: NG_ASYNC_VALIDATORS, useExisting: ArrayInputComponent, multi: true }
]
})
export class ArrayInputComponent implements ControlValueAccessor, AsyncValidator {
@Input() public name!: string;
@ContentChild(ArrayItemDirective, { static: true })
public itemTemplate!: ArrayItemDirective;
public get value() {
return this.array.value;
}
private array = new FormArray([]);
public controlIds: symbol[] = [];
constructor(
@Optional() @Host() parent: ControlContainer,
private changeDetector: ChangeDetectorRef
) {
if (parent) {
this.array.setParent(parent.control as any);
}
this.array.valueChanges.subscribe(value => {
this.onChange(value);
this.onTouch();
});
}
public validate() {
return combineLatest(this.array.controls.map(c => c.statusChanges)).pipe(
startWith(this.array.controls.map(c => c.status)),
filter(() => !this.array.controls.some(c => c.pending)),
map(() => (this.array.valid ? null : { array: true })),
take(1)
);
}
private onChange: (val?: any) => void = () => {};
private onTouch: () => void = () => {};
public registerOnChange(fn: any): void {
this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouch = fn;
}
public add() {
this.controlIds.push(Symbol());
this.changeDetector.detectChanges();
}
public removeAt(index: number) {
this.controlIds.splice(index, 1);
this.array.removeAt(index);
this.changeDetector.detectChanges();
}
public bindItem(item: ArrayValueDirective) {
const isNew = item.index >= this.array.length;
if (!isNew) {
const value = this.array.at(item.index).value;
item.setValue(value);
}
this.array.setControl(item.index, item.control, { emitEvent: isNew });
}
public writeValue(newArray: any[] | null): void {
if (!equal(this.value, newArray)) {
this.array.clear({ emitEvent: false });
this.controlIds.splice(0);
if (newArray) {
for (const val of newArray) {
this.array.push(new FormControl(val), { emitEvent: false });
this.controlIds.push(Symbol());
}
}
}
this.changeDetector.detectChanges();
}
}
问题是 ArrayInputComponent > validate
在值更改时被调用,但控件的 statusChanges
永远不会触发,即使状态从 PENDING
更改为 VALID
。因此,数组控件保留在 PENDING
中,整个表单无效。
目前我能找到的唯一解决方法是在异步验证器完成后的下一个 tick 中检查父组件的有效性
...
return checkPostalCodeXHR(control.value).pipe(
map(name => {
setTimeout(() => control.parent?.updateValueAndValidity());
return null;
}),
catchError(() => {
if (control.value) {
control.markAsTouched();
}
setTimeout(() => control.parent?.updateValueAndValidity());
return of({ postalCodeInvalid: true });
})
);
...
我知道在某些情况下 Angular 将 emitEvent
设置为 false
以进行更新,但也做了一些更改以针对异步验证器修复此问题(https://github.com/profanis/angular/commit/bae96682e7d5ea1942c72583e7d68a24507c1a5a).
有人对此有解决方案吗?
可能是我声明不正确,或者我应该以其他方式检查数组的有效性?
提前谢谢你。
编辑:我针对这个问题发起了一次 Stackblitz。 https://stackblitz.com/edit/angular-template-form-array-jf9h5v.
最好检查浏览器的控制台,而不是 Stackblitz 控制台。您可以看到第一个控件的 VALID 状态是如何不发出的,但是如果您检查控制台中的控件日志,它在扩展中被评估,状态是 VALID。
添加的 Stackblitz 的这个分支解决了这个问题。
https://stackblitz.com/edit/angular-template-form-array-jf9h5v-uh3t4x
对于 FormArray 的控件,statusChanges
似乎没有发出新的状态。但是数组的 statusChanges
可以。这可用于正确验证数组。
public validate() {
return this.array.statusChanges.pipe(
startWith(this.array.status),
filter(() => !this.array.pending),
map(() => (this.array.valid ? null : { array: true })),
take(1)
);
}