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

I need to create a search entry that filters the list of sub-accounts in each parent account. Currently, when you enter any of the input filters, all accounts are used instead of just the associated account.

Live Examples (StackBlitz)
Basic (no FormArray)
With FormArray

Requirements

  • The number of accounts and sub-accounts is unknown (1 ... *)
  • Each account requires its own search login (FormControl) in HTML
  • Entering text on input A should filter the list only for account A. Entering text on input B should filter the list only for account B.

Questions

  • How can I guarantee that each FormControl only filters the account in the current * ngFor context?

  • How can I independently look at an unknown number of FormControls to change values? I understand that I can see FormArray, but I hope that there will be a better way.

Ideally, the solution should:

  • Use reactive forms
  • Change the observed value when changing the value
  • Allow adding / removing FormControls from the form dynamically
+5
source share
4 answers

This is my approach to solving your problem.

First, you formControl over external accounts and create a dedicated formControl for each account and save them to FormGroup . As a reference ID, I used the account number. In doing so, I declared a function called getSearchCtrl(accountNumber) to retrieve the right formControl .

Use [formControlName]="account.accountNumber" to connect your template using your formControls provided in FormGroup .

Match the correct formControl with getSearchCtrl(account.accountNumber) with your filter pipe and pass the value.

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

I also edited your stackblitz app: https://stackblitz.com/edit/angular-reactive-filter-4trpka?file=app%2Fapp.component.html

I hope this solution helps you.

 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
source

Ref Angular.io - Pipe - Application: No FilterPipe or OrderByPipe

Angular does not offer such pipes because they work poorly and prevent aggressive minimization.

Based on this tip, I would replace the pipe filter with a method.

As @Claies says, you also need to store your search terms separately.
Since the number of accounts is not known at compile time, initialize the searchTerm array with Array(this.accounts.length) and process empty search terms with this.searchTerms[accountIndex] || '' 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> 

Link: StackBlitz

+1
source

In the previous answer, the filter function will be called whenever a change is detected (basically, any event in the browser that is not associated with this component), which can be bad. A more angular and efficient way is to use the power of the 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 ) ) } 

Written on a tablet from memory, if it does not work when copying, use ideological tips to find errors. :)

Thus, you can even pass the API call to the same observable, removing the need to subscribe and unsubscribe yourself, since the async channel handles all this.

Edit: to adapt this to work with 2 inputs, you can combine .fromEvent () from both, and in the filter call change the behavior according to evt.target.id. Sorry for the incomplete example, writing code on the tablet is horrible: D

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

+1
source

Another way is to create an account component to encapsulate each account and find it. In this case, there is no need for a pipe - Application Example

 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()) ); } } 

The parent will

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

Source: https://habr.com/ru/post/1275278/


All Articles