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

Commit c812ee6

Browse files
committed
docs(add observables to TOH http)
e2e prose tweaks d d d section lint jade space update remove r test tweak tweak
1 parent b669e30 commit c812ee6

11 files changed

+230
-3
lines changed

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

+22-1
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

+3
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-operators
8+
import './rxjs-operators';
9+
// #enddocregion rxjs-operators
710

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

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

+2
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

+3-1
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

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 #searchBox id="search-box" (keyup)="search.next(searchBox.value)" />
5+
<div>
6+
<div class="search-result" (click)="gotoDetail(hero)" *ngFor="let hero of heroes">
7+
{{hero.name}}
8+
</div>
9+
</div>
10+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// #docregion
2+
import { Component, OnInit } from '@angular/core';
3+
import { Router } from '@angular/router';
4+
import { Observable } from 'rxjs/Observable';
5+
import { Subject } from 'rxjs/Subject';
6+
7+
import { HeroSearchService } from './hero-search.service';
8+
import { Hero } from './hero';
9+
10+
@Component({
11+
selector: 'hero-search',
12+
templateUrl: 'app/hero-search.component.html',
13+
providers: [HeroSearchService]
14+
})
15+
export class HeroSearchComponent implements OnInit {
16+
search = new Subject<string>();
17+
heroes: Hero[] = [];
18+
19+
constructor(private _heroSearchService: HeroSearchService, private _router: Router) {}
20+
21+
gotoDetail(hero: Hero) {
22+
let link = ['/detail', hero.id];
23+
this._router.navigate(link);
24+
}
25+
26+
// #docregion search
27+
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));
35+
}
36+
// #enddocregion search
37+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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';
8+
import 'rxjs/add/observable/of';

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

+17
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

+111-1
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,99 @@ block review
358358
figure.image-display
359359
img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editting w/ HTTP")
360360

361+
:marked
362+
## Observables
363+
364+
In this section we discuss `Observables` as an alternative to promises when processing http calls.
365+
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+
368+
Observables and promises are both great for processing http calls, but as we will see, the observables api is much richer.
369+
370+
`RxJs` offers a wide variety of operators that we can use to manage event flows.
371+
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.
373+
374+
We start by creating `HeroSearchService`, a simple service for sending search queries to our api.
375+
376+
+makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".")
377+
378+
: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.
380+
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+
)
390+
391+
: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
399+
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=".")
407+
408+
: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.
411+
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.
414+
415+
### distinctUntilChanged
416+
`distinctUntilChanged` tells `RxJs` to only react if the value of the textbox actually changed.
417+
418+
### switchMap
419+
`switchMap` is where observables from key events and observables from http calls converge on a single observable.
420+
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.
422+
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.
424+
425+
We short circuit the http call and return an observable containing an empty array if the search box is empty.
426+
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.
435+
436+
### Import operators
437+
The `RxJs` operators are not included by default, so we have to include them using `import` statements.
438+
439+
We have combined all operator imports in a single file.
440+
441+
+makeExample('toh-6/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".")
442+
443+
:marked
444+
We can then load all the operators in a single operation by importing `rxjs-operators` in `AppComponent`
445+
446+
+makeExample('toh-6/ts/app/app.component.ts', 'rxjs-operators', 'app/app/app.component.ts')(format=".")
447+
448+
:marked
449+
Here is the final search component.
450+
451+
figure.image-display
452+
img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component")
453+
361454
.l-main-section
362455
:marked
363456
## Application structure and code
@@ -381,6 +474,10 @@ block filetree
381474
.file hero-detail.component.css
382475
.file hero-detail.component.html
383476
.file hero-detail.component.ts
477+
.file hero-search.component.html
478+
.file hero-search.component.ts
479+
.file hero-search.service.ts
480+
.file rxjs-operators.ts
384481
.file hero.service.ts
385482
.file heroes.component.css
386483
.file heroes.component.html
@@ -407,7 +504,8 @@ block filetree
407504
- We extended HeroService to support post, put and delete calls.
408505
- We updated our components to allow adding, editing and deleting of heroes.
409506
- We configured an in-memory web API.
410-
507+
- We learned how to use Observables.
508+
411509
Below is a summary of the files we changed and added.
412510

413511
block file-summary
@@ -430,3 +528,15 @@ block file-summary
430528
in-memory-data.service.ts,
431529
sample.css`
432530
)
531+
532+
+makeTabs(
533+
`toh-6/ts/app/hero-search.service.ts,
534+
toh-6/ts/app/hero-search.component.ts,
535+
toh-6/ts/app/hero-search.component.html,
536+
toh-6/ts/app/rxjs-operators.ts`,
537+
null,
538+
`hero-search.service.ts,
539+
hero-search.component.ts,
540+
hero-search.service.html,
541+
rxjs-operators.ts`
542+
)
Loading

0 commit comments

Comments
 (0)