Skip to content
This repository was archived by the owner on Dec 4, 2017. It is now read-only.

Commit d623425

Browse files
committed
docs(toh-pt6): add observables - ward's changes
1 parent 7dbbc0c commit d623425

File tree

7 files changed

+162
-90
lines changed

7 files changed

+162
-90
lines changed

public/docs/_examples/toh-6/ts/app/app.component.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { Component } from '@angular/core';
44
import { ROUTER_DIRECTIVES } from '@angular/router';
55

66
import { HeroService } from './hero.service';
7-
// #docregion rxjs-operators
8-
import './rxjs-operators';
9-
// #enddocregion rxjs-operators
7+
// #docregion rxjs-extensions
8+
import './rxjs-extensions';
9+
// #enddocregion rxjs-extensions
1010

1111
@Component({
1212
selector: 'my-app',
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<!-- #docregion -->
22
<div id="search-component">
3-
<h4>Hero Search</h4>
4-
<input #searchBox id="search-box" (keyup)="search.next(searchBox.value)" />
3+
<h4>Hero Search</h4>
4+
<input #searchBox id="search-box" (keyup)="search.next(searchBox.value)" />
55
<div>
6-
<div class="search-result" (click)="gotoDetail(hero)" *ngFor="let hero of heroes">
6+
<div *ngFor="let hero of heroes | async"
7+
(click)="gotoDetail(hero)" class="search-result" >
78
{{hero.name}}
89
</div>
910
</div>
10-
</div>
11+
</div>
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// #docplaster
12
// #docregion
23
import { Component, OnInit } from '@angular/core';
34
import { Router } from '@angular/router';
@@ -13,25 +14,40 @@ import { Hero } from './hero';
1314
providers: [HeroSearchService]
1415
})
1516
export class HeroSearchComponent implements OnInit {
17+
// #docregion subject
1618
search = new Subject<string>();
17-
heroes: Hero[] = [];
19+
// #enddocregion subject
20+
// #docregion search
21+
heroes: Observable<Hero>;
22+
// #enddocregion search
1823

19-
constructor(private _heroSearchService: HeroSearchService, private _router: Router) {}
24+
constructor(
25+
private heroSearchService: HeroSearchService,
26+
private router: Router) {}
2027

21-
gotoDetail(hero: Hero) {
22-
let link = ['/detail', hero.id];
23-
this._router.navigate(link);
24-
}
2528

2629
// #docregion search
2730
ngOnInit() {
28-
this.search.asObservable()
29-
.debounceTime(300)
30-
.distinctUntilChanged()
31-
.switchMap(term => term ? this._heroSearchService.search(term)
32-
: Observable.of([]))
33-
.subscribe(result => this.heroes = result,
34-
error => console.log(error));
31+
this.heroes = this.search
32+
.asObservable() // "cast" as Observable
33+
.debounceTime(300) // wait for 300ms pause in events
34+
.distinctUntilChanged() // ignore if next search term is same as previous
35+
.switchMap(term => term // switch to new observable each time
36+
// return the http search observable
37+
? this.heroSearchService.search(term)
38+
// or the observable of empty heroes if no search term
39+
: Observable.of<Hero[]>([]))
40+
41+
.catch(error => {
42+
// Todo: real error handling
43+
console.log(error);
44+
return Observable.throw(error);
45+
});
3546
}
3647
// #enddocregion search
48+
49+
gotoDetail(hero: Hero) {
50+
let link = ['/detail', hero.id];
51+
this.router.navigate(link);
52+
}
3753
}

public/docs/_examples/toh-6/ts/app/hero-search.service.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
import { Injectable } from '@angular/core';
33
import { Http, Response } from '@angular/http';
44

5+
import { Hero } from './hero';
6+
57
@Injectable()
68
export class HeroSearchService {
79

8-
constructor(private _http: Http) {}
10+
constructor(private http: Http) {}
911

1012
// #docregion observable-search
1113
search(term: string) {
12-
return this._http
14+
return this.http
1315
.get(`app/heroes/?name=${term}+`)
14-
.map((r: Response) => r.json().data);
16+
.map((r: Response) => r.json().data as Hero[]);
1517
}
1618
// #enddocregion observable-search
1719
}

public/docs/_examples/toh-6/ts/app/hero.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export class HeroService {
1717

1818
constructor(private http: Http) { }
1919

20-
getHeroes(): Promise<Hero[]> {
20+
getHeroes() {
2121
return this.http.get(this.heroesUrl)
2222
// #docregion to-promise
2323
.toPromise()
2424
// #enddocregion to-promise
2525
// #docregion to-data
26-
.then(response => response.json().data)
26+
.then(response => response.json().data as Hero[])
2727
// #enddocregion to-data
2828
// #docregion catch
2929
.catch(this.handleError);

public/docs/_examples/toh-6/ts/app/rxjs-operators.ts renamed to public/docs/_examples/toh-6/ts/app/rxjs-extensions.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
// #docregion
2+
// Observable class extensions
3+
import 'rxjs/add/observable/of';
4+
import 'rxjs/add/observable/throw';
5+
6+
// Observable operators
7+
import 'rxjs/add/operator/catch';
8+
import 'rxjs/add/operator/debounceTime';
9+
import 'rxjs/add/operator/distinctUntilChanged';
10+
import 'rxjs/add/operator/do';
11+
import 'rxjs/add/operator/filter';
212
import 'rxjs/add/operator/map';
313
import 'rxjs/add/operator/switchMap';
4-
import 'rxjs/add/operator/filter';
5-
import 'rxjs/add/operator/do';
6-
import 'rxjs/add/operator/distinctUntilChanged';
7-
import 'rxjs/add/operator/debounceTime';
8-
import 'rxjs/add/observable/of';

public/docs/ts/latest/tutorial/toh-pt6.jade

+108-60
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,10 @@ block get-heroes-details
131131
:marked
132132
The Angular `http.get` returns an RxJS `Observable`.
133133
*Observables* are a powerful way to manage asynchronous data flows.
134-
We'll learn about `Observables` *later*.
134+
We'll learn about [Observables](#observables) later in this chapter.
135135

136-
For *now* we get back on familiar ground by immediately converting that `Observable` to a `Promise` using the `toPromise` operator.
136+
For *now* we get back on familiar ground by immediately by
137+
converting that `Observable` to a `Promise` using the `toPromise` operator.
137138
+makeExample('toh-6/ts/app/hero.service.ts', 'to-promise')(format=".")
138139
:marked
139140
Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ... not out of the box.
@@ -361,92 +362,139 @@ block review
361362
:marked
362363
## Observables
363364

364-
In this section we discuss `Observables` as an alternative to promises when processing http calls.
365+
Each `Http` method returns an `Observable` of HTTP `Response` objects.
365366

366-
`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`.
367+
Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller.
368+
In this section we learn to return the `Observable` directly and discuss when and why that might be
369+
a good thing to do.
367370

368-
Observables and promises are both great for processing http calls, but as we will see, the observables api is much richer.
371+
### Background
372+
An *observable* is a stream of events that we can process with array-like operators.
369373

370-
`RxJs` offers a wide variety of operators that we can use to manage event flows.
374+
Angular core has basic support for observables. We developers augment that support with
375+
operators and extensions from the [RxJS Observables](http://reactivex.io/rxjs/) library.
376+
We'll see how shortly.
371377

372-
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.
378+
Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`.
379+
That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller.
380+
381+
Converting to a promise is often a good choice. We typically ask `http` to fetch a single chunk of data.
382+
When we receive the data, we're done.
383+
A single result in the form of a promise is easy for the calling component to consume
384+
and it helps that promises are widely understood by JavaScript programmers.
385+
386+
But requests aren't always "one and done". We may start one request,
387+
then cancel it, and make a different request ... before the server has responded to the first request.
388+
Such a _request-cancel-new-request_ sequence is difficult to implement with *promises*.
389+
It's easy with *observables* as we'll see.
390+
391+
### Search-by-name
392+
We're going to add a *hero search* feature to the Tour of Heroes.
393+
As the user types a name into a search box, we'll make repeated http requests for heroes filtered by that name.
373394

374-
We start by creating `HeroSearchService`, a simple service for sending search queries to our api.
395+
We start by creating `HeroSearchService` that sends search queries to our server's web api.
375396

376397
+makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".")
377398

378399
:marked
379-
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.
400+
The `http.get` call in `HeroSearchService` is similar to the `http.get` call in the `HeroService`.
401+
The notable difference: we no longer call `toPromise`.
402+
We simply return the *observable* instead.
380403

381-
Now, let's implement our search component `HeroSearchComponent`.
382-
383-
+makeTabs(
384-
`toh-6/ts/app/hero-search.component.ts,
385-
toh-6/ts/app/hero-search.component.html`,
386-
null,
387-
`hero-search.component.ts,
388-
hero-search.component.html`
389-
)
404+
### HeroSearchComponent
405+
Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`.
390406

407+
The component template is simple - just a textbox and a list of matching search results.
408+
+makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html')
391409
:marked
392-
The `HeroSearchComponent` UI is simple - just a textbox and a list of matching search results.
393-
394-
To keep track of text changes in the search box we are using an RxJs `Subject`.
395-
396-
Observables are great for managing event streams. In our example we will actually be dealing with two different types of event streams:
397-
398-
1) Key events from typing into the search textbox
410+
As the user types in the search box, a *keyup* event binding calls `search.next` with the new search box value.
399411

400-
2) Results from http calls based on values entered in the search textbox
401-
402-
Although we are dealing with two different streams, we can use `RxJs` operators to combine them into a single stream.
403-
404-
Converging on a single stream is the end game, but let's start by going through the different parts of the observable chain.
405-
406-
+makeExample('toh-6/ts/app/hero-search.component.ts', 'search', 'app/hero-search.component.ts')(format=".")
412+
The component's data bound `search` property returns a `Subject`.
413+
A `Subject` is a producer of an _observable_ event stream.
414+
Each call to `search.next` puts a new string into this subject's _observable_ stream.
407415

416+
The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there.
417+
418+
But `heroes` is an `Observable` of heroes, not an array of heroes.
419+
The `*ngFor` can't do anything with that until we flow it through the `AsyncPipe` (`heroes | async`).
420+
The `AsyncPipe` subscribes to the observable and produces the array of heroes to `*ngFor`.
421+
422+
Time to create the `HeroSearchComponent` class and metadata.
423+
+makeExample('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts')
424+
:marked
425+
Scroll down to where we create the `search` subject.
426+
+makeExample('toh-6/ts/app/hero-search.component.ts', 'subject')
408427
:marked
409-
### asObservable
410-
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.
428+
We're binding to that `search` subject in our template.
429+
The user is sending it a stream of strings, the filter criteria for the name search.
411430

412-
### debounceTime(300)
413-
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.
431+
A `Subject` is also an `Observable`.
432+
We're going to access that `Observable` and append operators to it that turn the stream
433+
of strings into a stream of `Hero[]` arrays.
434+
435+
Each user keystroke could result in a new http request returning a new Observable array of heroes.
414436

415-
### distinctUntilChanged
416-
`distinctUntilChanged` tells `RxJs` to only react if the value of the textbox actually changed.
437+
This could be a very chatty, taxing our server resources and burning up our cellular network data plan.
438+
Fortunately we can chain `Observable` operators to reduce the request flow
439+
and still get timely results. Here's how:
417440

418-
### switchMap
419-
`switchMap` is where observables from key events and observables from http calls converge on a single observable.
441+
+makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".")
442+
:marked
443+
* The `asObservable` operator casts the `Subject` as an `Observable` of filter strings.
420444

421-
Every qualifying key event will trigger an http call, so we may get into a situation where we have multiple http calls in flight.
445+
* `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds
446+
before passing along the latest string. We'll never make requests more frequently than 300ms.
422447

423-
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.
448+
* `distinctUntilChanged` ensures that we only send a request if the filter text changed.
449+
There's no point in repeating a request for the same search term.
424450

425-
We short circuit the http call and return an observable containing an empty array if the search box is empty.
451+
* `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet.
452+
It discards previous search observables, returning only the latest search service observable.
426453

427-
### subscribe
428-
`subscribe` is similar to `then` in the promise world.
429-
430-
The first callback is the success callback where we grab the result of the search and assign it to our `heroes` array.
431-
432-
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.
433-
434-
In our current implementation we are just logging the error to the console, but a real life application should do better.
454+
.l-sub-section
455+
:marked
456+
The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)
457+
(formerly known as "flatMapLatest") is very clever.
458+
459+
Every qualifying key event can trigger an http call.
460+
Even with a 300ms pause between requests, we could have multiple http requests in flight
461+
and they may not return in the order sent.
462+
463+
`switchMap` preserves the original request order while returning
464+
only the observable from the most recent http call.
465+
Results from prior calls will be discarded.
435466

436-
### Import operators
437-
The `RxJs` operators are not included by default, so we have to include them using `import` statements.
467+
We also short-circuit the http call and return an observable containing an empty array
468+
if the search text is empty.
469+
:marked
470+
* `catch` intercepts a failed observable.
471+
Our simple example prints the error to the console; a real life application should do better.
472+
Then it re-throws the failed observable so that downstream processes know it failed.
473+
The `AsyncPipe` in the template is downstream. It sees the failure and ignores it.
474+
475+
### Import RxJS operators
476+
The RxJS operators are not available in Angular's base `Observable` implementation.
477+
We have to extend `Observable` by *importing* them.
438478

439-
We have combined all operator imports in a single file.
479+
We could extend `Observable` with just the operators we need here by
480+
including the pertinent `import` statements at the top of this file.
440481

441-
+makeExample('toh-6/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".")
482+
.l-sub-section
483+
:marked
484+
Many authorities say we should do just that.
485+
:marked
486+
We take a different approach in this example.
487+
We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file.
442488

489+
+makeExample('toh-6/ts/app/rxjs-extensions.ts', null, 'app/rxjs-extensions.ts')(format=".")
443490
:marked
444-
We can then load all the operators in a single operation by importing `rxjs-operators` in `AppComponent`
491+
We load them all at once by importing `rxjs-extensions` in `AppComponent`.
445492

446-
+makeExample('toh-6/ts/app/app.component.ts', 'rxjs-operators', 'app/app/app.component.ts')(format=".")
447-
493+
+makeExample('toh-6/ts/app/app.component.ts', 'rxjs-extensions', 'app/app/app.component.ts')(format=".")
448494
:marked
449-
Here is the final search component.
495+
Finally, we add the `HeroSearchComponent` to the bottom of the `DashboardComponent`.
496+
Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles.
497+
At some point it might look like this.
450498

451499
figure.image-display
452500
img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component")
@@ -539,4 +587,4 @@ block file-summary
539587
hero-search.component.ts,
540588
hero-search.service.html,
541589
rxjs-operators.ts`
542-
)
590+
)

0 commit comments

Comments
 (0)