47

I am trying to call to a service on input key-up event.

The HTML

<input placeholder="enter name" (keyup)='onKeyUp($event)'>

Below is the onKeyUp() function

onKeyUp(event) {
    let observable = Observable.fromEvent(event.target, 'keyup')
        .map(value => event.target.value)
        .debounceTime(1000)
        .distinctUntilChanged()
        .flatMap((search) => {
            // call the service
        });
    observable.subscribe((data) => {
        // data
    });
}

It was found from the network tab of the browser that, it is calling the key-up function on every key-up event(as it is supposed to do), but what I am trying to achieve is a debounce time of 1sec between each service call. Also, the event is triggered if I move the arrow key move.

plunkr link

martin
  • 93,354
  • 25
  • 191
  • 226
  • Specify RxJS version, because versions 4 and 5 have bunch of differences – DDRamone Jan 30 '17 at 12:13
  • This must be RxJS 5 because he's using Angular2. – martin Jan 30 '17 at 12:16
  • 1
    What you have now is correct so you just don't know how to call the service or what? – martin Jan 30 '17 at 12:46
  • @martin I am able to call the service. But the issue I am facing is too much call to the server. In every key up event, there is a debounce effect. But after the debounce time the service is called multiple times. ie to say if there are a 10 key up events in between a debounce time, the service is called 10 times. Also if I moved the left-right arrow keys with the same search term, the service is called(which is undesired). –  Jan 31 '17 at 05:50
  • @DDRamone "rxjs": "5.0.0-beta.12" –  Jan 31 '17 at 05:50
  • @varun This is weird because it `debounceTime()` ignores items and you also have `distinctUntilChanged()` that should ignore duplicates. I'm suspicious the problem will be someplace else. Can you make a plnkr reproducing the problem? – martin Jan 31 '17 at 06:55
  • @martin http://plnkr.co/edit/2YOezky5qB2h4tzXojzi –  Jan 31 '17 at 08:27

4 Answers4

77

So the chain is really correct but the problem is that you're creating an Observable and subscribe to it on every keyup event. That's why it prints the same value multiple times. There're simply multiple subscriptions which is not what you want to do.

There're obviously more ways to do it correctly, for example:

@Component({
  selector: 'my-app',
  template: `
    <div>
      <input type="text" (keyup)='keyUp.next($event)'>
    </div>
  `,
})
export class App implements OnDestroy {

  public keyUp = new Subject<KeyboardEvent>();

  private subscription: Subscription;

  constructor() {
    this.subscription = this.keyUp.pipe(
      map(event => event.target.value),
      debounceTime(1000),
      distinctUntilChanged(),
      mergeMap(search => of(search).pipe(
        delay(500),
      )),
    ).subscribe(console.log);
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

See your updated demo: http://plnkr.co/edit/mAMlgycTcvrYf7509DOP

Jan 2019: Updated for RxJS 6

martin
  • 93,354
  • 25
  • 191
  • 226
  • 7
    This works, but is little bit misleading. keyUp is no of type Subject. If you want Subject you have to specify in input `(keyup)='keyUp.next($event.target.value)'` – Ondrej Peterka Dec 21 '17 at 20:46
  • 1
    What if the user hits "Enter" you would like for the what ever logic to execute without waiting for the denounce. Is there a way to ignore the denounce? – BlackEagle Dec 04 '18 at 15:52
  • @BlackEagle just add the same function as in the template for the keyup.enter event, such as (keyup.enter)="console.log($event.target.value)" – Torsten N. Apr 20 '20 at 09:57
9

@marlin has given a great solution and it works fine in angular 2.x but with angular 6 they have started to use rxjs 6.0 version and that has some slight different syntax so here is the updated solution.

import {Component} from '@angular/core';
import {Observable, of, Subject} from 'rxjs';
import {debounceTime, delay, distinctUntilChanged, flatMap, map, tap} from 'rxjs/operators';

@Component({
    selector: 'my-app',
    template: `
        <div>
            <input type="text" (keyup)='keyUp.next($event)'>
        </div>
     `,
})
export class AppComponent {
    name: string;

    public keyUp = new Subject<string>();

    constructor() {
        const subscription = this.keyUp.pipe(
            map(event => event.target.value),
            debounceTime(1000),
            distinctUntilChanged(),
            flatMap(search => of(search).pipe(delay(500)))
        ).subscribe(console.log);
    }
}
Imal Hasaranga Perera
  • 9,683
  • 3
  • 51
  • 41
1

Well, here's a basic debouncer.

ngOnInit ( ) {
        let inputBox = this.myInput.nativeElement;
        let displayDiv = this.myDisplay.nativeElement;

        let source = Rx.Observable.fromEvent ( inputBox, 'keyup' )
            .map ( ( x ) => { return x.currentTarget.value; } )
            .debounce ( ( x ) => { return Rx.Observable.timer ( 1000 ); } );

        source.subscribe (
            ( x ) => { displayDiv.innerText = x; },
            ( err ) => { console.error ( 'Error: %s', err ) },
            () => {} );
    }
}

If you set up the two indicated view children (the input and the display), you'll see the debounce work. Not sure if this doesn't do anything your does, but this basic form is (as far as I know) the straightforward way to debounce, I use this starting point quite a bit, the set of the inner text is just a sample, it could make a service call or whatever else you need it to do.

Tim Consolazio
  • 4,802
  • 2
  • 19
  • 28
1

I would suggest that you check the Angular2 Tutorial that show a clean and explained example of something similar to what you're asking.

https://angular.io/docs/ts/latest/tutorial/toh-pt6.html#!#observables

Simon Dufour
  • 184
  • 7