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