docs(server-communication): fix http after testing chapter discoveries

This commit is contained in:
Ward Bell 2016-04-13 08:21:32 -07:00
parent dd7b5176a8
commit 353abff084
12 changed files with 213 additions and 117 deletions

View File

@ -1,15 +1,21 @@
//#docregion
import {bootstrap} from 'angular2/platform/browser';
// #docplaster
// #docregion
import { bootstrap } from 'angular2/platform/browser';
// #docregion http-providers
import { HTTP_PROVIDERS } from 'angular2/http';
// #enddocregion http-providers
// #docregion import-rxjs
// Add all operators to Observable
import 'rxjs/Rx';
// #enddocregion import-rxjs
import {WikiComponent} from './wiki/wiki.component';
import {WikiSmartComponent} from './wiki/wiki-smart.component';
import {TohComponent} from './toh/toh.component';
import { WikiComponent } from './wiki/wiki.component';
import { WikiSmartComponent } from './wiki/wiki-smart.component';
import { TohComponent } from './toh/toh.component';
bootstrap(WikiComponent);
bootstrap(WikiSmartComponent);
bootstrap(TohComponent);
// #docregion http-providers
bootstrap(TohComponent, [HTTP_PROVIDERS]);
// #enddocregion http-providers

View File

@ -1,3 +1,4 @@
// ToH Promise Version
// #docregion
import {Component, OnInit} from 'angular2/core';
import {Hero} from './hero';
@ -5,22 +6,7 @@ import {HeroService} from './hero.service.1';
@Component({
selector: 'hero-list',
// #docregion template
template: `
<h3>Heroes:</h3>
<ul>
<li *ngFor="#hero of heroes">
{{ hero.name }}
</li>
</ul>
New Hero:
<input #newHero />
<button (click)="addHero(newHero.value); newHero.value=''">
Add Hero
</button>
<div class="error" *ngIf="errorMessage">{{errorMessage}}</div>
`,
// #enddocregion template
templateUrl: 'app/toh/hero-list.component.html',
styles: ['.error {color:red;}']
})
// #docregion component
@ -29,7 +15,7 @@ export class HeroListComponent implements OnInit {
constructor (private _heroService: HeroService) {}
errorMessage: string;
heroes:Hero[];
heroes: Hero[];
ngOnInit() { this.getHeroes(); }

View File

@ -0,0 +1,13 @@
<!-- #docregion -->
<h3>Heroes:</h3>
<ul>
<li *ngFor="#hero of heroes">
{{ hero.name }}
</li>
</ul>
New Hero:
<input #newHero />
<button (click)="addHero(newHero.value); newHero.value=''">
Add Hero
</button>
<div class="error" *ngIf="errorMessage">{{errorMessage}}</div>

View File

@ -5,20 +5,7 @@ import {HeroService} from './hero.service';
@Component({
selector: 'hero-list',
template: `
<h3>Heroes:</h3>
<ul>
<li *ngFor="#hero of heroes">
{{ hero.name }}
</li>
</ul>
New Hero:
<input #newHero />
<button (click)="addHero(newHero.value); newHero.value=''">
Add Hero
</button>
<div class="error" *ngIf="errorMessage">{{errorMessage}}</div>
`,
templateUrl: 'app/toh/hero-list.component.html',
styles: ['.error {color:red;}']
})
// #docregion component

View File

@ -1,9 +1,9 @@
/* Promise version */
// ToH Promise Version
// #docplaster
// #docregion
import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
import {Http, Response} from 'angular2/http';
import {Headers, RequestOptions} from 'angular2/http';
import {Hero} from './hero';
@ -15,27 +15,36 @@ export class HeroService {
private _heroesUrl = 'app/heroes.json';
// #docregion methods
getHeroes () {
getHeroes (): Promise<Hero[]> {
return this.http.get(this._heroesUrl)
.toPromise()
.then(res => <Hero[]> res.json().data, this.handleError)
.then(data => { console.log(data); return data; }); // eyeball results in the console
.then(this.extractData)
.catch(this.handleError);
}
addHero (name: string) : Promise<Hero> {
addHero (name: string): Promise<Hero> {
let body = JSON.stringify({ name });
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this._heroesUrl, body, options)
.toPromise()
.then(res => <Hero> res.json().data)
.then(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
if (res.status < 200 || res.status >= 300) {
throw new Error('Bad response status: ' + res.status);
}
let body = res.json();
return body.data || { };
}
private handleError (error: any) {
// in a real world app, we may send the error to some remote logging infrastructure
console.error(error); // log to console instead
// In a real world app, we might send the error to remote logging infrastructure
let errMsg = error.message || 'Server error';
console.error(errMsg); // log to console instead
return Promise.reject(errMsg);
}

View File

@ -3,7 +3,7 @@
// #docregion
// #docregion v1
import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
import {Http, Response} from 'angular2/http';
// #enddocregion v1
// #docregion import-request-options
import {Headers, RequestOptions} from 'angular2/http';
@ -31,18 +31,13 @@ export class HeroService {
// #enddocregion endpoint
// #docregion methods
// #docregion error-handling
// #docregion error-handling, http-get
getHeroes (): Observable<Hero[]> {
// #docregion http-get, http-get-v1
return this.http.get(this._heroesUrl)
.map(this.extractData)
// #enddocregion v1, http-get-v1, error-handling
.do(data => console.log(data)) // eyeball results in the console
// #docregion v1, http-get-v1, error-handling
.catch(this.handleError);
// #enddocregion http-get, http-get-v1
}
// #enddocregion error-handling
// #enddocregion error-handling, http-get
// #enddocregion v1
// #docregion addhero
@ -62,6 +57,7 @@ export class HeroService {
// #docregion v1
// #docregion extract-data
private extractData(res: Response) {
if (res.status < 200 || res.status >= 300) {
throw new Error('Bad response status: ' + res.status);
@ -69,12 +65,13 @@ export class HeroService {
let body = res.json();
return body.data || { };
}
// #enddocregion extract-data
// #docregion error-handling
private handleError (error: any) {
// in a real world app, we may send the error to some remote logging infrastructure
console.error(error); // log to console instead
// In a real world app, we might send the error to remote logging infrastructure
let errMsg = error.message || 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}
// #enddocregion error-handling

View File

@ -0,0 +1,32 @@
// ToH Promise Version
console.log ('Promise version');
import { Component } from 'angular2/core';
import { HTTP_PROVIDERS } from 'angular2/http';
import { HeroListComponent } from './hero-list.component.1';
import { HeroService } from './hero.service.1';
import { provide } from 'angular2/core';
import { XHRBackend } from 'angular2/http';
import { InMemoryBackendService,
SEED_DATA } from 'a2-in-memory-web-api/core';
import { HeroData } from '../hero-data';
@Component({
selector: 'my-toh',
template: `
<h1>Tour of Heroes</h1>
<hero-list></hero-list>
`,
directives:[HeroListComponent],
providers: [
HTTP_PROVIDERS,
HeroService,
// in-memory web api providers
provide(XHRBackend, { useClass: InMemoryBackendService }), // in-mem server
provide(SEED_DATA, { useClass: HeroData }) // in-mem server data
]
})
export class TohComponent { }

View File

@ -0,0 +1,44 @@
// #docplaster
// #docregion
import { Component } from 'angular2/core';
import { HTTP_PROVIDERS } from 'angular2/http';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from './hero.service';
// #enddocregion
// #docregion in-mem-web-api-imports
import { provide } from 'angular2/core';
import { XHRBackend } from 'angular2/http';
// in-memory web api imports
import { InMemoryBackendService,
SEED_DATA } from 'a2-in-memory-web-api/core';
import { HeroData } from '../hero-data';
// #enddocregion in-mem-web-api-imports
// #docregion
@Component({
selector: 'my-toh',
// #docregion template
template: `
<h1>Tour of Heroes</h1>
<hero-list></hero-list>
`,
// #enddocregion template
directives: [HeroListComponent],
providers: [
HTTP_PROVIDERS,
HeroService,
// #enddocregion
// #docregion in-mem-web-api-providers
// in-memory web api providers
provide(XHRBackend, { useClass: InMemoryBackendService }), // in-mem server
provide(SEED_DATA, { useClass: HeroData }) // in-mem server data
// #enddocregion in-mem-web-api-providers
// #docregion
]
})
export class TohComponent { }
// #enddocregion

View File

@ -1,24 +1,23 @@
// #docplaster
// #docregion
import {Component} from 'angular2/core';
import {HTTP_PROVIDERS} from 'angular2/http';
import { Component } from 'angular2/core';
import { HTTP_PROVIDERS } from 'angular2/http';
import {Hero} from './hero';
import {HeroListComponent} from './hero-list.component';
import {HeroService} from './hero.service';
//#enddocregion
import { HeroListComponent } from './hero-list.component';
import { HeroService } from './hero.service';
// #enddocregion
//#docregion in-mem-web-api-imports
import {provide} from 'angular2/core';
import {XHRBackend} from 'angular2/http';
// #docregion in-mem-web-api-imports
import { provide } from 'angular2/core';
import { XHRBackend } from 'angular2/http';
// in-memory web api imports
import {InMemoryBackendService,
SEED_DATA} from 'a2-in-memory-web-api/core';
import {HeroData} from '../hero-data';
import { InMemoryBackendService,
SEED_DATA } from 'a2-in-memory-web-api/core';
import { HeroData } from '../hero-data';
// #enddocregion in-mem-web-api-imports
//#docregion
// #docregion
@Component({
selector: 'my-toh',
@ -28,17 +27,17 @@ import {HeroData} from '../hero-data';
<hero-list></hero-list>
`,
// #enddocregion template
directives:[HeroListComponent],
providers: [
directives: [HeroListComponent],
providers: [
HTTP_PROVIDERS,
HeroService,
//#enddocregion
//#docregion in-mem-web-api-providers
// #enddocregion
// #docregion in-mem-web-api-providers
// in-memory web api providers
provide(XHRBackend, { useClass: InMemoryBackendService }), // in-mem server
provide(SEED_DATA, { useClass: HeroData }) // in-mem server data
//#enddocregion in-mem-web-api-providers
//#docregion
// #enddocregion in-mem-web-api-providers
// #docregion
]
})
export class TohComponent { }

View File

@ -36,7 +36,7 @@ export class HeroService {
}
private handleError (error: any) {
// in a real world app, we may send the error to some remote logging infrastructure
// In a real world app, we might send the error to remote logging infrastructure
let errMsg = error.message || 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);

View File

@ -21,7 +21,6 @@ include ../_util-fns
[Enabling RxJS Operators](#enable-rxjs-operators)<br>
[Extract JSON data with RxJS map](#map)<br>
[Error handling](#error-handling)<br>
[Log results to console](#do)<br>
[Send data to the server](#update)<br>
[Add headers](#headers)<br>
[Promises instead of observables](#promises)<br>
@ -67,14 +66,20 @@ figure.image-display
making them available to the child components of this "Tour of Heroes" application.
.l-sub-section
:marked
Alternatively, we may choose to add the `HTTP_PROVIDERS` while bootstrapping the app:
+makeExample('server-communication/ts/app/main.ts','http-providers','app/main.ts')(format='.')
:marked
Learn about providers in the [Dependency Injection](dependency-injection.html) chapter.
:marked
This sample only has one child, the `HeroListComponent` shown here in full:
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', null, 'app/toh/hero-list.component.ts')
This sample only has one child, the `HeroListComponent`. Here's its template:
+makeExample('server-communication/ts/app/toh/hero-list.component.html', null, 'app/toh/hero-list.component.html (Template)')
:marked
The component template displays a list of heroes with the `NgFor` repeater directive.
figure.image-display
img(src='/resources/images/devguide/server-communication/hero-list.png' alt="Hero List")
:marked
Beneath the heroes is an input box and an *Add Hero* button where we can enter the names of new heroes
and add them to the database.
We use a [local template variable](template-syntax.html#local-vars), `newHero`, to access the
@ -82,22 +87,26 @@ figure.image-display
When the user clicks the button, we pass that value to the component's `addHero` method and then
clear it to make ready for a new hero name.
Below the button is an optional error message.
Below the button is a (hidden) area for an error message.
a(id="oninit")
a(id="HeroListComponent")
:marked
### The *HeroListComponent* class
Here's the component class:
+makeExample('server-communication/ts/app/toh/hero-list.component.ts','component', 'app/toh/hero-list.component.ts (class)')
:marked
We [inject](dependency-injection.html) the `HeroService` into the constructor.
That's the instance of the `HeroService` that we provided in the parent shell `TohComponent`.
Notice that the component **does not talk to the server directly!**
The component doesn't know or care how we get the data.
Those details it delegates to the `heroService` class (which we'll get to in a moment).
This is a golden rule: *always delegate data access to a supporting service class*.
This is a golden rule: **always delegate data access to a supporting service class**.
Although the component should request heroes immediately,
we do **not** call the service `get` method in the component's constructor.
Although _at runtime_ the component requests heroes immediately after creation,
we do **not** call the service's `get` method in the component's constructor.
We call it inside the `ngOnInit` [lifecycle hook](lifecycle-hooks.html) instead
and count on Angular to call `ngOnInit` when it instantiates this component.
.l-sub-section
@ -106,8 +115,9 @@ a(id="HeroListComponent")
Components are easier to test and debug when their constructors are simple and all real work
(especially calling a remote server) is handled in a separate method.
:marked
The service `get` and `addHero` methods return an `Observable` of HTTP Responses to which we `subscribe`,
specifying the actions to take if a method succeeds or fails.
The service `get` and `addHero` methods return an `Observable` of HTTP hero data.
We subscribe to this `Observable`,
specifying the actions to take when the request succeeds or fails.
We'll get to observables and subscription shortly.
With our basic intuitions about the component squared away, we can turn to development of the backend data source
@ -122,7 +132,7 @@ a(id="HeroListComponent")
In this chapter, we get the heroes from the server using Angular's own HTTP Client service.
Here's the new `HeroService`:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'v1', 'app/toh/hero.service.ts')(format=".")
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'v1', 'app/toh/hero.service.ts')
:marked
We begin by importing Angular's `Http` client service and
[inject it](dependency-injection.html) into the `HeroService` constructor.
@ -133,7 +143,7 @@ a(id="HeroListComponent")
+makeExample('server-communication/ts/index.html', 'http', 'index.html')(format=".")
:marked
Look closely at how we call `http.get`
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get-v1', 'app/toh/hero.service.ts (http.get)')(format=".")
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get', 'app/toh/hero.service.ts (getHeroes)')(format=".")
:marked
We pass the resource URL to `get` and it calls the server which should return heroes.
.l-sub-section
@ -153,13 +163,6 @@ a(id="HeroListComponent")
In fact, the `http.get` method returns an **Observable** of HTTP Responses (`Observable<Response>`) from the RxJS library
and `map` is one of the RxJS *operators*.
.callout.is-important
header HTTP GET Delayed
:marked
The `http.get` does **not send the request just yet!** This observable is
[*cold*](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables)
which means the request won't go out until something *subscribes* to the observable.
That *something* is the [HeroListComponent](#subscribe).
.l-main-section
:marked
@ -193,15 +196,37 @@ a(id="HeroListComponent")
It's best to add that statement early when we're bootstrapping the application.
:
+makeExample('server-communication/ts/app/main.ts', 'import-rxjs', 'app/main.ts (import rxjs)')(format=".")
a(id="map")
a(id="extract-data")
:marked
<a id="map"></a>
### Map the response object
Let's come back to the `HeroService` and look at the `http.get` call again to see why we needed `map()`
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get-v1', 'app/toh/hero.service.ts (http.get)')(format=".")
### Process the response object
Remember that our `getHeroes` method mapped the `http.get` response object to heroes with an `extractData` helper method:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'extract-data', 'app/toh/hero.service.ts (extractData)')(format=".")
:marked
The `response` object does not hold our data in a form we can use directly.
It takes an additional step &mdash; calling `response.json()` &mdash; to transform the bytes from the server into a JSON object.
To make it useful in our application we must
* check for a bad response
* parse the response data into a JSON object
.alert.is-important
:marked
*Beta alert*: error status interception and parsing may be absorbed within `http` when Angular is released.
:marked
#### Bad status codes
A status code outside the 200-300 range is an error from the _application point of view_
but it is not an error from the _`http` point of view_.
For example, a `404 - Not Found` is a response like any other.
The request went out; a response came back; here it is, thank you very much.
We'd have an observable error only if `http` failed to operate (e.g., it errored internally).
Because a status code outside the 200-300 range _is an error_ from the application point of view,
we intercept it and throw, moving the observable chain to the error path.
The `catch` operator that is next in the `getHeroes` observable chain will handle our thrown error.
#### Parse to JSON
The response data are in JSON string form.
We must parse that string into JavaScript objects which we do by calling `response.json()`.
.l-sub-section
:marked
This is not Angular's own design.
@ -228,8 +253,18 @@ a(id="HeroListComponent")
It has no interest in what we do to get them.
It doesn't care where they come from.
And it certainly doesn't want to deal with a response object.
.callout.is-important
header HTTP GET is delayed
:marked
The `http.get` does **not send the request just yet!** This observable is
[*cold*](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables)
which means the request won't go out until something *subscribes* to the observable.
That *something* is the [HeroListComponent](#subscribe).
<a id="error-handling"></a>
a(id="error-handling")
:marked
### Always handle errors
The eagle-eyed reader may have spotted our use of the `catch` operator in conjunction with a `handleError` method.
@ -243,8 +278,8 @@ a(id="HeroListComponent")
In this simple app we provide rudimentary error handling in both the service and the component.
We use the Observable `catch` operator on the service level.
It takes an error handling function with the failed `Response` object as the argument.
Our service handler, `errorHandler`, logs the response to the console,
It takes an error handling function with an error object as the argument.
Our service handler, `handleError`, logs the response to the console,
transforms the error into a user-friendly message, and returns the message in a new, failed observable via `Observable.throw`.
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'error-handling', 'app/toh/hero.service.ts')(format=".")
@ -263,19 +298,7 @@ a(id="HeroListComponent")
.l-sub-section
:marked
Want to see it fail? Reset the api endpoint in the `HeroService` to a bad value. Remember to restore it!
<a id="do"></a>
<a id="console-log"></a>
:marked
### Peek at results in the console
During development we're often curious about the data returned by the server.
Logging to console without disrupting the flow would be nice.
The Observable `do` operator is perfect for the job.
It passes the input through to the output while we do something with a useful side-effect such as writing to console.
Slip it into the pipeline between `map` and `catch` like this.
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get', 'app/toh/hero.service.ts')(format=".")
:marked
Remember to comment it out before going to production!
<a id="update"></a>
<a id="post"></a>
@ -327,7 +350,7 @@ code-example(format="." language="javascript").
:marked
### JSON results
As with `get`, we extract the data from the response with `json()` and unwrap the hero via the `data` property.
As with `getHeroes`, we [extract the data](#extract-data) from the response with `json()` and unwrap the hero via the `data` property.
.alert.is-important
:marked
Know the shape of the data returned by the server.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB