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

Commit f50dff8

Browse files
thelgevoldwardbell
authored andcommittedJul 19, 2016
docs(toh-pt6): add observables to HTTP tutorial chapter
closes #1528
1 parent f056a2d commit f50dff8

File tree

12 files changed

+306
-7
lines changed

12 files changed

+306
-7
lines changed
 

‎public/docs/_examples/toh-6/e2e-spec.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,31 @@ describe('TOH Http Chapter', function () {
2323

2424
addButton: element.all(by.buttonText('Add New Hero')).get(0),
2525

26-
heroDetail: element(by.css('my-app my-hero-detail'))
26+
heroDetail: element(by.css('my-app my-hero-detail')),
27+
28+
searchBox: element(by.css('#search-box')),
29+
searchResults: element.all(by.css('.search-result'))
2730
};
2831
}
2932

33+
it('should search for hero and navigate to details view', function() {
34+
let page = getPageStruct();
35+
36+
return sendKeys(page.searchBox, 'Magneta').then(function () {
37+
expect(page.searchResults.count()).toBe(1);
38+
let hero = page.searchResults.get(0);
39+
return hero.click();
40+
})
41+
.then(function() {
42+
browser.waitForAngular();
43+
let inputEle = page.heroDetail.element(by.css('input'));
44+
return inputEle.getAttribute('value');
45+
})
46+
.then(function(value) {
47+
expect(value).toBe('Magneta');
48+
});
49+
});
50+
3051
it('should be able to add a hero from the "Heroes" view', function(){
3152
let page = getPageStruct();
3253
let heroCount: webdriver.promise.Promise<number>;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { Component } from '@angular/core';
44
import { ROUTER_DIRECTIVES } from '@angular/router';
55

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

811
@Component({
912
selector: 'my-app',

‎public/docs/_examples/toh-6/ts/app/dashboard.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ <h4>{{hero.name}}</h4>
99
</div>
1010
</div>
1111
</div>
12+
<hero-search></hero-search>
13+

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { Router } from '@angular/router';
55

66
import { Hero } from './hero';
77
import { HeroService } from './hero.service';
8+
import { HeroSearchComponent } from './hero-search.component';
89

910
@Component({
1011
selector: 'my-dashboard',
1112
templateUrl: 'app/dashboard.component.html',
12-
styleUrls: ['app/dashboard.component.css']
13+
styleUrls: ['app/dashboard.component.css'],
14+
directives: [HeroSearchComponent]
1315
})
1416
export class DashboardComponent implements OnInit {
1517

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!-- #docregion -->
2+
<div id="search-component">
3+
<h4>Hero Search</h4>
4+
<input #searchBox id="search-box" (keyup)="search.next(searchBox.value)" />
5+
<div>
6+
<div *ngFor="let hero of heroes | async"
7+
(click)="gotoDetail(hero)" class="search-result" >
8+
{{hero.name}}
9+
</div>
10+
</div>
11+
</div>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// #docplaster
2+
// #docregion
3+
import { Component, OnInit } from '@angular/core';
4+
import { Router } from '@angular/router';
5+
import { Observable } from 'rxjs/Observable';
6+
import { Subject } from 'rxjs/Subject';
7+
8+
import { HeroSearchService } from './hero-search.service';
9+
import { Hero } from './hero';
10+
11+
@Component({
12+
selector: 'hero-search',
13+
templateUrl: 'app/hero-search.component.html',
14+
providers: [HeroSearchService]
15+
})
16+
export class HeroSearchComponent implements OnInit {
17+
// #docregion subject
18+
search = new Subject<string>();
19+
// #enddocregion subject
20+
// #docregion search
21+
heroes: Observable<Hero>;
22+
// #enddocregion search
23+
24+
constructor(
25+
private heroSearchService: HeroSearchService,
26+
private router: Router) {}
27+
28+
29+
// #docregion search
30+
ngOnInit() {
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+
});
46+
}
47+
// #enddocregion search
48+
49+
gotoDetail(hero: Hero) {
50+
let link = ['/detail', hero.id];
51+
this.router.navigate(link);
52+
}
53+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// #docregion
2+
import { Injectable } from '@angular/core';
3+
import { Http, Response } from '@angular/http';
4+
5+
import { Hero } from './hero';
6+
7+
@Injectable()
8+
export class HeroSearchService {
9+
10+
constructor(private http: Http) {}
11+
12+
// #docregion observable-search
13+
search(term: string) {
14+
return this.http
15+
.get(`app/heroes/?name=${term}+`)
16+
.map((r: Response) => r.json().data as Hero[]);
17+
}
18+
// #enddocregion observable-search
19+
}

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

Lines changed: 2 additions & 2 deletions
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);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// #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';
12+
import 'rxjs/add/operator/map';
13+
import 'rxjs/add/operator/switchMap';

‎public/docs/_examples/toh-6/ts/sample.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,20 @@ button.delete-button{
55
background-color: gray !important;
66
color:white;
77
}
8+
9+
.search-result{
10+
border-bottom: 1px solid gray;
11+
border-left: 1px solid gray;
12+
border-right: 1px solid gray;
13+
width:195px;
14+
height: 20px;
15+
padding: 5px;
16+
background-color: white;
17+
cursor: pointer;
18+
}
19+
20+
#search-box{
21+
width: 200px;
22+
height: 20px;
23+
}
24+

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

Lines changed: 161 additions & 3 deletions
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.
@@ -358,6 +359,146 @@ block review
358359
figure.image-display
359360
img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editting w/ HTTP")
360361

362+
:marked
363+
## Observables
364+
365+
Each `Http` method returns an `Observable` of HTTP `Response` objects.
366+
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.
370+
371+
### Background
372+
An *observable* is a stream of events that we can process with array-like operators.
373+
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.
377+
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.
394+
395+
We start by creating `HeroSearchService` that sends search queries to our server's web api.
396+
397+
+makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".")
398+
399+
:marked
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.
403+
404+
### HeroSearchComponent
405+
Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`.
406+
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')
409+
:marked
410+
As the user types in the search box, a *keyup* event binding calls `search.next` with the new search box value.
411+
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.
415+
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')
427+
:marked
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.
430+
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.
436+
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:
440+
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.
444+
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.
447+
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.
450+
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.
453+
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.
466+
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.
478+
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.
481+
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.
488+
489+
+makeExample('toh-6/ts/app/rxjs-extensions.ts', null, 'app/rxjs-extensions.ts')(format=".")
490+
:marked
491+
We load them all at once by importing `rxjs-extensions` in `AppComponent`.
492+
493+
+makeExample('toh-6/ts/app/app.component.ts', 'rxjs-extensions', 'app/app/app.component.ts')(format=".")
494+
:marked
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.
498+
499+
figure.image-display
500+
img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component")
501+
361502
.l-main-section
362503
:marked
363504
## Application structure and code
@@ -381,6 +522,10 @@ block filetree
381522
.file hero-detail.component.css
382523
.file hero-detail.component.html
383524
.file hero-detail.component.ts
525+
.file hero-search.component.html
526+
.file hero-search.component.ts
527+
.file hero-search.service.ts
528+
.file rxjs-operators.ts
384529
.file hero.service.ts
385530
.file heroes.component.css
386531
.file heroes.component.html
@@ -407,7 +552,8 @@ block filetree
407552
- We extended HeroService to support post, put and delete calls.
408553
- We updated our components to allow adding, editing and deleting of heroes.
409554
- We configured an in-memory web API.
410-
555+
- We learned how to use Observables.
556+
411557
Below is a summary of the files we changed and added.
412558

413559
block file-summary
@@ -430,3 +576,15 @@ block file-summary
430576
in-memory-data.service.ts,
431577
sample.css`
432578
)
579+
580+
+makeTabs(
581+
`toh-6/ts/app/hero-search.service.ts,
582+
toh-6/ts/app/hero-search.component.ts,
583+
toh-6/ts/app/hero-search.component.html,
584+
toh-6/ts/app/rxjs-operators.ts`,
585+
null,
586+
`hero-search.service.ts,
587+
hero-search.component.ts,
588+
hero-search.service.html,
589+
rxjs-operators.ts`
590+
)
Loading

0 commit comments

Comments
 (0)
This repository has been archived.