There are plenty of options available in Angular world to implement TypeAhead feature for your project, such as;
- Angular Material Autocomplete (https://material.angular.io/components/autocomplete/examples)
- NG Bootstrap Typeahead (https://ng-bootstrap.github.io/#/components/typeahead/examples)
- Prime-Ng (https://www.primefaces.org/primeng/#/autocomplete)
Although, there is no need to reinvent the wheel here, still a simple analysis on how a type ahead works in the above libraries will help us explore the power of RxJS.
Please Note: This tutorial is just an analysis and basic, there are a lot more components / services involved while creating a fully featured Autocomplete / Type a head.
Nested Subscription – Anti Pattern
A simple demo with a button click as an Observable and in turn triggers another Observable with a timer. Our usual method of thinking is to have a Subscriber inside another Subscriber.
// @ViewChild('btn') btn: ElementRef; obs2: Observable<any>; numClicks: number = 0; // const obs1 = fromEvent(this.btn.nativeElement, 'click'); obs1.subscribe((event) => { // First Subscriber this.numClicks++; return this.obs2.subscribe((value) => // Second Subscriber console.log('Final Value ->', value, this.numClicks) ); }); //
The problem with this approach is, the timer values and number of clicks are never in sync and out of place.
Meaning, the inner Observables keep running concurrently and never cancel out on new clicks. Which could potentially or possibly cause memory leaks.
The switchMap implementation
A more frequently used operator in RxJS is switchMap. As the name suggests a switchMap switches to a new observable there by canceling all previous observables, commonly called as switching to new Observable.
// const sObs1 = fromEvent(this.btnSwitchMap.nativeElement, 'click'); this.subscription1$ = sObs1 .pipe( switchMap((event) => { this.numClicks++; return this.obs2; }) ) .subscribe((value) => console.log('Switch Map Final Value -> ', value, this.numClicks) ); // Output. Interval Value, Number of Clicks // Switch Map Final Value -> 0 1 // Switch Map Final Value -> 1 1 // Button Clicked // Switch Map Final Value -> 0 2 // Switch Map Final Value -> 1 2 // Switch Map Final Value -> 2 2 // Switch Map Final Value -> 3 2 // Button Clicked // Switch Map Final Value -> 0 3 // Switch Map Final Value -> 1 3 // Switch Map Final Value -> 2 3 // Execution continues ... // //
If you notice the output, the interval timer kicks in every second after a button is clicked. When the button is clicked again, the timer starts from zero thereby starting a new Observable and canceling all previous subscriptions.
By doing this, we can eliminate nested subscriptions which will create unwanted results and memory leaks.
switchMap for Typeahead / Autocomplete
// // Component class file // @ViewChild('autoCompleteInput') autoCompleteInput: ElementRef; numClicks: number = 0; typeSubscription$; hackersList: { hackerName: string; statement: string; alias: string; id: string; }[] = []; ngAfterViewInit() { // Autocomplete / Typeahead const typeAheadEvent = fromEvent( this.autoCompleteInput.nativeElement, 'keyup' ); this.typeSubscription$ = typeAheadEvent .pipe( map((e: any) => { console.log('Map -> ', e.target.value); return e.target.value; }), switchMap((typedValue: string) => { console.log('switchMap typedValue ->', typedValue); if (typedValue.length > 2) { return this.data .getHackersByName(typedValue) .pipe(tap((t) => console.log('Tapped -> ', t))); } return of(null); }) ) .subscribe({ next: (result: any) => { console.log('Final Value -> ', result); this.hackersList = result; console.log('hackersList', this.hackersList); }, error: (err) => console.error(err), complete: () => console.log('Typing completed !!!'), }); } // Output - Type "dig" // Map -> d // switchMap typedValue -> d // Final Value -> null // hackersList null // Map -> di // switchMap typedValue -> di // Final Value -> null // hackersList null // Map -> dig // switchMap typedValue -> dig // getHackersByName dig // Final Value -> (3) [{…}, {…}, {…}] // hackersList (3) [{…}, {…}, {…}] //
Once you start typing in “dig”, the map operator in pipe returns only the typed in value instead of the entire element ref object.
Immediately we can see a problem here. As soon as we start typing in the API request is made for each typed in value which will impact our performance.
debounceTime(milliseconds) operator will fix that issue, since it creates a delay to next operator momentarily. So our modified code will look like this.
// this.typeSubscription$ = typeAheadEvent .pipe( debounceTime(500), // waits for 500ms before stepping to next step in pipeline map((e: any) => { console.log('Map -> ', e.target.value); return e.target.value; }), // distinctUntilChanged(), switchMap((typedValue: string) => { console.log('switchMap typedValue ->', typedValue); if (typedValue.length > 2) { return this.data .getHackersByName(typedValue) // .pipe(tap((t) => console.log('Tapped -> ', t))); } return of(null); }) ) .subscribe({ next: (result: any) => { console.log('Final Value -> ', result); this.hackersList = result; console.log('hackersList', this.hackersList); }, error: (err) => console.error(err), complete: () => console.log('Typing completed !!!'), }); // Output // Map -> dig // switchMap typedValue -> dig // getHackersByName dig // Final Value -> (3) [{…}, {…}, {…}] // hackersList (3) [{…}, {…}, {…}]
Now, every typing ins will wait for 500 milliseconds before emitting to the next operator.
In Conclusion
Creating a fully functional Autocomplete or Typeahead requires a whole lot of other functionalities like dropdown of the searched list, and selecting an item from that list. What we have seen here is how the switchMap was fit in this scenario to understand better about that operator. We learnt about canceling effects and avoiding nested Subscriptions.