Skip to content
This repository was archived by the owner on Dec 4, 2017. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 34b17f7

Browse files
committedMay 28, 2016
docs(add observables to TOH http)
e2e prose tweaks
1 parent 03f346b commit 34b17f7

File tree

11 files changed

+233
-6
lines changed

11 files changed

+233
-6
lines changed
 

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

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

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

24-
heroDetail: element(by.css('my-app my-hero-detail'))
24+
heroDetail: element(by.css('my-app my-hero-detail')),
25+
26+
searchBox: element(by.css('#search-box')),
27+
searchResults: element.all(by.css('.search-result'))
2528
}
2629
}
2730

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

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
// #docregion
33
import { Component } from '@angular/core';
44
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router-deprecated';
5-
5+
// #docregion rxjs-operators
6+
import './rxjs-operators';
7+
// #enddocregion rxjs-operators
68
import { DashboardComponent } from './dashboard.component';
79
import { HeroesComponent } from './heroes.component';
810
import { HeroDetailComponent } from './hero-detail.component';
11+
import { HeroSearchComponent } from './hero-search.component';
912
import { HeroService } from './hero.service';
1013

1114
@Component({
@@ -20,7 +23,7 @@ import { HeroService } from './hero.service';
2023
<router-outlet></router-outlet>
2124
`,
2225
styleUrls: ['app/app.component.css'],
23-
directives: [ROUTER_DIRECTIVES],
26+
directives: [ROUTER_DIRECTIVES, HeroSearchComponent],
2427
providers: [
2528
ROUTER_PROVIDERS,
2629
HeroService,

‎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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
// #docplaster
22
// #docregion
33
import { Component, OnInit } from '@angular/core';
4-
import { Router } from '@angular/router-deprecated';
4+
import { Router } from '@angular/router-deprecated';
55

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

911
@Component({
1012
selector: 'my-dashboard',
1113
templateUrl: 'app/dashboard.component.html',
12-
styleUrls: ['app/dashboard.component.css']
14+
styleUrls: ['app/dashboard.component.css'],
15+
directives: [HeroSearchComponent]
1316
})
1417
export class DashboardComponent implements OnInit {
1518

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!-- #docregion -->
2+
<div id="search-component">
3+
<h4>Hero Search</h4>
4+
<input id="search-box" [ngFormControl]="search" />
5+
<div *ngIf="showResult">
6+
<div class="search-result" (click)="gotoDetail(hero)" *ngFor="let hero of heroes">
7+
{{hero.name}}
8+
</div>
9+
</div>
10+
</div>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// #docregion
2+
import { Component, OnInit } from '@angular/core';
3+
import { Control } from '@angular/common';
4+
import { Router } from '@angular/router-deprecated';
5+
6+
import { HeroSearchService } from './hero-search.service';
7+
import { Hero } from './hero';
8+
9+
@Component({
10+
selector: 'hero-search',
11+
templateUrl: 'app/hero-search.component.html',
12+
providers: [HeroSearchService]
13+
})
14+
export class HeroSearchComponent implements OnInit {
15+
search = new Control('');
16+
heroes: Hero[] = [];
17+
showResult = true;
18+
19+
constructor(private _heroSearchService:HeroSearchService, private _router:Router) {}
20+
21+
gotoDetail(hero: Hero) {
22+
let link = ['HeroDetail', { id: hero.id }];
23+
this._router.navigate(link);
24+
}
25+
26+
// #docregion search
27+
ngOnInit() {
28+
this.search.valueChanges
29+
.debounceTime(300)
30+
.distinctUntilChanged()
31+
.do(value => this.showResult = value.length > 0)
32+
.filter(value => value.length > 0)
33+
.switchMap(term => this._heroSearchService.search(term))
34+
.subscribe(result => this.heroes = result, error => console.log(error));
35+
}
36+
// #enddocregion search
37+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// #docregion
2+
import { Injectable } from '@angular/core';
3+
import { Http, Response } from '@angular/http';
4+
5+
@Injectable()
6+
export class HeroSearchService {
7+
8+
constructor(private _http:Http) {}
9+
10+
// #docregion observable-search
11+
search(term:string) {
12+
return this._http
13+
.get(`app/heroes/?name=${term}+`)
14+
.map((r:Response) => r.json().data)
15+
}
16+
// #enddocregion observable-search
17+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// #docregion
2+
import 'rxjs/add/operator/map';
3+
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';

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,19 @@ button.delete-button{
44
color:white;
55
}
66

7+
.search-result{
8+
border-bottom: 1px solid gray;
9+
border-left: 1px solid gray;
10+
border-right: 1px solid gray;
11+
width:195px;
12+
height: 20px;
13+
padding: 5px;
14+
background-color: white;
15+
cursor: pointer;
16+
}
717

18+
#search-box{
19+
width: 200px;
20+
height: 20px;
21+
}
822

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

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ code-example(format="." language="bash").
110110

111111
The Angular `http.get` returns an RxJS `Observable`.
112112
*Observables* are a powerful way to manage asynchronous data flows.
113-
We'll learn about `Observables` *later*.
113+
We'll learn about `Observables` in the next chapter.
114114

115115
For *now* we get back on familiar ground by immediately converting that `Observable` to a `Promise` using the `toPromise` operator.
116116
+makeExample('toh-6/ts/app/hero.service.ts', 'to-promise')(format=".")
@@ -302,6 +302,103 @@ figure.image-display
302302
figure.image-display
303303
img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editting w/ HTTP")
304304

305+
.l-main-section
306+
:marked
307+
## Observables
308+
309+
In this section we discuss `Observables` as an alternative to promises when processing http calls.
310+
311+
`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`.
312+
313+
Observables and promises are both great for processing http calls, but observables differ in that the api for processing events is much richer.
314+
315+
`RxJs` offers a wide variety of operators that we can use to manage event flows.
316+
317+
In this chapter 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.
318+
319+
We start by creating `HeroSearchService`, a simple service for sending search queries to our api.
320+
321+
+makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".")
322+
323+
:marked
324+
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.
325+
326+
Now, let's implement our search component `HeroSearchComponent`.
327+
328+
+makeTabs(
329+
`toh-6/ts/app/hero-search.component.ts,
330+
toh-6/ts/app/hero-search.component.html`,
331+
null,
332+
`hero-search.component.ts,
333+
hero-search.component.html`
334+
)
335+
336+
:marked
337+
The `HeroSearchComponent` UI is simple - just a textbox and a list of matching search results.
338+
339+
We have bound the `search` property to the textbox as an `ngFormControl` in order to track changes.
340+
341+
Observables are great for managing event streams. In our example we will actually be dealing with two different types of event streams:
342+
343+
1) Key events from typing into the search textbox
344+
345+
2) Results from http calls based on values entered in the search textbox
346+
347+
Although we are dealing with two different streams, we can use `RxJs` operators to combine them into a single stream.
348+
349+
Converging on a single stream is the end game, but let's start by going through the different parts of the observable chain.
350+
351+
+makeExample('toh-6/ts/app/hero-search.component.ts', 'search', 'app/hero-search.component.ts')(format=".")
352+
353+
:marked
354+
### valueChanges
355+
`ValueChanges` is an observable that represents text changes triggered by typing into the search textbox.
356+
357+
### debounceTime(300)
358+
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.
359+
360+
### distinctUntilChanged
361+
`distinctUntilChanged` tells `RxJs` to only react if the value of the textbox actually changed.
362+
363+
### do
364+
`do` is an operator for triggering side effects outside the observable chain. In our case we are using it to set a flag to hide the search results if the search textbox is empty.
365+
366+
### filter
367+
By specifying a `filter` we are telling `RxJs` to only process search values that meet our filter condition.
368+
369+
### switchMap
370+
`switchMap` is where observables from key events and observables from http calls converge on a single observable.
371+
372+
Every qualifying key event will trigger an http call, so we may get into a situation where we have multiple http calls in flight.
373+
374+
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.
375+
376+
### subscribe
377+
`subscribe` is similar to `then` in the promise world.
378+
379+
The first callback is the success callback where we grab the result of the search and assign it to our `heroes` array.
380+
381+
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.
382+
383+
In our current implementation we are just logging the error to the console, but a real life application should do better.
384+
385+
None of these operators are not included by default, so we have to load each one using `import` statements before using them.
386+
387+
We have combined all the operator imports in a single file.
388+
389+
+makeExample('toh-6/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".")
390+
391+
:marked
392+
We can then load all the operators in a single operation by importing `rxjs-operators` is `AppComponent`
393+
394+
+makeExample('toh-6/ts/app/app.component.ts', 'rxjs-operators', 'app/app/app.component.ts')(format=".")
395+
396+
:marked
397+
Here is the final search component.
398+
399+
figure.image-display
400+
img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component")
401+
305402
:marked
306403
### Review the App Structure
307404
Let’s verify that we have the following structure after all of our good refactoring in this chapter:
@@ -320,11 +417,15 @@ figure.image-display
320417
.file hero-detail.component.css
321418
.file hero-detail.component.html
322419
.file hero-detail.component.ts
420+
.file hero-search.component.html
421+
.file hero-search.component.ts
422+
.file hero-search.service.ts
323423
.file hero.service.ts
324424
.file heroes.component.css
325425
.file heroes.component.html
326426
.file heroes.component.ts
327427
.file main.ts
428+
.file rxjs-operators.ts
328429
.file hero-data.service.ts
329430
.file node_modules ...
330431
.file typings ...
@@ -346,6 +447,7 @@ figure.image-display
346447
- We extended HeroService to support post, put and delete calls.
347448
- We updated our components to allow adding, editing and deleting of heroes.
348449
- We configured an in-memory web api.
450+
- We learned how to use Observables.
349451

350452
Below is a summary of the files we changed.
351453

@@ -365,3 +467,14 @@ figure.image-display
365467
hero.service.ts`
366468
)
367469

470+
+makeTabs(
471+
`toh-6/ts/app/hero-search.service.ts,
472+
toh-6/ts/app/hero-search.component.ts,
473+
toh-6/ts/app/hero-search.component.html,
474+
toh-6/ts/app/rxjs-operators.ts`,
475+
null,
476+
`hero-search.service.ts,
477+
hero-search.component.ts,
478+
hero-search.service.html,
479+
rxjs-operators.ts`
480+
)
Loading

0 commit comments

Comments
 (0)
This repository has been archived.