【问题标题】:How to use *ngFor to filter an unknown number of elements in Angular?如何使用 *ngFor 过滤 Angular 中未知数量的元素?
【发布时间】:2018-07-20 20:25:28
【问题描述】:

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

现场示例(StackBlitz)
Basic (no FormArray)
With FormArray

要求

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

问题

  1. 如何确保每个 FormControl 只过滤当前 *ngFor 上下文中的帐户?

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

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

  1. 使用响应式表单
  2. 当值改变时发出一个 Observable
  3. 允许在表单中动态添加/删除 FormControls

【问题讨论】:

  • 将您的问题缩小到段落。
  • 很遗憾,您不太可能得到任何已售出的答案,因为您没有将代码包含在问题正文中。链接到您的代码可能正在运行的第 3 方网站并不能替代它。
  • 话虽如此,看看您的示例,您只有一个 searchTerm 属性,但您有多个要过滤的数组。您需要找到一种方法使每个搜索框都独一无二(可能使用父中继器中的ind),并且每个子中继器都有一个唯一的searchTerm
  • 感谢您的提示!我会在星期一试一试。我认为 Stackbliz 会更好,因为它有很多代码。我因为这个问题太长而被否决了,但我会记住这一点!
  • 克莱斯把我引向了正确的方向。我正在使用以下(目前):stackblitz.com/edit/angular-form-array-basic-solution

标签: angular typescript rxjs angular-reactive-forms angular-forms


【解决方案1】:

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

Angular 不提供这样的管道,因为它们的性能很差并且会阻止激进的缩小。

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

正如@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

【讨论】:

  • 归根结底,我想我使用管道,特别是如果有可能在超过一个组件。
  • @AluanHaddad,我最近看到了关于纯过滤器管道(或者更确切地说是与数组一起使用时的管道)的进一步解释,它们不响应输入数组的更改。要使他们这样做,需要pure: false 属性,但随后他们将响应所有更改周期更新,这是性能受到影响的地方。我注意到问题中没有公开给出处理帐户数组更改的要求,不过后来的评论好像已经介绍过了。
  • 这实际上是一个非常困难的问题。由于数组选区可以更改而不影响长度,因此它意味着显式检查或脏检查。
  • @AluanHaddad - 不用担心被否决,请注意有些晦涩的文档。
  • 不过,这是一个很好的答案,而且您确实实现了一个管道。我会注意到,如果 Angular 无法确定 expr 的派生方式,则需要重新评估表达式 filteredSubaccounts(expr)。顺便说一句,如果您使用changeDetectionStrateg.onPush,则意味着需要重新绑定数组并且纯管道就足够了。
【解决方案2】:

在上一个答案中,过滤器函数将在每次更改检测时调用(基本上浏览器中与此组件无关的任何事件),这可能很糟糕。一种更加 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

【讨论】:

  • 您将如何使用响应式表单来实现这一点(例如,我们不会丢失表单验证)?
  • 您可以使用 this.myForm.get('searchControl').valueChanges 作为流的来源,而不是 Observable.fromEvent()。
  • 这就是我目前正在使用的,但使用的是 FormArray。我认为 RXJS 最终会导致最好的解决方案,但你的回答还没有让我到达那里(我需要继续前进)。我赞成你的回答。谢谢!
  • 谢谢!抱歉,我无法发布完整的代码。挂在这里时通常只在平板电脑上。无论如何,从长远来看,对 rxjs 感到满意肯定会得到回报。数十小时但数百小时:)
【解决方案3】:

这是我解决问题的方法。

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

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

将正确的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>

【讨论】:

  • 这个解决方案有几个潜在的问题: 1. 由于Form是在构造函数中创建的并且不使用FormArray,我认为没有一种简单的方法可以在不重新创建的情况下动态添加或删除FormControls整个表格。 2.getSearchCtrl方法在Angular每次检测到变化时调用,每个账号调用一次,可能会影响性能。可能有一个更简单的 RXJS 解决方案,但我很欣赏答案使用 Reactive Forms 并对我的代码进行了最小的更改。这是对我最初的问题最有帮助的回答。
【解决方案4】:

另一种方法是创建一个帐户组件来封装每个帐户及其搜索。在这种情况下不需要管道 - 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())
    ); 
  }
}

父母是

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 = [
    ...
  ];
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-06-27
    • 1970-01-01
    • 1970-01-01
    • 2019-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-16
    相关资源
    最近更新 更多