异步验证程序和mat-autocomplete无法协同工作

问题描述

在validate函数中,我向api发送了一个请求,以检查数据是否有效,并且可以正常工作。

但是,如果值是一个对象,我只会返回null,但这会破坏mat-autocomplete(面板永远不会关闭)。

import { ChangeDetectionStrategy,ChangeDetectorRef,Component,Optional,EventEmitter,Output,Self } from '@angular/core';
import { AbstractControl,ControlValueAccessor,NgControl,ValidationErrors } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { ErrorStateMatcher } from '@angular/material/core';
import { Observable } from 'rxjs';
import { finalize,map } from 'rxjs/operators';
import { PostalArea } from 'src/app/feature/address/postal-area/portal-area.model';
import { PostalAreaService } from 'src/app/feature/address/postal-area/postal-area.service';

@Component({
  selector: 'postal-code',templateUrl: './postal-code.component.html',styleUrls: ['./postal-code.component.scss'],changeDetection: ChangeDetectionStrategy.OnPush
})

export class PostalCodeComponent implements ControlValueAccessor {

  stateMatcher: ErrorStateMatcher = new CtrlErrorStateMatcher();
  postalArea$: Observable<PostalArea[]>;

  @Output() onPostalAreaSelected: EventEmitter<PostalArea> = new EventEmitter();

  constructor(
    private readonly _postalAreaService: PostalAreaService,private readonly _changeDetectorRef: ChangeDetectorRef,@Optional() @Self() public ngControl: NgControl,) {
    if (this.ngControl != null) {
      // Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
      this.ngControl.valueAccessor = this;
    }
  }

  ngOnInit() {
    this.ngControl.control.setAsyncValidators(this.validate.bind(this));
    this.ngControl.control.updateValueAndValidity();
  }

  onTouched = (_value?: any) => { };
  onChanged = (_value?: any) => { };

  writeValue(val: string): void {
    this.ngControl.control?.setValue(val);
  }

  registerOnChange(fn: any): void {
    this.onChanged = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    if (typeof control.value !== 'string') {
      return null;
    }
    this.postalArea$ = this._postalAreaService.getPostalAreas(control.value);

    return this._postalAreaService.getPostalAreas(control.value).pipe(
      map(postalArea => {
        if (postalArea.length === 0) {
          return { invalidPostalCode: true };
        } else {
          return null;
        }
      }),finalize(() => {
        this._changeDetectorRef.markForCheck();
      })
    );
  }

  displayFn(postalArea?: PostalArea) {
    return postalArea ? postalArea.zipCode : '';
  }

  onSelectionChanged(event: MatAutocompleteSelectedEvent) {
    this.onPostalAreaSelected.emit(event.option.value);
  }
}

export class CtrlErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: AbstractControl): boolean {
    return !!(control && control.invalid && control.touched);
  }
}

模板:

  <input matInput [errorStateMatcher]="stateMatcher" [formControl]="ngControl?.control"
    [matAutocomplete]="postalCodeAutoComplete" (input)="onChanged($event.target.value)" (blur)="onTouched()"
    name="postal-code" />

  <mat-autocomplete #postalCodeAutoComplete="matAutocomplete" [displayWith]="displayFn.bind(this)"
    (optionSelected)="onSelectionChanged($event)">
    <mat-option *ngFor="let postalArea of (postalArea$ | async)" [value]="postalArea">
      {{ postalArea.zipCode }} {{ postalArea.name }}
    </mat-option>
  </mat-autocomplete>

解决方法

我对我对这个问题的看法进行了重构..所以这是一个可行的解决方案:)

import { ChangeDetectionStrategy,ChangeDetectorRef,Component,EventEmitter,Optional,Output,Self } from '@angular/core';
import { AbstractControl,ControlValueAccessor,NgControl,ValidationErrors } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { ErrorStateMatcher } from '@angular/material/core';
import { Observable,of } from 'rxjs';
import { debounceTime,distinctUntilChanged,finalize,map,startWith,switchMap } from 'rxjs/operators';
import { PostalArea } from 'src/app/feature/address/postal-area/portal-area.model';
import { PostalAreaService } from 'src/app/feature/address/postal-area/postal-area.service';

@Component({
  selector: 'ssp-postal-code',templateUrl: './postal-code.component.html',styleUrls: ['./postal-code.component.scss'],changeDetection: ChangeDetectionStrategy.OnPush
})

export class PostalCodeComponent implements ControlValueAccessor {
  stateMatcher: ErrorStateMatcher = new CtrlErrorStateMatcher();

  postalArea$: Observable<PostalArea[]>;

  @Output() onPostalAreaSelected: EventEmitter<PostalArea> = new EventEmitter();

  constructor(
    private readonly _postalAreaService: PostalAreaService,@Optional() @Self() public ngControl: NgControl,) {
    if (this.ngControl != null) {
      // Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
      this.ngControl.valueAccessor = this;
    }
  }

  ngOnInit() {
    this.ngControl.control.setAsyncValidators(this.validate.bind(this));
    this.ngControl.control.updateValueAndValidity();
    this.postalArea$ = this.ngControl.valueChanges
      .pipe(
        debounceTime(600),distinctUntilChanged(),switchMap(val => {
          if (!this.ngControl.errors) {
            return this.filter(val || ''); // this.filter(val || '');
          }
        })
      );
  }

  onTouched = (_value?: any) => { };
  onChanged = (_value?: any) => { };

  writeValue(val: string): void {
    this.ngControl.control?.setValue(val);
  }

  registerOnChange(fn: any): void {
    this.onChanged = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.postalArea$.pipe(
      map(postalArea => {
        if (postalArea.length === 0) {
          this.ngControl.control.setErrors({ invalidPostalCode: true });
        } else {
          return null;
        }
      })
    );
  }

  displayFn(postalArea?: PostalArea) {
    return postalArea ? postalArea.zipCode : '';
  }

  onSelectionChanged(event: MatAutocompleteSelectedEvent) {
    this.onChanged(event.option.value.zipCode);
    this.onPostalAreaSelected.emit(event.option.value);
  }

  filter(val: string): Observable<any[]> {
    return this._postalAreaService.getPostalAreas(val)
      .pipe(
        map(response => response.filter(option => {
          return option;
        }))
      );
  }

}

export class CtrlErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: AbstractControl): boolean {
    return !!(control && control.invalid && control.touched);
  }
}