I'm currently building a type ahead search. I'm trying to adapt it from the example here https://www.learnrxjs.io/learn-rxjs/recipes/type-ahead but I need to make an http call on keystroke. I'm testing it against timezones from my api. It works, but it seems to be compounding api calls every time I input a keyup
.
If I type 'us' it will return results and make two identical api calls. If I add 'a' to make 'usa' it will then make 3 api calls. If I backspace a letter it makes 4, so on and so forth.
From my understanding switchMap
is supposed to cancel calls that are going as newer ones come in, but it doesn't seem to do that with my implementation. I cannot for the life of me figure out why it's doing this when I keep going over the example. I tried to use .share()
to attempt to consolidate streams but it didn't seem to help.
Search service:
results: any;
search(table: string, page: any, elementId: string) {
const searchInput = document.getElementById(elementId);
const keyup$ = fromEvent(searchInput, 'keyup');
let searchQuery;
keyup$.pipe(
debounceTime(500),
map((e: any) => searchQuery = e.target.value),
distinctUntilChanged(),
switchMap(value => this.apiDataService.getSearchData(this.apiDataService.apiBase + `/search?table=${table}&query=${searchQuery}&page=${page}`))
)
.subscribe(res => {
this.results = res.body.data;
});
}
apiData service:
getSearchData(dataUrl: string): Observable<any> {
return this.http.get<[]>(dataUrl, this.httpOptions);
}
html component:
<input id="timezoneSearch" placeholder="Search" [(ngModel)]="search.params" (keyup)="search.search('timezone', '1', 'timezoneSearch')" mdbInput type="text">
<div class="dropdown-item" *ngFor="let option of search.results"
(click)="accordion.selectOption(obj.currentObject, option.uuid, 'admin', 'ktimezone')">
{{option.name}}
</div>
You can just subscribe to the input events of the input element:
<input #timezoneSearch placeholder="Search" [(ngModel)]="search.params" mdbInput type="text">
@ViewChild('timezoneSearch') timezoneSearch: ElementRef;
ngAfterViewInit() {
fromEvent(this.timezoneSearch.nativeElement, 'input').pipe(
debounceTime(500),
map((e: InputEvent) => e.target.value),
switchMap(value => this.apiDataService.getSearchData(this.apiDataService.apiBase + `/search?table=${'timezone'}&query=${value}&page=${1}`))
).subscribe(
res => this.results = res.body.data
);
}
Furthermore, you don't need to put the results in a field, but instead directly use the Observable in the template:
<div *ngFor="let option of results$ | async"
...
</div>
ngAfterViewInit() {
this.results$ = fromEvent(this.timezoneSearch.nativeElement, 'input').pipe(
debounceTime(500),
map((e: InputEvent) => e.target.value),
switchMap(value => this.apiDataService.getSearchData(this.apiDataService.apiBase + `/search?table=${'timezone'}&query=${value}&page=${1}`)),
map(res => res.body.data)
);