angularrxjsswitchmap

Angular|RxJS - Type-ahead search request gets cancelled by browser (on click)


I am new to RxJS (and frontend) but I know that switchMap() in the following code is used to break and re-run a search request as the user types into a text box. Search works fine, the only problem is that if the user clicks anywhere outside the textbox (like anywhere on the page, or on a different browser tab or window, or chrome devtools) after entering the text and before the request is fulfilled, the browser cancels the search request altogether.

(Note: The search request is cancelled and re-run correctly if user keeps typing while the request is being processed. Only click event causes the complete request cancellation without re-run)

enter image description here

  search = (input: Observable<string>) => {
    return input.pipe(
      debounceTime(500),
      distinctUntilChanged(),
      switchMap((text) => text.length < 2 ? this.clearDropDown() // clear dropdown showing list of item names.
        : this.onKeyUp(text).pipe(
          map(result => {
           this.spinnerService.end(); // stop loading spinner.
          if (results.data) {
              return results.data; // return list of item names for the dropdown.
          }
      })
        )));
  }

  onKeyUp(text: string): Observable<any> {
      this.spinnerService.start(); // start loading spinner animation overlay on the page.
      return this.http.post(this.apiUrlString, { 'itemName': text }); // this search request works fine.
  }

The HTML, in case it is relevant:

    <div>
      <input
        id="itemSearchValue"
        type="text"
        class="form-control/bg-light/rounded/dropdown-toggle"
        [(ngModel)]="selectedItem"
        (selectItem)="onSelectItem($event)" // irrelevant to the question.
        formControlName="itemSearchValue"
        [ngbTypeahead]="search"
        #instance="ngbTypeahead" />
    </div>

How can I make sure the request is not cancelled by the browser when the user emits clicks somewhere? I'd like to keep the switchMap() method.


Solution

  • Looks like all subscriptions are being cancelled onBlur

    From the github: https://github.com/ng-bootstrap/ng-bootstrap/blob/master/src/typeahead/typeahead.ts

      handleBlur() {
        this._resubscribeTypeahead.next(null);
        this._onTouched();
      }
    

    So the solution is to not let the type-ahead have access to the network request subscription. Just subscribe within the component and pass a reference to the data instead.

      data: string[] = [];
      sub = new Subscription();
    
      search = (input: Observable<string>) => {
        return input.pipe(
          debounceTime(500), // half second buffer before request starts
          distinctUntilChanged(),
          switchMap((text) => {
            if (text.length < 2) {
              this.clearDropDown(); // clear dropdown showing list of item names.
              return [];
            }
            this.subscribeToKeyUp(text); // update data
            return [this.data];
          })
        );
      };
    
      subscribeToKeyUp(text: string) {
        this.sub.unsubscribe(); // cancel if a request is still going
        this.spinnerService.start(); // start loading spinner animation overlay on the page.
        this.sub = this.onKeyUp(text).subscribe((results) => {
          if (results?.data) this.data = results.data;
          this.spinnerService.end(); // stop loading spinner.
        });
      }
    
      onKeyUp(text: string): Observable<any> {
        return this.http.post(this.apiUrlString, { itemName: text });
      }
    

    Here's a stackblitz that simulates a 3 second delay between request and response. Clicking off between the half second debounce time and the 3 second response still allows the request to complete.

    https://stackblitz.com/edit/angular-cgyadh?file=src/app/typeahead-basic.ts

    P.S. I saw that you mentioned requests can take 30+ seconds, in that case I'd make a button to call subscribeToKeyUp(text) and remove that call from the search function. That way the massive request doesn't get triggered on every letter. Just a suggestion anyway.