Angular formArray と仲良くなる
はじめに
Angular のチュートリアルを終えて、簡単なアプリケーションの Showcase のようなものを Firebase 上に運用しています。
今回は Angular formArray と仲良くなるべく3つほどTipsを書きました。
Angular formArray サンプル
サンプルアプリケーションのソースコードを Github で公開しています。
何点かつまづいた点を中心に解説していきます。
Angular formArray 内の autocomplete を filter したい
formControl 内の Filter autocomplete については公式に stackblitz 含め example があります。
しかし、動的に追加される formArray に対して 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>
filteredOptions
は Observable<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 内の重複した key をまとめたい
formArray で要素を追加できる UI において、重複された key が選択される場合があります。
例えばこのサンプルでいうと以下のように Apple
が2行に渡って選択されている状態です。
パッと思いつく制御として以下2つがありました。
- 重複はエラーとする
- 重複した key を集計して個数カウントおよびエラー実装を行う
今回は後者の集計を実装してみました。
まず key である product に入力があるものだけを arr という投げやりな変数に詰めています。
空のマップ const m = new Map();
に対して product を key に productNumber を集計し forEach で product と productNumber を詰め直しています。
export interface Products { product: Product; productNumber: number; } export interface Order { products: Products[]; } ~~ 省略 ~~ calculate(order?: Order) { const arr = []; for (const p of order.products) { if (p.product) { arr.push(p); } } const m = new Map(); this.productsList = []; arr.reduce((aggr, current) => { aggr.set(current.product, aggr.has(current.product) ? aggr.get(current.product) + current.productNumber : current.productNumber); return aggr; }, m).forEach(function(value, key) { this.push({product: key, productNumber: value}); }, this.productsList); }
map / reduce 便利ですね。
これでAngular formArray 内の重複した key をまとめることができました。
Angular formArray にデータが追加される度に Angular material の dataTable にデータを足したい
formArray として追加され、重複した key を集計した後に dataTable に pushします。
Angular material datatable はどことなく扱いづらい印象がありますが、かなり簡単でした。
MatTableDataSource
を import し FormGroup.valueChanges の度に productsList を更新するだけです。
import { MatTableDataSource } from '@angular/material'; data = new MatTableDataSource(this.productsList); calculate(order?: Order) { this.data = new MatTableDataSource(this.productsList); }
また、material datatable は template 内で簡単な演算ができてしまうので、小計の計算をやってみました。
もちろん ts 側で calcSubTotal()
のようなメソッドを作成して template から呼び出すこともできそうです。
<ng-container matColumnDef="Subtotal"> <th mat-header-cell *matHeaderCellDef> Subtotal </th> <td mat-cell *matCellDef="let data">{{ data.product.price * data.productNumber || 0 | number}}</td> <td mat-footer-cell *matFooterCellDef>{{getTotal() || 0 | number}}</td> </ng-container>
まとめ
Angular formArray と仲良くなるべく、サンプルアプリケーションを作成してみました。
ソースコードを公開しています。githubはこちら。