47

I have a component that wraps another component <inner-component> and binds to the InnerComponent.innerChanged() custom event. I want to bubble up using an @output property, but I also want to debounce the output.

How do I use RxJS .debounce() or .debounceTime() to do this?

Something like this:

import {Component, Output, EventEmitter} from 'angular2/core';
import 'rxjs/add/operator/debounce';
import 'rxjs/add/operator/debounceTime';

@Component({
  selector: 'debounced-component',
  template: `
    <div>
      <h1>Debounced Outer Component</h1>
      // export class InnerComponent{
      //   @Output() innerChanged: new EventEmitter<string>();
      //   onKeyUp(value){
      //     this.innerChanged.emit(value);
      //   }
      // }
      <input #inner type="text" (innerChange)="onInnerChange(inner.value)">
    </div>
  `
})
export class DebouncedComponent {
  @Output() outerValueChanged: new EventEmitter<string>();

  constructor() {}

  onInnerChange(value) {
    this.outerValuedChanged.emit(value); // I want to debounce() this.
  }
}
michael
  • 4,377
  • 8
  • 47
  • 73
  • Seems like duplicate of [this question](http://stackoverflow.com/a/36849347/2435473) – Pankaj Parkar Oct 10 '16 at 13:18
  • I don't think it's the same because I can't use `Observable.fromEvent()` and I don't have a `FormControl.valueChanges`. `this.outerValuedChanged.debounce(500).emit(value)` doesn't seem to work... – michael Oct 10 '16 at 14:25

2 Answers2

99

To debounce values you could use a Subject. A subject is both an observable and a observer. This means you can treat it as an observable and pass values to it as well.

You could leverage this to pass the new values from the inner-component to it and debounce it this way.

export class DebouncedComponent {
  @Output() outerValueChanged: new EventEmitter<string>();
  const debouncer: Subject<string> = new Subject<string>();

  constructor() {
      // you listen to values here which are debounced
      // on every value, you call the outer component
      debouncer
        .debounceTime(100)
        .subscribe((value) => this.outerValuedChanged.emit(value));
  }

  onInnerChange(value) {
    // send every value from the inner to the subject
    debouncer.next(value);
  }
}

This is untested pseudo-code. You can see a working example of the concept here (http://jsbin.com/bexiqeq/15/edit?js,console). It's without angular but the concept remains the same.


Update: For newer versions of Angular you might need a slight: change debouncer.debounceTime(100) gets changed to debouncer.pipe(debounceTime(100))

constructor() {
      // you listen to values here which are debounced
     // on every value, you call the outer component
     debouncer
       .pipe(debounceTime(100))
       .subscribe((value) => this.outerValuedChanged.emit(value));
}
Pinna_be
  • 4,517
  • 2
  • 18
  • 34
KwintenP
  • 4,637
  • 2
  • 22
  • 30
  • 2
    thank you! I only had to change to `private debouncer: Subject = new Subject();` to make TS happy. – michael Oct 10 '16 at 15:10
  • This for some reason does NOT work on Angular2 (check github issues). Try this way: https://stackoverflow.com/questions/32051273/angular2-and-debounce (value is triggered without any debounce) – Romain Bruckert Jun 05 '17 at 13:21
  • It is working for angular 2 perfectly fine. Thank you! – Baso Oct 12 '17 at 13:39
  • 5
    You shouldn't forget to unsubscribe onDestroy – Kirill Groshkov Aug 16 '19 at 18:06
  • Hi, how can I adjust this flow for multiple incoming events which are depending on a given ID ... So I am getting a bunch of events with different object Ids and I want to debounce this for every target object Id every xxx milli seconds? So I need for every object id a debouncer responsible only for one object id? – ArgV Mar 29 '20 at 10:58
7

Here is a working example Class with all needed imports, Angular 4+, TypeScript and tslint-friendly :) thought it might help some people looking for what I was looking moments ago!

import { Component, Input, Output, EventEmitter, ViewChild, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/debounceTime';

@Component({
  selector: 'app-searchbox',
  template: `
    <input type="text" autocomplete="off" [(ngModel)]="query" [placeholder]="placeholder" (change)="onInputChange($event)">
  `
})
export class SearchboxComponent implements OnDestroy {
  @Input() debounceTime = 500;
  @Output() change = new EventEmitter<string>();

  query = '';
  placeholder = 'Search...';
  results;

  debouncer = new Subject<string>();
  subs = new Array<Subscription>();

  constructor() {
    this.subs.push(this.debouncer.debounceTime(this.debounceTime).subscribe(
      (value: string) => { this.change.emit(value); },
      (error) => { console.log(error); }
    ));
  }

  onInputChange(event: any) {
    this.debouncer.next(event.target.value);
  }

  ngOnDestroy() {
    for (const sub of this.subs) {
      sub.unsubscribe();
    }
  }
}
Rom
  • 426
  • 5
  • 11
  • I've noticed that the `@Input() debounceTime` is not honored here if edited in the html; it always uses the default value. How can I ensure that it uses the custom value? – Logan Jahnke Jun 06 '18 at 14:42
  • Do you have a Jsfiddle with the html you mentioned? Make sure that your parent cmp sends the number as an Input eg. `` – Rom Jun 14 '18 at 09:44