Nite
Nite

Reputation: 373

Angular - improve my autocomplete with switchMap

At the moment I have the following setup :

In my component :

/**/
onCitySearch() {
    this.citiesService.getCities(this.citySearchControl.value);
}

In my service :

    /**/
    getCities(term: string) {
        if (term.length < 3 || term === this.lastSearchTerm || this.autocompleteAjaxLock) {
        return;
    }

    this.autocompleteAjaxLock = true;
    this.httpClient
        .get<City[]>(environment.api_url + '/api/city/autocomplete?term=' + term)
        .subscribe(
            (cities) => {
                this.lastSearchTerm = term;
                this.cities = cities;
                this.citiesUpdated.next([...this.cities]);
                this.autocompleteAjaxLock = false;
            },
            error => this.autocompleteAjaxLock = false
        );
}

My component is subscribed to the service cities and automatically gets notified when the search is updated. Now, with the lock system I setup, if the user types quickly my search doesnt get updated. I read around about switchMap, that should be appropriate for my use case. Where and how should I implement it ? In my component or in the service function ?

Upvotes: 0

Views: 737

Answers (2)

Daniel Gimenez
Daniel Gimenez

Reputation: 20454

A few general things:

  1. You're right that switchMap would be a good choice as it has a cancelling effect on previous emissions.
  2. It's always good to introduce a delay with typed user input in the form of debounceTime.
  3. Try to get out of the habit of assigning variables outside of your stream with tap or subscribe, especially when there is likely a way to assign a variable directly, such as with an async pipe.

Instead of switchMap, the solution below uses switchScan to maintain a typeahead state.

  • First the user input is debounced.
  • It is then cleansed with map so only relevant data is returned.
  • distinctUntilChanged is used to prevent repeats of cleansed results.
  • It is then filtered so that it is at least 3 characters long.
  • Now the magic happens with switchScan:
  • The second parameter sets the initial state.
  • If the currentTerm exists in terms then the exiting terms and new currentTerm are returned as part of a new state.
  • Otherwise, the term is searched for with the api call, added to terms and a new state with currentTerm is returned with the updated terms.
  • Finally, the result is transformed with map as just an array of terms corresponding to currentTerm.
interface TypeaheadState = {
  terms: { [term: string]: City[] };
  currentTerm: string;
}
readonly searchTermSubject = new Subject<string>();

readonly typeAhead$ = searchTermSubject.pipe(
  debounceTime(100), 
  map(x => x.trim().toLowerCase()),
  distinctUntilChanged(),
  filter(x => x.length >= 3),
  switchScan((state, currentTerm) =>
      state[currentTerm] 
        ? of({ currentTerm, terms: state.terms })
        : this.httpClient
          .get<City[]>(environment.api_url + '/api/city/autocomplete?term=' + term)
          .pipe(map(res => ({ currentTerm, terms: { ...state.terms, [currentTerm]: res }))),
      { currentTerm: '', terms: { '': [] } } as TypeaheadState 
    )
  ),
  map(state => state.terms[state.currentTerm])
);

If switchMap was used then the state would've had to exist outside of the operator. By using a x-scan operator you're able to keep everything local to the oeprator, resulting in a cleaner solution. The switchScan documentation is scant, so check the mergeScan documentation instead, as it is pretty much the same except for the switching effect.

Upvotes: 1

Youp Bernoulli
Youp Bernoulli

Reputation: 5635

Why is the autoCompleteAjax lock? Are you "afraid" of too many Http requests? Just make sure you store the subscriptions created with the this.httpClient.get(...) invocation. Then upon every subsequent call to getCities loop through the cache of subscriptions and unsubscribe them (this will cancel the Http request and/or response).

As an alternative (addition) you might want to implement a short delay upfront (before invoking getCities) and when the next key stroke comes in the queued requests have to be cancelled (which will not even reach the GetCities method). This can be done very nicely with the well praised RxJs library (that comes alongside with every modern Angular application setup).

Have a look at this Cancel a delay if same observable emits for examples of RxJs switchMap and debounceTime method/operator.

Upvotes: 1

Related Questions