Книга: Angular 2 Cookbook
Назад: Using QueryLists and Observables to follow changes in ViewChildren
Дальше: 6. The Component Router

Building a fully featured AutoComplete with Observables

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.

Note

The code, links, and a live example of this are available at .

Getting ready

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.

How to do it...

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.

Using the FormControl valueChanges Observable

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)));     }   }   

Debouncing the input

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)));     }   }   

Note

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.

Ignoring serial duplicates

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)));     }   }   

Flattening Observables

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));     }   }   

Handling unordered responses

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.

How it works...

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.

See also

  • Basic Utilization of Observables with HTTP demonstrates the basics of how to use an observable interface
  • Implementing a Publish-Subscribe model using Subjects shows you how to configure input and output for RxJS Observables
  • Creating an Observable authentication service using BehaviorSubjects instructs you on how to reactively manage the state in your application
  • Building a generalized Publish-Subscribe service to replace $broadcast, $emit, and $on assembles a robust PubSub model for connecting application components with channels
Назад: Using QueryLists and Observables to follow changes in ViewChildren
Дальше: 6. The Component Router

thank you
Flame
cant read the code since it is all on a single line. Also this comments section is russian
Rakuneque
DATA COLLECTION AND ANALYSIS Two reviewers extracted data and assessed methodological quality independently lasix torsemide conversion Many others were in that space already