RxJS Observables afford you a lot of firepower, and it would be a shame to miss out on them. A huge library of transformations and utilities are baked right in that allow you to elegantly architect complex portions of your application in a reactive fashion.
In this recipe, you'll take a naïve autocomplete form and build a robust set of features to enhance behavior and performance.
The code, links, and a live example of this are available at .
Begin with the following application:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {SearchComponent} from './search.component'; import {APIService} from './api.service'; import {HttpModule} from '@angular/http'; @NgModule({ imports: [ BrowserModule, HttpModule ], declarations: [ SearchComponent ], providers: [ APIService ], bootstrap: [ SearchComponent ] }) export class AppModule {} [app/search.component.ts] import {Component} from '@angular/core'; import {APIService} from './api.service'; @Component({ selector: 'search', template: ` <input #queryField (keyup)="search(queryField.value)"> <p *ngFor="let result of results">{{result}}</p> ` }) export class SearchComponent { results:Array<string> = []; constructor(private apiService_:APIService) {} search(query:string):void { this.apiService_ .search(query) .subscribe(result => this.results.push(result)); } } [app/api.service.ts] import {Injectable} from '@angular/core'; import {Http} from '@angular/http'; import {Observable} from 'rxjs/Rx'; @Injectable() export class APIService { constructor(private http_:Http) {} search(query:string):Observable<string> { return this.http_ .get('static/response.json') .map(r => r.json()['prefix'] + query) // Below is just a clever way of randomly // delaying the response between 0 to 1000ms .concatMap( x => Observable.of(x).delay(Math.random()*1000)); } }
Your objective is to dramatically enhance this using RxJS.
As is, this application is listening for keyup events in the search input, performing an HTTP request to a static JSON file and adding the response to a list of results.
Angular 2 has observable behavior already available to you in a number of places. One of them is inside ReactiveFormsModule
, which allows you to use an Observable that is attached to a form input. Convert this input to use FormControl
, which exposes a valueChanges
Observable:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {SearchComponent} from './search.component'; import {APIService} from './api.service'; import {HttpModule} from '@angular/http'; import {ReactiveFormsModule} from '@angular/forms'; @NgModule({ imports: [ BrowserModule, HttpModule, ReactiveFormsModule ], declarations: [ SearchComponent ], providers: [ APIService ], bootstrap: [ SearchComponent ] }) export class AppModule {} [app/search.component.ts] import {Component} from '@angular/core'; import {APIService} from './api.service'; import {FormControl} from '@angular/forms'; @Component({ selector: 'search', template: ` <input [formControl]="queryField"> <p *ngFor="let result of results">{{result}}</p> ` }) export class SearchComponent { results:Array<string> = []; queryField:FormControl = new FormControl(); constructor(private apiService_:APIService) { this.queryField.valueChanges .subscribe(query => this.apiService_ .search(query) .subscribe(result => this.results.push(result))); } }
Each time the input value changes, Angular will dutifully fire off a request and handle the response as soon as it is ready. In the case where the user is querying a very long term, such as supercalifragilisticexpialidocious, it may be necessary for you to only send off a single request once you think they're done with typing, as opposed to 34 requests, one for each time the input changes.
RxJS Observables have this built in. debounceTime(delay)
will create a new Observable
that will only pass along the latest value when there haven't been any other values for <delay>
ms. This should be added to the valueChanges
Observable since this is the source that you wish to debounce. 200 ms will be suitable for your purposes:
[app/search.component.ts] import {Component} from '@angular/core'; import {APIService} from './api.service'; import {FormControl} from '@angular/forms'; @Component({ selector: 'search', template: ` <input [formControl]="queryField"> <p *ngFor="let result of results">{{result}}</p> ` }) export class SearchComponent { results:Array<string> = []; queryField:FormControl = new FormControl(); constructor(private apiService_:APIService) { this.queryField.valueChanges .debounceTime(200) .subscribe(query => this.apiService_ .search(query) .subscribe(result => this.results.push(result))); } }
The origin of the term debounce comes from the world of circuits. Mechanical buttons or switches utilize metal contacts to open and close circuit connections. When the metal contacts are closed, they will bang together and rebound before being settled, causing bounce. This bounce is problematic in the circuit, as it will often register as a repeat toggling of the switch or button—obviously buggy behavior. The workaround for this is to find a way to ignore the expected bounce noise—debouncing! This can be accomplished by either ignoring the bounce noise or introducing a delay before reading the value, both of which can be done with hardware or software.
Since you are reading input from a textbox, it is very possible that the user will type one character, then type another character and press backspace. From the perspective of the Observable
, since it is now debounced by a delay period, it is entirely possible that the user input will be interpreted in such a way that the debounced output will emit two identical values sequentially. RxJS offers excellent protection against this, distinctUntilChanged()
, which will discard an emission that will be a duplicate of its immediate predecessor:
[app/search.component.ts] import {Component} from '@angular/core'; import {APIService} from './api.service'; import {FormControl} from '@angular/forms'; @Component({ selector: 'search', template: ` <input [formControl]="queryField"> <p *ngFor="let result of results">{{result}}</p> ` }) export class SearchComponent { results:Array<string> = []; queryField:FormControl = new FormControl(); constructor(private apiService_:APIService) { this.queryField.valueChanges .debounceTime(200) .distinctUntilChanged() .subscribe(query => this.apiService_ .search(query) .subscribe(result => this.results.push(result))); } }
You have chained quite a few RxJS methods up to this point, and seeing nested subscribe()
invocations might feel a bit funny to you. It should make sense since the valueChanges Observable
handler is invoking a service method, which returns a separate Observable
. In TypeScript, this is effectively represented as Observable<Observable<string>>
. Gross!
Since you only really care about the emitted strings coming from the service method, it would be much easier to just combine all the emitted strings coming out of each returned Observable
into a single Observable
. Fortunately, RxJS makes this easy with flatMap
, which flattens all the emissions from the inner Observables into a single outer Observable
. In TypeScript, using flatMap
would convert this into Observable<string>
, which is exactly what you need:
[app/search.component.ts] import {Component} from '@angular/core'; import {APIService} from './api.service'; import {FormControl} from '@angular/forms'; @Component({ selector: 'search', template: ` <input [formControl]="queryField"> <p *ngFor="let result of results">{{result}}</p> ` }) export class SearchComponent { results:Array<string> = []; queryField:FormControl = new FormControl(); constructor(private apiService_:APIService) { this.queryField.valueChanges .debounceTime(200) .distinctUntilChanged() .flatMap(query => this.apiService_.search(query)) .subscribe(result => this.results.push(result)); } }
When testing input now, you will surely notice that the delay intentionally introduced inside the API service will cause the responses to be returned out of order. This is a pretty effective simulation of network latency, so you'll need a good way of handling this.
Ideally, you would like to be able to throw out Observables that are in flight once you have a more recent query to execute. For example, consider that you've typed g
and then o
. Now once the second query for go
is returned and if the first query for g
hasn't returned yet, you'd like to just throw it out and forget about it since the response is now irrelevant.
RxJS also makes this very easy with switchMap
. This does the same things as flatMap
, but it will unsubscribe from any in-flight Observables
that have not emitted any values yet:
[app/search.component.ts] import {Component} from '@angular/core'; import {APIService} from './api.service'; import {FormControl} from '@angular/forms'; @Component({ selector: 'search', template: ` <input [formControl]="queryField"> <p *ngFor="let result of results">{{result}}</p> ` }) export class SearchComponent { results:Array<string> = []; queryField:FormControl = new FormControl(); constructor(private apiService_:APIService) { this.queryField.valueChanges .debounceTime(200) .distinctUntilChanged() .switchMap(query => this.apiService_.search(query)) .subscribe(result => this.results.push(result)); } }
Your AutoComplete input should now be debounced and it should ignore redundant requests and return in-order results.
There are a lot of moving pieces going on in this recipe, but the core theme remains the same: RxJS Observables expose many methods that can pipe the output from one observable into an entirely different observable. It can also combine multiple observables into a single observable, as well as introduce state-dependent operations into a stream of the input. At the end of this recipe, the power of reactive programming should be obvious.