如何使用*ngFor过滤Angular中未知数量的元素?

How to use *ngFor to filter an unknown number of elements in Angular?

我需要创建一个搜索输入来过滤每个父帐户中的子帐户列表。目前,输入任何一个输入都会过滤所有帐户,而不仅仅是关联的帐户。

实例 (StackBlitz)
Basic (no FormArray)
With FormArray

要求

  1. 帐户和子帐户的数量未知 (1...*)
  2. 每个帐户都需要在 HTML
  3. 中有自己的搜索输入 (FormControl)
  4. 输入 A 应该只过滤帐户 A 的列表。
    键入输入 B 应仅过滤帐户 B 的列表。

问题

  1. 如何保证每个FormControl只过滤当前*ngFor上下文中的账号?

  2. 如何独立观察未知数量的 FormControl 的值变化?我意识到我可以观看 FormArray,但我希望有更好的方法。

理想情况下,解决方案应该:

  1. 使用响应式表单
  2. 当值改变时发出一个 Observable
  3. 允许 FormControls added/removed 来自表单动态

参考 Angular.io - Pipe - Appendix: No FilterPipe or OrderByPipe

Angular doesn't offer such pipes because they perform poorly and prevent aggressive minification.

根据该建议,我将用一种方法替换管道过滤器。

正如@Claies所说,您还需要单独存储搜索词。
由于在编译时帐户数量未知,因此使用 Array(this.accounts.length) 初始化 searchTerm 数组并使用 this.searchTerms[accountIndex] || ''.

处理空的 searchTerms

app.component.ts

export class AppComponent {
  accounts = [
    {
      accountNumber: '12345',
      subAccounts: [ 
        { accountNumber: '123' },
        { accountNumber: '555' },
        { accountNumber: '123555' }
      ]
    },
    {
      accountNumber: '55555',
      subAccounts: [
        { accountNumber: '12555' },
        { accountNumber: '555' }
      ]
    }
  ];

  searchTerms = Array(this.accounts.length)

  filteredSubaccounts(accountNo, field) {
    const accountIndex = this.accounts.findIndex(account => account.accountNumber === accountNo);
    if (accountIndex === -1) {
      // throw error
    }
    const searchTerm = this.searchTerms[accountIndex] || '';    
    return this.accounts[accountIndex].subAccounts.filter(item => 
      item[field].toLowerCase().includes(searchTerm.toLowerCase()));
  }
}

app.component.html

<div>
    <div *ngFor="let account of accounts; let ind=index" class="act">
        <label for="search">Find an account...</label>
        <input id="search" [(ngModel)]="searchTerms[ind]" />
        <div *ngFor="let subAccount of filteredSubaccounts(account.accountNumber, 'accountNumber'); let i=index">
            <span>{{subAccount.accountNumber}}</span>
        </div>
    </div>
</div>

参考:StackBlitz

在之前的回答中,过滤器函数将在每次更改检测时调用(基本上与此组件无关的浏览器中的任何事件),这可能很糟糕。一种更 Angular 和高效的方法是利用 Observable 的强大功能:

<input #search/>
<div *ngIf="items$ | async as items">
   <div *ngFor="let item of items">{{item.name}}</div>
</div>

items:any[];
items$:Observable<any>;
@ViewChild('search') search:Elementref;
ngOnInit() {
   const searchProp = 'name';
   this.items$ = Observable.fromEvent(this.search.nativeElement, 'keyup').pipe(
      startWith(''),
      debounceTime(500), // let user type for half a sec
      distinctUntilChanged(), // don't run unless changed
      map(evt => this.items.filter(
         item => !evt.target.value || 
         item[searchProp].toLowerCase().indexOf(evt.target.value) >-1 
         )
      )
}

凭记忆写在平板电脑上,如果复制粘贴无效,请使用ide提示查找错误。 :)

通过这种方式,您甚至可以将 API 调用通过管道传输到同一个可观察对象中,从而无需自己订阅和取消订阅,因为异步管道会处理所有这些。

编辑:为了使其适用于 2 个输入,您可以合并两者的 .fromEvent(),并在过滤器调用中根据 evt.target.id 更改行为。抱歉,示例不完整,在平板电脑上编写代码太可怕了:D

RXJS 合并:https://www.learnrxjs.io/operators/combination/merge.html

这是我解决你问题的方法。

首先,您遍历外部帐户并为每个帐户创建一个专用 formControl 并将它们存储在 FormGroup 中。作为 ID 参考,我使用了帐号。有了这个,我声明了一个名为 getSearchCtrl(accountNumber) 的函数来检索正确的 formControl.

使用 [formControlName]="account.accountNumber" 将您的模板与 FormGroup 中提供的 formControl 连接起来。

将正确的 formControlgetSearchCtrl(account.accountNumber) 引用到您的 filter pipe 并传递值。

<div *ngFor="let subAccount of account.subAccounts | filter : 'accountNumber': getSearchCtrl(account.accountNumber).value; let i=index"> <span>{{subAccount.accountNumber}}</span> </div>

我还编辑了你的 stackblitz 应用程序:https://stackblitz.com/edit/angular-reactive-filter-4trpka?file=app%2Fapp.component.html

希望此解决方案对您有所帮助。

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl } from '@angular/forms';
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  searchForm: FormGroup;
  searchTerm = '';
  loaded = false;

  // Test Data. The real HTTP request returns one or more accounts.
  // Each account has one or more sub-accounts.
  accounts = [/* test data see stackblitz*/]

  public getSearchCtrl(accountNumber) {
    return this.searchForm.get(accountNumber)
  }

  constructor() {
    const group: any = {}
    this.accounts.forEach(a => {
      group[a.accountNumber] = new FormControl('')
    });
    this.searchForm = new FormGroup(group);
    this.loaded = true;
  }

  ngOnInit() {
    this.searchForm.valueChanges.subscribe(value => {
      console.log(value);
    });
  }
}
<ng-container *ngIf="loaded; else loading">

  <div [formGroup]="searchForm">
   <div *ngFor="let account of accounts; let ind=index" class="act">
    <label for="search">Find an account...</label>
    <input id="search" [formControlName]="account.accountNumber" />
    <div *ngFor="let subAccount of account.subAccounts | filter : 'accountNumber': getSearchCtrl(account.accountNumber).value; let i=index">
     <span>{{subAccount.accountNumber}}</span>
    </div>
   </div>
  </div>
</ng-container>

<ng-template #loading>LOADING</ng-template>

另一种方法是创建一个帐户组件来封装每个帐户及其搜索。在这种情况下不需要管道 - Example app

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-account',
  template: `
    <label for="search">Find an account...</label>
    <input id="search" [(ngModel)]="searchTerm" />
    <div *ngFor="let subAccount of subAccounts()">
      <span>{{subAccount.accountNumber}}</span>
    </div>
    <br/>
  `
})
export class AccountComponent {

  @Input() account;
  @Input() field;
  private searchTerm = '';

  subAccounts () {
    return this.account.subAccounts
      .filter(item => item[this.field].toLowerCase()
        .includes(this.searchTerm.toLowerCase())
    ); 
  }
}

parent 会是

import { Component } from '@angular/core';
@Component({
  selector: 'my-app',
  template: `
    <div>
      <div>
        <app-account *ngFor="let account of accounts;"
          class="act" 
          [account]="account" 
          [field]="'accountNumber'">
        </app-account>
      </div>
    </div>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  accounts = [
    ...
  ];
}