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)
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.
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.