Florent BRASSART
Florent BRASSART

Reputation: 33

ng-bootstrap typeahead display spinner when performing search

On my project, ng-bootstrap is used to create a typeahead in order to search for users. A spinner is displayed when the search is still going. The typeahead performs a search when:

  1. The user enters some text in the field
  2. The user clicks on the field
  3. The field gains focus

Everything is almost perfect except for one detail that I can't manage to fix. When entering text, if the search is still going, typing additionnal text makes the spinner disappear even though the new search will be performing.

This is due to the statement in the finalize of the first subscription being run after the first tap statement of the second subscription.

All my code is inspired by: https://ng-bootstrap.github.io/#/components/typeahead/examples

<input type="text"
       class="form-control"
       [class.is-invalid]="searchFailed"
       [formControl]="formControl"
       (selectItem)="onSelectMember($event)"
       [ngbTypeahead]="search"
       [resultFormatter]="formatMember"
       (focus)="focus$.next($any($event).target.value)"
       (click)="click$.next($any($event).target.value)"
       #instance="ngbTypeahead"
       [placeholder]="'MEMBERS.TYPEAHEAD.PLACEHOLDER' | translate" />
<span *ngIf="searching"><i class="fas fa-spinner fa-spin"></i> {{'MEMBERS.TYPEAHEAD.SEARCHING' | translate}}</span>
<div class="invalid-feedback" *ngIf="searchFailed">{{'MEMBERS.TYPEAHEAD.NORESULTS' | translate}}</div>
import { Component, Input, ViewChild } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { merge, Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, finalize, map, switchMap, tap } from 'rxjs/operators';
import { MemberService } from '../../@core/data';
import { Member } from '../../@core/models';

@Component({
  selector: 'de-member-type-ahead',
  templateUrl: './member-type-ahead.component.html',
})
export class MemberTypeAheadComponent {
  @Input() formControl: AbstractControl;
  @Input() searchRegisteredMembers: boolean = true;
  @Input() maxResults: number = 10;

  @ViewChild('instance', {static: true}) instance: NgbTypeahead;
  focus$ = new Subject<string>();
  click$ = new Subject<string>();

  searching = false;
  searchFailed = false;

  constructor(private memberService: MemberService) {
  }

  formatMember = (result: Member) => {
    return result.getFullName() + ' (' + result.getEmail() + ')';
  }

  onSelectMember(event) {
    event.preventDefault();
    this.formControl.patchValue(event.item);
  }

  search = (text$: Observable<string>) =>
    this.getSearchObservable(text$).pipe(
      tap(() => this.searching = true),
      switchMap(term =>
        this.memberService.searchByNameAndEmail(term, this.searchRegisteredMembers).pipe(
          tap(() => this.searchFailed = false),
          map(result => result.slice(0, this.maxResults)),
          catchError(() => {
            this.searchFailed = true;
            return of([]);
          }),
          finalize(() => this.searching = false)),
      ),
      tap(() => this.searching = false),
    )


  private getSearchObservable = (text$: Observable<string>) => {
    const debouncedText$ = text$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
    );

    const clicksWithClosedPopup$ = this.click$.pipe(
      filter(() => !this.instance.isPopupOpen()),
      filter(() => !this.searching),
    );

    const inputFocus$ = this.focus$.pipe(
      filter(() => !this.searching),
    );

    return merge(debouncedText$, clicksWithClosedPopup$, inputFocus$);
  }
}

I've also reproduced this behaviour by adapting the official Wikipedia example: https://stackblitz.com/edit/angular-rwxzjr?file=src%2Fapp%2Ftypeahead-http.ts (This works best by disabling cache and throttling your connection rate)

Do you have any idea how I could fix this?

Upvotes: 1

Views: 1464

Answers (1)

AliF50
AliF50

Reputation: 18879

This is the code that got modified:

  search = (text$: Observable<string>) =>
    this.getSearchObservable(text$).pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(term => {
        this.searching = true;
        return this._service.search(term).pipe(
          tap(() => { 
            this.searching = false;
            this.searchFailed = false; 
          }),
          catchError(() => {
            this.searching = false;
            this.searchFailed = true;
            return of([]);
          }))
      }),
    )
  
  private getSearchObservable = (text$: Observable<string>) => {
    const debouncedText$ = text$.pipe(
      // debounceTime(300),
      distinctUntilChanged(),
    );

    const clicksWithClosedPopup$ = this.click$.pipe(
      filter(() => !this.instance.isPopupOpen()),
      filter(() => !this.searching),
    );

    const inputFocus$ = this.focus$.pipe(
      filter(() => !this.searching),
    );

    return merge(debouncedText$, clicksWithClosedPopup$, inputFocus$);
  }

Upvotes: 0

Related Questions