Search code examples
angulartypescriptfilterpipedebouncing

Angular - How to debounce PipeTransform?


I am using *ngFor to display a collection, I also have one input which will be used as a filter criteria (this input is a formControl). Now I have added a PipeTransform to filter my collection based on that input. Everything works as expected but I can't figure out how to debounce. I want my filterPipe to be called 0.5s after last keystroke inside input but at the moment it is called instantly after any change inside my input.

HTML:


    <div class="test-ctr">
      <div class="form-ctr">
        <form class="example-form" [formGroup]="filterForm">>
          <input formControlName="nick" placeholder="ex. hdw"> 
        </form>
      </div>
      <div class="app-cards-ctr">
        <div class="app-card-ctr" *ngFor="let nick of nicks | filterPipe: filterForm.get('nick')?.value">
          <app-card [nick]="nick"></app-card>
        </div>
      </div>
    </div>

Component.ts:


    import { Component, OnInit } from '@angular/core';
    import { FormBuilder } from '@angular/forms';
    
    @Component({
      selector: 'app-test',
      templateUrl: './test.component.html',
      styleUrls: ['./test.component.scss'],
    })
    export class TestComponent implements OnInit {
      nicks: string[] = ['Siema', 'Elo', 'Tu', 'Hdw3DCtV', 'and', 'Gównow', 'Zdzisiu11', 'Zdzisiu1','Zdzisiu2','Zdzisiu3','Zdzisiu4','Zdzisiu5'];
    
      constructor(private formBuilder: FormBuilder) {}
    
      filterForm = this.formBuilder.group({
        nick: ['']
      })
    
      ngOnInit(): void {}
    }

Pipe.ts:


    import { Pipe, PipeTransform } from '@angular/core';
    
    @Pipe({ name: 'filterPipe', pure: true })
    export class FilterPipe implements PipeTransform {
      transform(value: string[], arg: string): string[] {
        console.log(arg);
        return value.filter(
          (e) => e.toLowerCase().indexOf(arg.toLowerCase()) !== -1
        );
      }
    }


Solution

  • Working example in Stackblitz

    HTML:

    <h3>Filter:</h3>
    <form [formGroup]="filterForm">
      <input type="text" formControlName="nick" />
    </form>
    
    <h3>Nicks:</h3>
    <div *ngFor="let nick of nicks | filterPipe: (debouncedControl$ | async)">
      {{ nick }}
    </div>
    

    Component:

    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css'],
    })
    export class AppComponent {
      nicks: string[] = [
        'Siema',
        'Elo',
        'Tu',
        'Hdw3DCtV',
        'and',
        'Gównow',
        'Zdzisiu11',
        'Zdzisiu1',
        'Zdzisiu2',
        'Zdzisiu3',
        'Zdzisiu4',
        'Zdzisiu5',
      ];
    
      constructor(private formBuilder: FormBuilder) {}
    
      filterForm = this.formBuilder.group({
        nick: [''],
      });
    
      debouncedControl$ = this.filterForm.controls.nick.valueChanges.pipe(
        startWith(''),
        debounceTime(500)
      );
    }
    

    Pipe:

    @Pipe({
      name: 'filterPipe',
    })
    export class FilterPipe implements PipeTransform {
      transform(values: string[], inputValue: string): string[] {
        return values.filter((v) => {
          return (
            v.toLowerCase().indexOf((inputValue || '').toLocaleLowerCase()) > -1
          );
        });
      }
    }
    

    All that said, this is probably not a scenario where I would reach for an angular pipe.

    Alternate component solution:

    @Component({
      selector: 'alternate',
      template: `
        <form [formGroup]="filterForm">
            <input type="text" formControlName="nick" />
        </form>
    
        <ul>
          <li *ngFor="let nick of filteredNicks$ | async"> {{nick}} </li>
        </ul>
      `,
    })
    export class AlternateSolutionComponent {
      constructor(private formBuilder: FormBuilder) {}
    
      nicks = ['Siema', 'Elo', 'Tu'];
    
      lowercaseIncludes(a: string, b: string): boolean {
        return a.toLocaleLowerCase().indexOf(b.toLocaleLowerCase()) > -1;
      }
    
      filterForm = this.formBuilder.group({ nick: [''] });
    
      filteredNicks$ = this.filterForm.controls.nick.valueChanges.pipe(
        startWith(''),
        debounceTime(500),
        map((inputValue) => {
          return this.nicks.filter((nick) =>
            this.lowercaseIncludes(nick, inputValue)
          );
        })
      );
    }