2

rxjs is pretty challenging for me and I have found myself stuck trying to solve this problem. The closest solution I have found on stack is the usage of merge operator. Here is the link

I am working in angular 2.

I have an input search field in html

<input (keydown.enter)="setFocus()" id="search-box" name="input-box" class="search-box" type="text" placeholder="Client search" (focus)="clearWarnings()" />

The user types in the field box which will trigger the corresponding function after a preset delay. Also the user may press the enter key( and a search icon) to trigger the search. What I aim here is to whenever the user hits the enter key, the debounce should NOT trigger the search because it is already running.

Here is the code so far using the merge function although it does not seem to work the way I intend.

ngAfterViewInit() {
        this.currentMember = this.appHelpersService.getLocalStorageSearchedRim();
        if (this.currentMember) {
            this.searchService.changeSearchTerm(this.currentMember);
        }

        var waitTime = AppConstants.SEARCH_TEXT_WAITTIME;
        const searchSource = document.getElementById("search-box");
        const keydownStream = fromEvent(this.elementRef.nativeElement, 'keyup');
        const inputStream = fromEvent(searchSource, "input");
        const allStreams = merge(keydownStream, inputStream);
        allStreams
            .pipe(
                map((event: KeyboardEvent | MouseEvent) => (<HTMLInputElement>event.target).value.trim()),
                filter((searchTerm: string) => {
                    waitTime = Number(searchTerm) ? AppConstants.SEARCH_NUMERIC_WAITTIME : AppConstants.SEARCH_TEXT_WAITTIME;
                    return searchTerm.length >= 2;
                }),
                debounce(() => timer(waitTime)),
                distinctUntilChanged()
            )
            .subscribe((searchTerm: string) => {
                this.showProgressbar = true;
                this.listSearchResults(searchTerm);
            });

    }

And the enter key event:

 setFocus(): void {
        const searchBox: HTMLElement = document.getElementById("search-box");
        const searchTerm = (<HTMLInputElement>searchBox).value;
        if (searchTerm && searchTerm.length > 0) {
            this.listSearchResults(searchTerm);
        }
        searchBox.focus();
    }

In the solution I have mentioned all events merged together will trigger the function but not necessarily cancel the other event that is waiting (debounce).

Thanks for the time

Tristan Forward
  • 3,304
  • 7
  • 35
  • 41
Joey Vico
  • 43
  • 1
  • 8
  • So Enter Key Press should be an immediate search while "typeahead" should be debounced? – Nico Aug 24 '18 at 04:19
  • Correct but with the requirement that if user hits Enter key the typeahead should not fire the function. There is no point of having the debounce firing another search if it is already running. – Joey Vico Aug 24 '18 at 05:12

1 Answers1

2

I think you have some errors in your snippet

const keydownStream = fromEvent(this.elementRef.nativeElement, 'keyup');

should be

const keyupStream = fromEvent(this.elementRef.nativeElement, 'keyUp');

And you really don't need another fromEvent since your keyupStream will already have the value from the input

Your enter function call and your search "typeahead" function calls has to be wrapped in an observable in order to cancel them.

Given that they are you could do something like

const search$ = fromEvent(this.search.nativeElement, 'keyup').pipe(share());
const searchKeyEnter$ = search$.pipe(filter((e: KeyboardEvent) => e.keyCode === 13 || e.which === 13))
const searchText$ = search$.pipe(filter((e: KeyboardEvent) => e.keyCode !== 13 && e.which !== 13), debounceTime(500))

const mergeKeyDown = merge(searchText$.pipe(mapTo('search')), searchKeyEnter$.pipe(mapTo('enter')))
  .pipe(
  withLatestFrom(search$),
  filter(([origin, data]) => data.target.value.length > 2),
  distinctUntilChanged(),
  switchMap(([origin, data]) => {
    if (origin === 'search') {
      console.log('search started')
      return of('').pipe(delay(3000), tap(() => console.log('search call has finished')))
    } else {
      return of('').pipe(tap(() => console.log(' i got called from enter')));
    }
  })
  ).subscribe(() => { })

What's happening here is that we share the event from the user typing in the input

fromEvent(this.search.nativeElement, 'keyup').pipe(share());

So that we distribute it to create and compose new observables of a specific type

Example to only take the enter key:

search$.pipe(filter((e: KeyboardEvent) => e.keyCode === 13 || e.which === 13))

We use mapTo so we can differentiate between which event was fired.

When any of those events are fired we want to again reuse the value which was just updated from the input using withLatestFrom.

Now in order to cancel any inflight async task the switchMap operator can be used.

A big thing while working with Observables is to create them so that you can reuse and compose them.

I created a stackblitz you can fork and try it out for yourself pay attention to the console.

https://stackblitz.com/edit/merging-events?file=src/app/app.component.ts

Hope this helps!

Nico
  • 1,961
  • 17
  • 20
  • Will be looking into this at home since at work stackblitz is blocked. Much appreciated Nico, I will let you know how it goes. – Joey Vico Aug 24 '18 at 06:05