From 7dbbc0c3e34cfdf977c14f82ddadf7c0ceea2027 Mon Sep 17 00:00:00 2001 From: Torgeir Helgevold Date: Mon, 23 May 2016 22:24:15 -0400 Subject: [PATCH 1/2] docs(add observables to TOH http) e2e prose tweaks d d d section lint jade space update remove r test tweak tweak --- public/docs/_examples/toh-6/e2e-spec.ts | 23 +++- .../_examples/toh-6/ts/app/app.component.ts | 3 + .../toh-6/ts/app/dashboard.component.html | 2 + .../toh-6/ts/app/dashboard.component.ts | 4 +- .../toh-6/ts/app/hero-search.component.html | 10 ++ .../toh-6/ts/app/hero-search.component.ts | 37 ++++++ .../toh-6/ts/app/hero-search.service.ts | 17 +++ .../_examples/toh-6/ts/app/rxjs-operators.ts | 8 ++ public/docs/_examples/toh-6/ts/sample.css | 17 +++ public/docs/ts/latest/tutorial/toh-pt6.jade | 112 +++++++++++++++++- .../images/devguide/toh/toh-hero-search.png | Bin 0 -> 10942 bytes 11 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.component.html create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.component.ts create mode 100644 public/docs/_examples/toh-6/ts/app/hero-search.service.ts create mode 100644 public/docs/_examples/toh-6/ts/app/rxjs-operators.ts create mode 100644 public/resources/images/devguide/toh/toh-hero-search.png diff --git a/public/docs/_examples/toh-6/e2e-spec.ts b/public/docs/_examples/toh-6/e2e-spec.ts index 96277a910a..e2ab2602dc 100644 --- a/public/docs/_examples/toh-6/e2e-spec.ts +++ b/public/docs/_examples/toh-6/e2e-spec.ts @@ -23,10 +23,31 @@ describe('TOH Http Chapter', function () { addButton: element.all(by.buttonText('Add New Hero')).get(0), - heroDetail: element(by.css('my-app my-hero-detail')) + heroDetail: element(by.css('my-app my-hero-detail')), + + searchBox: element(by.css('#search-box')), + searchResults: element.all(by.css('.search-result')) }; } + it('should search for hero and navigate to details view', function() { + let page = getPageStruct(); + + return sendKeys(page.searchBox, 'Magneta').then(function () { + expect(page.searchResults.count()).toBe(1); + let hero = page.searchResults.get(0); + return hero.click(); + }) + .then(function() { + browser.waitForAngular(); + let inputEle = page.heroDetail.element(by.css('input')); + return inputEle.getAttribute('value'); + }) + .then(function(value) { + expect(value).toBe('Magneta'); + }); + }); + it('should be able to add a hero from the "Heroes" view', function(){ let page = getPageStruct(); let heroCount: webdriver.promise.Promise; diff --git a/public/docs/_examples/toh-6/ts/app/app.component.ts b/public/docs/_examples/toh-6/ts/app/app.component.ts index 2a1ff50ba3..bf3dd76424 100644 --- a/public/docs/_examples/toh-6/ts/app/app.component.ts +++ b/public/docs/_examples/toh-6/ts/app/app.component.ts @@ -4,6 +4,9 @@ import { Component } from '@angular/core'; import { ROUTER_DIRECTIVES } from '@angular/router'; import { HeroService } from './hero.service'; +// #docregion rxjs-operators +import './rxjs-operators'; +// #enddocregion rxjs-operators @Component({ selector: 'my-app', diff --git a/public/docs/_examples/toh-6/ts/app/dashboard.component.html b/public/docs/_examples/toh-6/ts/app/dashboard.component.html index 028eab6eb3..e22a2a5ebb 100644 --- a/public/docs/_examples/toh-6/ts/app/dashboard.component.html +++ b/public/docs/_examples/toh-6/ts/app/dashboard.component.html @@ -9,3 +9,5 @@

{{hero.name}}

+ + diff --git a/public/docs/_examples/toh-6/ts/app/dashboard.component.ts b/public/docs/_examples/toh-6/ts/app/dashboard.component.ts index 08ffecc0ea..f7b4100cee 100644 --- a/public/docs/_examples/toh-6/ts/app/dashboard.component.ts +++ b/public/docs/_examples/toh-6/ts/app/dashboard.component.ts @@ -5,11 +5,13 @@ import { Router } from '@angular/router'; import { Hero } from './hero'; import { HeroService } from './hero.service'; +import { HeroSearchComponent } from './hero-search.component'; @Component({ selector: 'my-dashboard', templateUrl: 'app/dashboard.component.html', - styleUrls: ['app/dashboard.component.css'] + styleUrls: ['app/dashboard.component.css'], + directives: [HeroSearchComponent] }) export class DashboardComponent implements OnInit { diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.html b/public/docs/_examples/toh-6/ts/app/hero-search.component.html new file mode 100644 index 0000000000..3a47ba3caf --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.html @@ -0,0 +1,10 @@ + +
+

Hero Search

+ +
+
+ {{hero.name}} +
+
+
\ No newline at end of file diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts new file mode 100644 index 0000000000..2faafb100a --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts @@ -0,0 +1,37 @@ +// #docregion +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +import { HeroSearchService } from './hero-search.service'; +import { Hero } from './hero'; + +@Component({ + selector: 'hero-search', + templateUrl: 'app/hero-search.component.html', + providers: [HeroSearchService] +}) +export class HeroSearchComponent implements OnInit { + search = new Subject(); + heroes: Hero[] = []; + + constructor(private _heroSearchService: HeroSearchService, private _router: Router) {} + + gotoDetail(hero: Hero) { + let link = ['/detail', hero.id]; + this._router.navigate(link); + } + + // #docregion search + ngOnInit() { + this.search.asObservable() + .debounceTime(300) + .distinctUntilChanged() + .switchMap(term => term ? this._heroSearchService.search(term) + : Observable.of([])) + .subscribe(result => this.heroes = result, + error => console.log(error)); + } + // #enddocregion search +} diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts new file mode 100644 index 0000000000..20e88482b9 --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts @@ -0,0 +1,17 @@ +// #docregion +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; + +@Injectable() +export class HeroSearchService { + + constructor(private _http: Http) {} + + // #docregion observable-search + search(term: string) { + return this._http + .get(`app/heroes/?name=${term}+`) + .map((r: Response) => r.json().data); + } + // #enddocregion observable-search +} diff --git a/public/docs/_examples/toh-6/ts/app/rxjs-operators.ts b/public/docs/_examples/toh-6/ts/app/rxjs-operators.ts new file mode 100644 index 0000000000..2a02941ee0 --- /dev/null +++ b/public/docs/_examples/toh-6/ts/app/rxjs-operators.ts @@ -0,0 +1,8 @@ +// #docregion +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/observable/of'; diff --git a/public/docs/_examples/toh-6/ts/sample.css b/public/docs/_examples/toh-6/ts/sample.css index 0c99008d2d..a5ac5b4d70 100644 --- a/public/docs/_examples/toh-6/ts/sample.css +++ b/public/docs/_examples/toh-6/ts/sample.css @@ -5,3 +5,20 @@ button.delete-button{ background-color: gray !important; color:white; } + +.search-result{ + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + width:195px; + height: 20px; + padding: 5px; + background-color: white; + cursor: pointer; +} + +#search-box{ + width: 200px; + height: 20px; +} + diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index 5f65c5a421..fd5d2d53c0 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -358,6 +358,99 @@ block review figure.image-display img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editting w/ HTTP") +:marked + ## Observables + + In this section we discuss `Observables` as an alternative to promises when processing http calls. + + `Http` calls return RxJS observables by default, but so far we've been hiding that by converting the observable to a promise by calling `toPromise`. + + Observables and promises are both great for processing http calls, but as we will see, the observables api is much richer. + + `RxJs` offers a wide variety of operators that we can use to manage event flows. + + In this section we will discuss how to use some of these operators to build a type-to-search filter where we can search for heroes by name. + + We start by creating `HeroSearchService`, a simple service for sending search queries to our api. + ++makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".") + +:marked + The http call in `HeroSearchService` is not that different from our previous http calls, but we no longer call `toPromise`. This means we will return an observable instead of a promise. + + Now, let's implement our search component `HeroSearchComponent`. + ++makeTabs( + `toh-6/ts/app/hero-search.component.ts, + toh-6/ts/app/hero-search.component.html`, + null, + `hero-search.component.ts, + hero-search.component.html` +) + +:marked + The `HeroSearchComponent` UI is simple - just a textbox and a list of matching search results. + + To keep track of text changes in the search box we are using an RxJs `Subject`. + + Observables are great for managing event streams. In our example we will actually be dealing with two different types of event streams: + + 1) Key events from typing into the search textbox + + 2) Results from http calls based on values entered in the search textbox + + Although we are dealing with two different streams, we can use `RxJs` operators to combine them into a single stream. + + Converging on a single stream is the end game, but let's start by going through the different parts of the observable chain. + ++makeExample('toh-6/ts/app/hero-search.component.ts', 'search', 'app/hero-search.component.ts')(format=".") + +:marked + ### asObservable + We start the observable chain by calling `asObservable` on our `Subject` to return an observable that represents text changes as we are typing into the search box. + + ### debounceTime(300) + By specifying a debounce time of 300 ms we are telling `RxJs` that we only want to be notified of key events after intervals of 300 ms. This is a performance measure meant to prevent us from hitting the api too often with partial search queries. + + ### distinctUntilChanged + `distinctUntilChanged` tells `RxJs` to only react if the value of the textbox actually changed. + + ### switchMap + `switchMap` is where observables from key events and observables from http calls converge on a single observable. + + Every qualifying key event will trigger an http call, so we may get into a situation where we have multiple http calls in flight. + + Normally this could lead to results being returned out of order, but `switchMap` has built in support for ensuring that only the most recent http call will be processed. Results from prior calls will be discarded. + + We short circuit the http call and return an observable containing an empty array if the search box is empty. + + ### subscribe + `subscribe` is similar to `then` in the promise world. + + The first callback is the success callback where we grab the result of the search and assign it to our `heroes` array. + + The second callback is the error callback. This callback will execute if there is an error - either from the http call or in our processing of key events. + + In our current implementation we are just logging the error to the console, but a real life application should do better. + + ### Import operators + The `RxJs` operators are not included by default, so we have to include them using `import` statements. + + We have combined all operator imports in a single file. + ++makeExample('toh-6/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".") + +:marked + We can then load all the operators in a single operation by importing `rxjs-operators` in `AppComponent` + ++makeExample('toh-6/ts/app/app.component.ts', 'rxjs-operators', 'app/app/app.component.ts')(format=".") + +:marked + Here is the final search component. + +figure.image-display + img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") + .l-main-section :marked ## Application structure and code @@ -381,6 +474,10 @@ block filetree .file hero-detail.component.css .file hero-detail.component.html .file hero-detail.component.ts + .file hero-search.component.html + .file hero-search.component.ts + .file hero-search.service.ts + .file rxjs-operators.ts .file hero.service.ts .file heroes.component.css .file heroes.component.html @@ -407,7 +504,8 @@ block filetree - We extended HeroService to support post, put and delete calls. - We updated our components to allow adding, editing and deleting of heroes. - We configured an in-memory web API. - + - We learned how to use Observables. + Below is a summary of the files we changed and added. block file-summary @@ -430,3 +528,15 @@ block file-summary in-memory-data.service.ts, sample.css` ) + + +makeTabs( + `toh-6/ts/app/hero-search.service.ts, + toh-6/ts/app/hero-search.component.ts, + toh-6/ts/app/hero-search.component.html, + toh-6/ts/app/rxjs-operators.ts`, + null, + `hero-search.service.ts, + hero-search.component.ts, + hero-search.service.html, + rxjs-operators.ts` +) \ No newline at end of file diff --git a/public/resources/images/devguide/toh/toh-hero-search.png b/public/resources/images/devguide/toh/toh-hero-search.png new file mode 100644 index 0000000000000000000000000000000000000000..f09fd45d7effd039bec1813174d823fd7a1fd204 GIT binary patch literal 10942 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 literal 0 HcmV?d00001 From d623425e5820cad6b7ef81f79a531e79ff10f7ac Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Mon, 18 Jul 2016 20:59:02 -0700 Subject: [PATCH 2/2] docs(toh-pt6): add observables - ward's changes --- .../_examples/toh-6/ts/app/app.component.ts | 6 +- .../toh-6/ts/app/hero-search.component.html | 9 +- .../toh-6/ts/app/hero-search.component.ts | 42 +++-- .../toh-6/ts/app/hero-search.service.ts | 8 +- .../_examples/toh-6/ts/app/hero.service.ts | 4 +- .../{rxjs-operators.ts => rxjs-extensions.ts} | 15 +- public/docs/ts/latest/tutorial/toh-pt6.jade | 168 +++++++++++------- 7 files changed, 162 insertions(+), 90 deletions(-) rename public/docs/_examples/toh-6/ts/app/{rxjs-operators.ts => rxjs-extensions.ts} (68%) diff --git a/public/docs/_examples/toh-6/ts/app/app.component.ts b/public/docs/_examples/toh-6/ts/app/app.component.ts index bf3dd76424..d49c87ccbf 100644 --- a/public/docs/_examples/toh-6/ts/app/app.component.ts +++ b/public/docs/_examples/toh-6/ts/app/app.component.ts @@ -4,9 +4,9 @@ import { Component } from '@angular/core'; import { ROUTER_DIRECTIVES } from '@angular/router'; import { HeroService } from './hero.service'; -// #docregion rxjs-operators -import './rxjs-operators'; -// #enddocregion rxjs-operators +// #docregion rxjs-extensions +import './rxjs-extensions'; +// #enddocregion rxjs-extensions @Component({ selector: 'my-app', diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.html b/public/docs/_examples/toh-6/ts/app/hero-search.component.html index 3a47ba3caf..47c853746b 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.component.html +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.html @@ -1,10 +1,11 @@
-

Hero Search

- +

Hero Search

+
-
+
{{hero.name}}
-
\ No newline at end of file +
diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts index 2faafb100a..2b4d155046 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts @@ -1,3 +1,4 @@ +// #docplaster // #docregion import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @@ -13,25 +14,40 @@ import { Hero } from './hero'; providers: [HeroSearchService] }) export class HeroSearchComponent implements OnInit { + // #docregion subject search = new Subject(); - heroes: Hero[] = []; + // #enddocregion subject + // #docregion search + heroes: Observable; + // #enddocregion search - constructor(private _heroSearchService: HeroSearchService, private _router: Router) {} + constructor( + private heroSearchService: HeroSearchService, + private router: Router) {} - gotoDetail(hero: Hero) { - let link = ['/detail', hero.id]; - this._router.navigate(link); - } // #docregion search ngOnInit() { - this.search.asObservable() - .debounceTime(300) - .distinctUntilChanged() - .switchMap(term => term ? this._heroSearchService.search(term) - : Observable.of([])) - .subscribe(result => this.heroes = result, - error => console.log(error)); + this.heroes = this.search + .asObservable() // "cast" as Observable + .debounceTime(300) // wait for 300ms pause in events + .distinctUntilChanged() // ignore if next search term is same as previous + .switchMap(term => term // switch to new observable each time + // return the http search observable + ? this.heroSearchService.search(term) + // or the observable of empty heroes if no search term + : Observable.of([])) + + .catch(error => { + // Todo: real error handling + console.log(error); + return Observable.throw(error); + }); } // #enddocregion search + + gotoDetail(hero: Hero) { + let link = ['/detail', hero.id]; + this.router.navigate(link); + } } diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts index 20e88482b9..42018e3526 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.service.ts +++ b/public/docs/_examples/toh-6/ts/app/hero-search.service.ts @@ -2,16 +2,18 @@ import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; +import { Hero } from './hero'; + @Injectable() export class HeroSearchService { - constructor(private _http: Http) {} + constructor(private http: Http) {} // #docregion observable-search search(term: string) { - return this._http + return this.http .get(`app/heroes/?name=${term}+`) - .map((r: Response) => r.json().data); + .map((r: Response) => r.json().data as Hero[]); } // #enddocregion observable-search } diff --git a/public/docs/_examples/toh-6/ts/app/hero.service.ts b/public/docs/_examples/toh-6/ts/app/hero.service.ts index 8abbcc2778..04012768be 100644 --- a/public/docs/_examples/toh-6/ts/app/hero.service.ts +++ b/public/docs/_examples/toh-6/ts/app/hero.service.ts @@ -17,13 +17,13 @@ export class HeroService { constructor(private http: Http) { } - getHeroes(): Promise { + getHeroes() { return this.http.get(this.heroesUrl) // #docregion to-promise .toPromise() // #enddocregion to-promise // #docregion to-data - .then(response => response.json().data) + .then(response => response.json().data as Hero[]) // #enddocregion to-data // #docregion catch .catch(this.handleError); diff --git a/public/docs/_examples/toh-6/ts/app/rxjs-operators.ts b/public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts similarity index 68% rename from public/docs/_examples/toh-6/ts/app/rxjs-operators.ts rename to public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts index 2a02941ee0..a0facfe03e 100644 --- a/public/docs/_examples/toh-6/ts/app/rxjs-operators.ts +++ b/public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts @@ -1,8 +1,13 @@ // #docregion +// Observable class extensions +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/throw'; + +// Observable operators +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap'; -import 'rxjs/add/operator/filter'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/distinctUntilChanged'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/observable/of'; diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index fd5d2d53c0..0b00284f5c 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -131,9 +131,10 @@ block get-heroes-details :marked The Angular `http.get` returns an RxJS `Observable`. *Observables* are a powerful way to manage asynchronous data flows. - We'll learn about `Observables` *later*. + We'll learn about [Observables](#observables) later in this chapter. - For *now* we get back on familiar ground by immediately converting that `Observable` to a `Promise` using the `toPromise` operator. + For *now* we get back on familiar ground by immediately by + converting that `Observable` to a `Promise` using the `toPromise` operator. +makeExample('toh-6/ts/app/hero.service.ts', 'to-promise')(format=".") :marked Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ... not out of the box. @@ -361,92 +362,139 @@ block review :marked ## Observables - In this section we discuss `Observables` as an alternative to promises when processing http calls. + Each `Http` method returns an `Observable` of HTTP `Response` objects. - `Http` calls return RxJS observables by default, but so far we've been hiding that by converting the observable to a promise by calling `toPromise`. + Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller. + In this section we learn to return the `Observable` directly and discuss when and why that might be + a good thing to do. - Observables and promises are both great for processing http calls, but as we will see, the observables api is much richer. + ### Background + An *observable* is a stream of events that we can process with array-like operators. - `RxJs` offers a wide variety of operators that we can use to manage event flows. + Angular core has basic support for observables. We developers augment that support with + operators and extensions from the [RxJS Observables](http://reactivex.io/rxjs/) library. + We'll see how shortly. - In this section we will discuss how to use some of these operators to build a type-to-search filter where we can search for heroes by name. + Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`. + That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller. + + Converting to a promise is often a good choice. We typically ask `http` to fetch a single chunk of data. + When we receive the data, we're done. + A single result in the form of a promise is easy for the calling component to consume + and it helps that promises are widely understood by JavaScript programmers. + + But requests aren't always "one and done". We may start one request, + then cancel it, and make a different request ... before the server has responded to the first request. + Such a _request-cancel-new-request_ sequence is difficult to implement with *promises*. + It's easy with *observables* as we'll see. + + ### Search-by-name + We're going to add a *hero search* feature to the Tour of Heroes. + As the user types a name into a search box, we'll make repeated http requests for heroes filtered by that name. - We start by creating `HeroSearchService`, a simple service for sending search queries to our api. + We start by creating `HeroSearchService` that sends search queries to our server's web api. +makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".") :marked - The http call in `HeroSearchService` is not that different from our previous http calls, but we no longer call `toPromise`. This means we will return an observable instead of a promise. + The `http.get` call in `HeroSearchService` is similar to the `http.get` call in the `HeroService`. + The notable difference: we no longer call `toPromise`. + We simply return the *observable* instead. - Now, let's implement our search component `HeroSearchComponent`. - -+makeTabs( - `toh-6/ts/app/hero-search.component.ts, - toh-6/ts/app/hero-search.component.html`, - null, - `hero-search.component.ts, - hero-search.component.html` -) + ### HeroSearchComponent + Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`. + The component template is simple - just a textbox and a list of matching search results. ++makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html') :marked - The `HeroSearchComponent` UI is simple - just a textbox and a list of matching search results. - - To keep track of text changes in the search box we are using an RxJs `Subject`. - - Observables are great for managing event streams. In our example we will actually be dealing with two different types of event streams: - - 1) Key events from typing into the search textbox + As the user types in the search box, a *keyup* event binding calls `search.next` with the new search box value. - 2) Results from http calls based on values entered in the search textbox - - Although we are dealing with two different streams, we can use `RxJs` operators to combine them into a single stream. - - Converging on a single stream is the end game, but let's start by going through the different parts of the observable chain. - -+makeExample('toh-6/ts/app/hero-search.component.ts', 'search', 'app/hero-search.component.ts')(format=".") + The component's data bound `search` property returns a `Subject`. + A `Subject` is a producer of an _observable_ event stream. + Each call to `search.next` puts a new string into this subject's _observable_ stream. + The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there. + + But `heroes` is an `Observable` of heroes, not an array of heroes. + The `*ngFor` can't do anything with that until we flow it through the `AsyncPipe` (`heroes | async`). + The `AsyncPipe` subscribes to the observable and produces the array of heroes to `*ngFor`. + + Time to create the `HeroSearchComponent` class and metadata. ++makeExample('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts') +:marked + Scroll down to where we create the `search` subject. ++makeExample('toh-6/ts/app/hero-search.component.ts', 'subject') :marked - ### asObservable - We start the observable chain by calling `asObservable` on our `Subject` to return an observable that represents text changes as we are typing into the search box. + We're binding to that `search` subject in our template. + The user is sending it a stream of strings, the filter criteria for the name search. - ### debounceTime(300) - By specifying a debounce time of 300 ms we are telling `RxJs` that we only want to be notified of key events after intervals of 300 ms. This is a performance measure meant to prevent us from hitting the api too often with partial search queries. + A `Subject` is also an `Observable`. + We're going to access that `Observable` and append operators to it that turn the stream + of strings into a stream of `Hero[]` arrays. + + Each user keystroke could result in a new http request returning a new Observable array of heroes. - ### distinctUntilChanged - `distinctUntilChanged` tells `RxJs` to only react if the value of the textbox actually changed. + This could be a very chatty, taxing our server resources and burning up our cellular network data plan. + Fortunately we can chain `Observable` operators to reduce the request flow + and still get timely results. Here's how: - ### switchMap - `switchMap` is where observables from key events and observables from http calls converge on a single observable. ++makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".") +:marked + * The `asObservable` operator casts the `Subject` as an `Observable` of filter strings. - Every qualifying key event will trigger an http call, so we may get into a situation where we have multiple http calls in flight. + * `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds + before passing along the latest string. We'll never make requests more frequently than 300ms. - Normally this could lead to results being returned out of order, but `switchMap` has built in support for ensuring that only the most recent http call will be processed. Results from prior calls will be discarded. + * `distinctUntilChanged` ensures that we only send a request if the filter text changed. + There's no point in repeating a request for the same search term. - We short circuit the http call and return an observable containing an empty array if the search box is empty. + * `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet. + It discards previous search observables, returning only the latest search service observable. - ### subscribe - `subscribe` is similar to `then` in the promise world. - - The first callback is the success callback where we grab the result of the search and assign it to our `heroes` array. - - The second callback is the error callback. This callback will execute if there is an error - either from the http call or in our processing of key events. - - In our current implementation we are just logging the error to the console, but a real life application should do better. +.l-sub-section + :marked + The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) + (formerly known as "flatMapLatest") is very clever. + + Every qualifying key event can trigger an http call. + Even with a 300ms pause between requests, we could have multiple http requests in flight + and they may not return in the order sent. + + `switchMap` preserves the original request order while returning + only the observable from the most recent http call. + Results from prior calls will be discarded. - ### Import operators - The `RxJs` operators are not included by default, so we have to include them using `import` statements. + We also short-circuit the http call and return an observable containing an empty array + if the search text is empty. +:marked + * `catch` intercepts a failed observable. + Our simple example prints the error to the console; a real life application should do better. + Then it re-throws the failed observable so that downstream processes know it failed. + The `AsyncPipe` in the template is downstream. It sees the failure and ignores it. + + ### Import RxJS operators + The RxJS operators are not available in Angular's base `Observable` implementation. + We have to extend `Observable` by *importing* them. - We have combined all operator imports in a single file. + We could extend `Observable` with just the operators we need here by + including the pertinent `import` statements at the top of this file. -+makeExample('toh-6/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".") +.l-sub-section + :marked + Many authorities say we should do just that. +:marked + We take a different approach in this example. + We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file. ++makeExample('toh-6/ts/app/rxjs-extensions.ts', null, 'app/rxjs-extensions.ts')(format=".") :marked - We can then load all the operators in a single operation by importing `rxjs-operators` in `AppComponent` + We load them all at once by importing `rxjs-extensions` in `AppComponent`. -+makeExample('toh-6/ts/app/app.component.ts', 'rxjs-operators', 'app/app/app.component.ts')(format=".") - ++makeExample('toh-6/ts/app/app.component.ts', 'rxjs-extensions', 'app/app/app.component.ts')(format=".") :marked - Here is the final search component. + Finally, we add the `HeroSearchComponent` to the bottom of the `DashboardComponent`. + Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles. + At some point it might look like this. figure.image-display img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") @@ -539,4 +587,4 @@ block file-summary hero-search.component.ts, hero-search.service.html, rxjs-operators.ts` -) \ No newline at end of file +)