如何使用*ngFor过滤Angular中未知数量的元素?
How to use *ngFor to filter an unknown number of elements in Angular?
我需要创建一个搜索输入来过滤每个父帐户中的子帐户列表。目前,输入任何一个输入都会过滤所有帐户,而不仅仅是关联的帐户。
实例 (StackBlitz)
Basic (no FormArray)
With FormArray
要求
- 帐户和子帐户的数量未知 (1...*)
- 每个帐户都需要在 HTML
中有自己的搜索输入 (FormControl)
- 输入 A 应该只过滤帐户 A 的列表。
键入输入 B 应仅过滤帐户 B 的列表。
问题
如何保证每个FormControl只过滤当前*ngFor上下文中的账号?
如何独立观察未知数量的 FormControl 的值变化?我意识到我可以观看 FormArray,但我希望有更好的方法。
理想情况下,解决方案应该:
- 使用响应式表单
- 当值改变时发出一个 Observable
- 允许 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 连接起来。
将正确的 formControl
和 getSearchCtrl(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 = [
...
];
}
我需要创建一个搜索输入来过滤每个父帐户中的子帐户列表。目前,输入任何一个输入都会过滤所有帐户,而不仅仅是关联的帐户。
实例 (StackBlitz)
Basic (no FormArray)
With FormArray
要求
- 帐户和子帐户的数量未知 (1...*)
- 每个帐户都需要在 HTML 中有自己的搜索输入 (FormControl)
- 输入 A 应该只过滤帐户 A 的列表。
键入输入 B 应仅过滤帐户 B 的列表。
问题
如何保证每个FormControl只过滤当前*ngFor上下文中的账号?
如何独立观察未知数量的 FormControl 的值变化?我意识到我可以观看 FormArray,但我希望有更好的方法。
理想情况下,解决方案应该:
- 使用响应式表单
- 当值改变时发出一个 Observable
- 允许 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] || ''
.
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 连接起来。
将正确的 formControl
和 getSearchCtrl(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 = [
...
];
}