Angular formArray 内の autocomplete を filter する

はじめに

可変長の Angular formArray 内で autocomplete の要素を filter する方法をまとめます。
formArray が可変長の場合 filter を当てる autocomplete 要素の位置を特定する必要があるので少し工夫が必要でした。

サンプルアプリケーションのソースコードGithub で公開しています。

github.com

アプリケーションはこちら

fir-angular-showcase.web.app

Angular formArray 内の autocomplete を filter する

mat-option*ngFor="let product of filteredOptions[index] | async" のように filteredOptions が formArray の何番目であるかを認識させています。

<div *ngFor="let c of products.controls; let index=index; let last=last;" [formGroupName]="index">

  <mat-form-field>
    <input #stateInput matInput type="text" placeholder="Product" aria-label="Number" formControlName="product" [matAutocomplete]="auto">
    <button mat-button *ngIf="stateInput.value" matSuffix mat-icon-button aria-label="Clear" (click)="delSelectedProduct(index)">
      <mat-icon>close</mat-icon>
    </button>
          
    <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
      <mat-option (onSelectionChange)="stateInput.value != undefined" *ngFor="let product of filteredOptions[index] | async" [value]="product">
        {{product.productName}}
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>

</div>

filteredOptionsObservable<Product[]>[] = []; のように定義しています。
managedFilter というメソッドで formArray の index を受け取れるようにしておき入力された文字列を Product.productName に対して filter を試みます。
_filter というメソッドで実際に filter を実施しますが、Angular material の autocomplete で扱いやすいように Product[] のような Interface の配列として return するようにします。
template との紐付けは 要素が足される addInput() メソッドの中で this.managedFilter(this.products.length - 1); のようにしているのと、初期化時に this.managedFilter(0); としています。ここはなんかあまりかっこよくないですね。どうすれば良いのか。。

export interface Product {
  productName: string;
  productCode: string;
  price: number;
  maxQuantity: number;
}

~~ 省略 ~~

filteredOptions: Observable<Product[]>[] = [];

ngOnInit() {
    ~~ 省略 ~~
    this.managedFilter(0);
}

managedFilter(index: number) {
    const arrayControl = this.productFormGroup.get('products') as FormArray;
    this.filteredOptions[index] = arrayControl.at(index).get('product').valueChanges
      .pipe(
      startWith<string | Product>(''),
      map(value => typeof value === 'string' ? value : value.productName),
      map(productName => productName ? this._filter(productName) : this.sampleProducts.slice())
      );
}

private _filter(value: string): Product[] {
    const filterValue = value.toLowerCase();
    return this.sampleProducts.filter(option => option.productName.toLowerCase().includes(filterValue));
}

addInput() {
    this.products.push(this.formBuilder.group({
      product: this.formBuilder.control('', []),
      productNumber: this.formBuilder.control(1, [Validators.min(1), CustomValidator.integer]),
    }, { validators: CustomValidator.maxQuantity }));

    this.managedFilter(this.products.length - 1);
}

以上でAngular formArray 内の autocomplete を filter することができます がなんとか動いている状態を作り出せます。

まとめ

Angular formArray 内の autocomplete を filter する方法をまとめました。
サンプルアプリケーションのソースコードGithub で公開しています。

github.com