docs(HTTP Client): edit copy to conform to G-doc guidelines. (#2630)

This commit is contained in:
Ward Bell 2016-10-19 23:17:50 -07:00 committed by GitHub
parent 2f5306f1b6
commit c28b003a3c
16 changed files with 400 additions and 393 deletions

View File

@ -3,7 +3,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
// #docregion import-rxjs // #docregion import-rxjs
// Add the RxJS Observable operators we need in this app. // Add the RxJS Observable operators.
import './rxjs-operators'; import './rxjs-operators';
// #enddocregion import-rxjs // #enddocregion import-rxjs

View File

@ -3,10 +3,10 @@ import { InMemoryDbService } from 'angular-in-memory-web-api';
export class HeroData implements InMemoryDbService { export class HeroData implements InMemoryDbService {
createDb() { createDb() {
let heroes = [ let heroes = [
{ id: '1', name: 'Windstorm' }, { id: 1, name: 'Windstorm' },
{ id: '2', name: 'Bombasto' }, { id: 2, name: 'Bombasto' },
{ id: '3', name: 'Magneta' }, { id: 3, name: 'Magneta' },
{ id: '4', name: 'Tornado' } { id: 4, name: 'Tornado' }
]; ];
return {heroes}; return {heroes};
} }

View File

@ -1,8 +1,8 @@
{ {
"data": [ "data": [
{ "id": "1", "name": "Windstorm" }, { "id": 1, "name": "Windstorm" },
{ "id": "2", "name": "Bombasto" }, { "id": 2, "name": "Bombasto" },
{ "id": "3", "name": "Magneta" }, { "id": 3, "name": "Magneta" },
{ "id": "4", "name": "Tornado" } { "id": 4, "name": "Tornado" }
] ]
} }

View File

@ -2,7 +2,7 @@
// import 'rxjs/Rx'; // adds ALL RxJS statics & operators to Observable // import 'rxjs/Rx'; // adds ALL RxJS statics & operators to Observable
// See node_module/rxjs/Rxjs.js // See node_module/rxjs/Rxjs.js
// Import just the rxjs statics and operators we need for THIS app. // Import just the rxjs statics and operators needed for THIS app.
// Statics // Statics
import 'rxjs/add/observable/throw'; import 'rxjs/add/observable/throw';

View File

@ -2,13 +2,10 @@
<h1>Tour of Heroes ({{mode}})</h1> <h1>Tour of Heroes ({{mode}})</h1>
<h3>Heroes:</h3> <h3>Heroes:</h3>
<ul> <ul>
<li *ngFor="let hero of heroes"> <li *ngFor="let hero of heroes">{{hero.name}}</li>
{{hero.name}}
</li>
</ul> </ul>
New hero name:
<input #newHeroName /> <label>New hero name: <input #newHeroName /></label>
<button (click)="addHero(newHeroName.value); newHeroName.value=''"> <button (click)="addHero(newHeroName.value); newHeroName.value=''">Add Hero</button>
Add Hero
</button> <p class="error" *ngIf="errorMessage">{{errorMessage}}</p>
<div class="error" *ngIf="errorMessage">{{errorMessage}}</div>

View File

@ -8,7 +8,8 @@ import { HeroService } from './hero.service.promise';
selector: 'hero-list-promise', selector: 'hero-list-promise',
moduleId: module.id, moduleId: module.id,
templateUrl: 'hero-list.component.html', templateUrl: 'hero-list.component.html',
providers: [ HeroService ] providers: [ HeroService ],
styles: ['.error {color:red;}']
}) })
// #docregion component // #docregion component
export class HeroListPromiseComponent implements OnInit { export class HeroListPromiseComponent implements OnInit {

View File

@ -8,7 +8,8 @@ import { HeroService } from './hero.service';
moduleId: module.id, moduleId: module.id,
selector: 'hero-list', selector: 'hero-list',
templateUrl: 'hero-list.component.html', templateUrl: 'hero-list.component.html',
providers: [ HeroService ] providers: [ HeroService ],
styles: ['.error {color:red;}']
}) })
// #docregion component // #docregion component
export class HeroListComponent implements OnInit { export class HeroListComponent implements OnInit {

View File

@ -9,7 +9,7 @@ import { Hero } from './hero';
@Injectable() @Injectable()
export class HeroService { export class HeroService {
// URL to web api // URL to web api
private heroesUrl = 'app/heroes.json'; private heroesUrl = 'app/heroes';
constructor (private http: Http) {} constructor (private http: Http) {}
@ -22,11 +22,10 @@ export class HeroService {
} }
addHero (name: string): Promise<Hero> { addHero (name: string): Promise<Hero> {
let body = JSON.stringify({ name });
let headers = new Headers({ 'Content-Type': 'application/json' }); let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers }); let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, body, options) return this.http.post(this.heroesUrl, { name }, options)
.toPromise() .toPromise()
.then(this.extractData) .then(this.extractData)
.catch(this.handleError); .catch(this.handleError);
@ -37,12 +36,17 @@ export class HeroService {
return body.data || { }; return body.data || { };
} }
private handleError (error: any) { private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure // In a real world app, we might use a remote logging infrastructure
// We'd also dig deeper into the error to get a better message let errMsg: string;
let errMsg = (error.message) ? error.message : if (error instanceof Response) {
error.status ? `${error.status} - ${error.statusText}` : 'Server error'; const body = error.json() || '';
console.error(errMsg); // log to console instead const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Promise.reject(errMsg); return Promise.reject(errMsg);
} }

View File

@ -34,11 +34,10 @@ export class HeroService {
// #docregion addhero, addhero-sig // #docregion addhero, addhero-sig
addHero (name: string): Observable<Hero> { addHero (name: string): Observable<Hero> {
// #enddocregion addhero-sig // #enddocregion addhero-sig
let body = JSON.stringify({ name });
let headers = new Headers({ 'Content-Type': 'application/json' }); let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers }); let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, body, options) return this.http.post(this.heroesUrl, { name }, options)
.map(this.extractData) .map(this.extractData)
.catch(this.handleError); .catch(this.handleError);
} }
@ -50,14 +49,19 @@ export class HeroService {
return body.data || { }; return body.data || { };
} }
// #enddocregion extract-data // #enddocregion extract-data
// #docregion error-handling // #docregion error-handling
private handleError (error: any) {
private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure // In a real world app, we might use a remote logging infrastructure
// We'd also dig deeper into the error to get a better message let errMsg: string;
let errMsg = (error.message) ? error.message : if (error instanceof Response) {
error.status ? `${error.status} - ${error.statusText}` : 'Server error'; const body = error.json() || '';
console.error(errMsg); // log to console instead const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg); return Observable.throw(errMsg);
} }
// #enddocregion error-handling, methods // #enddocregion error-handling, methods

View File

@ -1,3 +1,5 @@
/* tslint:disable: member-ordering forin */
// #docplaster
// #docregion // #docregion
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
@ -8,33 +10,27 @@ import { Subject } from 'rxjs/Subject';
import { WikipediaService } from './wikipedia.service'; import { WikipediaService } from './wikipedia.service';
@Component({ @Component({
moduleId: module.id,
selector: 'my-wiki-smart', selector: 'my-wiki-smart',
template: ` templateUrl: 'wiki.component.html',
<h1>Smarter Wikipedia Demo</h1> providers: [ WikipediaService ]
<p><i>Fetches when typing stops</i></p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>
`,
providers: [WikipediaService]
}) })
export class WikiSmartComponent { export class WikiSmartComponent {
title = 'Smarter Wikipedia Demo';
constructor (private wikipediaService: WikipediaService) { } fetches = 'Fetches when typing stops';
items: Observable<string[]>;
// #docregion subject // #docregion subject
private searchTermStream = new Subject<string>(); private searchTermStream = new Subject<string>();
search(term: string) { this.searchTermStream.next(term); } search(term: string) { this.searchTermStream.next(term); }
// #enddocregion subject // #enddocregion subject
constructor (private wikipediaService: WikipediaService) {
// #docregion observable-operators // #docregion observable-operators
items: Observable<string[]> = this.searchTermStream this.items = this.searchTermStream
.debounceTime(300) .debounceTime(300)
.distinctUntilChanged() .distinctUntilChanged()
.switchMap((term: string) => this.wikipediaService.search(term)); .switchMap((term: string) => this.wikipediaService.search(term));
// #enddocregion observable-operators // #enddocregion observable-operators
}
} }

View File

@ -0,0 +1,11 @@
<!-- #docregion -->
<h1>{{title}}</h1>
<p><i>{{fetches}}</i></p>
<!-- #docregion keyup -->
<input #term (keyup)="search(term.value)"/>
<!-- #enddocregion keyup -->
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>

View File

@ -5,25 +5,19 @@ import { Observable } from 'rxjs/Observable';
import { WikipediaService } from './wikipedia.service'; import { WikipediaService } from './wikipedia.service';
@Component({ @Component({
moduleId: module.id,
selector: 'my-wiki', selector: 'my-wiki',
template: ` templateUrl: 'wiki.component.html',
<h1>Wikipedia Demo</h1> providers: [ WikipediaService ]
<p><i>Fetches after each keystroke</i></p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>
`,
providers: [WikipediaService]
}) })
export class WikiComponent { export class WikiComponent {
title = 'Wikipedia Demo';
fetches = 'Fetches after each keystroke';
items: Observable<string[]>; items: Observable<string[]>;
constructor (private wikipediaService: WikipediaService) {}
search (term: string) { search (term: string) {
this.items = this.wikipediaService.search(term); this.items = this.wikipediaService.search(term);
} }
constructor (private wikipediaService: WikipediaService) { }
} }

View File

@ -6,8 +6,6 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="sample.css">
<!-- Polyfill(s) for older browsers --> <!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/core-js/client/shim.min.js"></script>

View File

@ -1 +0,0 @@
.error {color:red;}

View File

@ -108,7 +108,7 @@
"server-communication": { "server-communication": {
"title": "HTTP Client", "title": "HTTP Client",
"intro": "Talk to a remote server with an HTTP Client." "intro": "Use an HTTP Client to talk to a remote server."
}, },
"lifecycle-hooks": { "lifecycle-hooks": {

View File

@ -9,105 +9,105 @@ block includes
.l-sub-section .l-sub-section
:marked :marked
The [`WebSocket`](https://tools.ietf.org/html/rfc6455) protocol is another important communication technology; The [`WebSocket`](https://tools.ietf.org/html/rfc6455) protocol is another important communication technology;
we won't cover it in this chapter. it isn't covered in this page.
:marked :marked
Modern browsers support two HTTP-based APIs: Modern browsers support two HTTP-based APIs:
[XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) and [XMLHttpRequest (XHR)](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) and
[JSONP](https://en.wikipedia.org/wiki/JSONP). A few browsers also support [JSONP](https://en.wikipedia.org/wiki/JSONP). A few browsers also support
[Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
The !{_Angular_http_library} simplifies application programming of the **XHR** and **JSONP** APIs The !{_Angular_http_library} simplifies application programming with the **XHR** and **JSONP** APIs.
as we'll learn in this chapter covering: This page covers:
- [HTTP client sample overview](#http-client) - [The Tour of Heroes *HTTP* client demo](#http-client).
- [Fetch data with http.get](#fetch-data) - [Fetch data with http.get](#fetch-data).
<li if-docs="ts"> [RxJS Observable of HTTP Responses](#rxjs)</li> <li if-docs="ts"> [RxJS library](#rxjs).</li>
<li if-docs="ts"> [Enabling RxJS Operators](#enable-rxjs-operators)</li> <li if-docs="ts"> [Enable RxJS operators](#enable-rxjs-operators).</li>
- [Extract JSON data](#extract-data) - [Process the response object](#extract-data).
- [Error handling](#error-handling) - [Always handle errors](#error-handling).
- [Send data to the server](#update) - [Send data to the server](#update).
<li if-docs="ts"> [Promises instead of observables](#promises)</li> <li if-docs="ts"> [Fall back to promises](#promises).</li>
- [Cross-origin requests: Wikipedia example](#cors) - [Cross-origin requests: Wikipedia example](#cors).
<ul if-docs="ts"> <ul if-docs="ts">
<li> [Set query string parameters](#search-parameters)</li> <li> [Search parameters](#search-parameters).</li>
<li> [Debounce search term input](#more-observables)</li> <li> [More fun with observables](#more-observables).</li>
</ul> </ul>
- [Appendix: the in-memory web api service](#in-mem-web-api) - [Appendix: Tour of Heroes in-memory server](#in-mem-web-api).
We illustrate these topics with code that you can <live-example>run live</live-example>. A <live-example>live example</live-example> illustrates these topics.
.l-main-section .l-main-section
:marked :marked
# Demos # Demos
This chapter describes server communication with the help of the following demos This page describes server communication with the help of the following demos:
block demos-list block demos-list
:marked :marked
- [HTTP client: Tour of Heroes with Observables](#http-client) - [The Tour of Heroes *HTTP* client demo](#http-client).
- [HTTP client: Tour of Heroes with !{_Promise}s](#promises) - [Fall back to !{_Promise}s](#promises).
- [JSONP client: Wikipedia to fetch data from a service that does not support CORS](#cors) - [Cross-origin requests: Wikipedia example](#cors).
- [JSONP client: Wikipedia using observable operators to reduce server calls](#more-observables) - [More fun with observables](#more-observables).
:marked :marked
These demos are orchestrated by the root `AppComponent` The root `AppComponent` orchestrates these demos:
+makeExample('server-communication/ts/app/app.component.ts', null, 'app/app.component.ts') +makeExample('server-communication/ts/app/app.component.ts', null, 'app/app.component.ts')
+ifDocsFor('ts') +ifDocsFor('ts')
:marked :marked
There is nothing remarkable here _except_ for the import of RxJS operators. There is nothing remarkable here _except_ for the import of RxJS operators, which is
described [later](#rxjs).
+makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs')(format='.') +makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs')(format='.')
:marked
We'll talk about that [below](#rxjs) when we're ready to explore observables.
:marked
First, we have to configure our application to use server communication facilities.
.l-main-section#http-providers .l-main-section#http-providers
:marked :marked
# Providing HTTP Services # Providing HTTP services
We use the !{_Angular_Http} client to communicate with a server using a familiar HTTP request/response protocol. First, configure the application to use server communication facilities.
The !{_Angular_Http} client communicates with the server using a familiar HTTP request/response protocol.
The `!{_Http}` client is one of a family of services in the !{_Angular_http_library}. The `!{_Http}` client is one of a family of services in the !{_Angular_http_library}.
+ifDocsFor('ts') +ifDocsFor('ts')
.l-sub-section .l-sub-section
:marked :marked
SystemJS knows how to load services from the !{_Angular_http_library} when we import from the `@angular/http` module When importing from the `@angular/http` module, SystemJS knows how to load services from
because we registered that module name in the `system.config` file. the !{_Angular_http_library}
because the `systemjs.config.js` file maps to that module name.
:marked :marked
Before we can use the `!{_Http}` client , we'll have to register it as a service provider with the Dependency Injection system. Before you can use the `!{_Http}` client, you need to register it as a service provider with the dependency injection system.
.l-sub-section .l-sub-section
:marked :marked
Learn about providers in the [Dependency Injection](dependency-injection.html) chapter. Read about providers in the [Dependency Injection](dependency-injection.html) page.
:marked :marked
In this demo, we register providers by importing other NgModules to our root NgModule. Register providers by importing other NgModules to the root NgModule in `app.module.ts`.
+makeExample('server-communication/ts/app/app.module.1.ts', null, 'app/app.module.ts (v1)')(format='.') +makeExample('server-communication/ts/app/app.module.1.ts', null, 'app/app.module.ts (v1)')(format='.')
block http-providers block http-providers
:marked :marked
We begin by importing the symbols we need, most of them familiar by now. Begin by importing the necessary members.
The newcomers are the `HttpModule` and the `JsonpModule` from the !{_Angular_http_library}. The newcomers are the `HttpModule` and the `JsonpModule` from the !{_Angular_http_library}. For more information about imports and related terminology, see the [MDN reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) on the `import` statement.
We add these modules to the application by passing them to the `imports` array in our root NgModule. To add these modules to the application, pass them to the `imports` array in the root `@NgModule`.
.l-sub-section .l-sub-section
:marked :marked
We need the HttpModule to make HTTP calls. The `HttpModule` is necessary for making HTTP calls.
We don't need the JsonpModule for plain HTTP. Though the JsonpModule isn't necessary for plain HTTP,
We will demonstrate JSONP support later in this chapter. there is a JSONP demo later in this page.
We're loading its module now to save time. Loading its module now saves time.
.l-main-section#http-client .l-main-section#http-client
:marked :marked
# The Tour of Heroes _HTTP_ Client Demo # The Tour of Heroes HTTP client demo
Our first demo is a mini-version of the [tutorial](../tutorial)'s "Tour of Heroes" (ToH) application. The first demo is a mini-version of the [tutorial](../tutorial)'s "Tour of Heroes" (ToH) application.
This version gets some heroes from the server, displays them in a list, lets us add new heroes, and saves them to the server. This version gets some heroes from the server, displays them in a list, lets the user add new heroes, and saves them to the server.
We use the !{_Angular_Http} client to communicate via `XMLHttpRequest (XHR)`. The app uses the !{_Angular_Http} client to communicate via `XMLHttpRequest (XHR)`.
It works like this. It works like this:
figure.image-display figure.image-display
img(src='/resources/images/devguide/server-communication/http-toh.gif' alt="ToH mini app" width="250") img(src='/resources/images/devguide/server-communication/http-toh.gif' alt="ToH mini app" width="250")
:marked :marked
@ -115,12 +115,12 @@ figure.image-display
+makeExample('server-communication/ts/app/toh/hero-list.component.html', null, 'app/toh/hero-list.component.html (template)') +makeExample('server-communication/ts/app/toh/hero-list.component.html', null, 'app/toh/hero-list.component.html (template)')
:marked :marked
It presents the list of heroes with an `ngFor`. It presents the list of heroes with an `ngFor`.
Below the list is an input box and an *Add Hero* button where we can enter the names of new heroes Below the list is an input box and an *Add Hero* button where you can enter the names of new heroes
and add them to the database. and add them to the database.
We use a [template reference variable](template-syntax.html#ref-vars), `newHeroName`, to access the A [template reference variable](template-syntax.html#ref-vars), `newHeroName`, accesses the
value of the input box in the `(click)` event binding. value of the input box in the `(click)` event binding.
When the user clicks the button, we pass that value to the component's `addHero` method and then When the user clicks the button, that value passes to the component's `addHero` method and then
clear it to make it ready for a new hero name. the event binding clears it to make it ready for a new hero name.
Below the button is an area for an error message. Below the button is an area for an error message.
@ -134,46 +134,43 @@ a#HeroListComponent
Angular [injects](dependency-injection.html) a `HeroService` into the constructor Angular [injects](dependency-injection.html) a `HeroService` into the constructor
and the component calls that service to fetch and save data. and the component calls that service to fetch and save data.
The component **does not talk directly to the !{_Angular_Http} client**! The component **does not talk directly to the !{_Angular_Http} client**.
The component doesn't know or care how we get the data. The component doesn't know or care how it gets the data.
It delegates to the `HeroService`. It delegates to the `HeroService`.
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 _at runtime_ the component requests heroes immediately after creation, Although _at runtime_ the component requests heroes immediately after creation,
we do **not** call the service's `get` method in the component's constructor. you **don't** call the service's `get` method in the component's constructor.
We call it inside the `ngOnInit` [lifecycle hook](lifecycle-hooks.html) instead Instead, call it inside the `ngOnInit` [lifecycle hook](lifecycle-hooks.html)
and count on Angular to call `ngOnInit` when it instantiates this component. and rely on Angular to call `ngOnInit` when it instantiates this component.
.l-sub-section .l-sub-section
:marked :marked
This is a *best practice*. This is a *best practice*.
Components are easier to test and debug when their constructors are simple and all real work 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. (especially calling a remote server) is handled in a separate method.
block getheroes-and-addhero block getheroes-and-addhero
:marked :marked
The service's `getHeroes()` and `addHero()` methods return an `Observable` of hero data that the !{_Angular_Http} client fetched from the server. The service's `getHeroes()` and `addHero()` methods return an `Observable` of hero data that the !{_Angular_Http} client fetched from the server.
*Observables* are a big topic, beyond the scope of this chapter. Think of an `Observable` as a stream of events published by some source.
But we need to know a little about them to appreciate what is going on here. To listen for events in this stream, ***subscribe*** to the `Observable`.
These subscriptions specify the actions to take when the web request
We should think of an `Observable` as a stream of events published by some source.
We listen for events in this stream by ***subscribing*** to the `Observable`.
In these subscriptions we specify the actions to take when the web request
produces a success event (with the hero data in the event payload) or a fail event (with the error in the payload). produces a success event (with the hero data in the event payload) or a fail event (with the error in the payload).
:marked :marked
With our basic intuitions about the component squared away, we're ready to look inside the `HeroService`. With a basic understanding of the component, you're ready to look inside the `HeroService`.
a#HeroService a#HeroService
.l-main-section#fetch-data .l-main-section#fetch-data
:marked :marked
## Fetch data with the **HeroService** ## Fetch data with http.get
In many of our previous samples we faked the interaction with the server by In many of the previous samples the app faked the interaction with the server by
returning mock heroes in a service like this one: returning mock heroes in a service like this one:
+makeExample('toh-4/ts/app/hero.service.ts', 'just-get-heroes')(format=".") +makeExample('toh-4/ts/app/hero.service.ts', 'just-get-heroes')(format=".")
:marked :marked
In this chapter, we revise that `HeroService` to get the heroes from the server using the !{_Angular_Http} client service: You can revise that `HeroService` to get the heroes from the server using the !{_Angular_Http} client service:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'v1', 'app/toh/hero.service.ts (revised)') +makeExample('server-communication/ts/app/toh/hero.service.ts', 'v1', 'app/toh/hero.service.ts (revised)')
:marked :marked
@ -181,25 +178,25 @@ a#HeroService
[injected](dependency-injection.html) into the `HeroService` constructor. [injected](dependency-injection.html) into the `HeroService` constructor.
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'ctor') +makeExample('server-communication/ts/app/toh/hero.service.ts', 'ctor')
:marked :marked
Look closely at how we call `!{_priv}http.get` Look closely at how to call `!{_priv}http.get`:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get', 'app/toh/hero.service.ts (getHeroes)')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get', 'app/toh/hero.service.ts (getHeroes)')(format=".")
:marked :marked
We pass the resource URL to `get` and it calls the server which should return heroes. You pass the resource URL to `get` and it calls the server which returns heroes.
.l-sub-section .l-sub-section
:marked :marked
It *will* return heroes once we've set up the [in-memory web api](#in-mem-web-api) The server returns heroes once you've set up the [in-memory web api](#in-mem-web-api)
described in the appendix below. described in the appendix below.
Alternatively, we can (temporarily) target a JSON file by changing the endpoint URL: Alternatively, you can temporarily target a JSON file by changing the endpoint URL:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint-json')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint-json')(format=".")
+ifDocsFor('ts') +ifDocsFor('ts')
:marked :marked
<a id="rxjs"></a> <a id="rxjs"></a>
The return value may surprise us. If you are familiar with asynchronous methods in modern JavaScript, you might expect the `get` method to return a
Many of us who are familiar with asynchronous methods in modern JavaScript would expect the `get` method to return a
[promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
We'd expect to chain a call to `then()` and extract the heroes. You'd expect to chain a call to `then()` and extract the heroes.
Instead we're calling a `map()` method. Instead you're calling a `map()` method.
Clearly this is not a promise. Clearly this is not a promise.
In fact, the `http.get` method returns an **Observable** of HTTP Responses (`Observable<Response>`) from the RxJS library In fact, the `http.get` method returns an **Observable** of HTTP Responses (`Observable<Response>`) from the RxJS library
@ -207,60 +204,65 @@ a#HeroService
.l-main-section .l-main-section
:marked :marked
# RxJS Library # RxJS library
[RxJS](https://github.com/ReactiveX/RxJS) ("Reactive Extensions") is a 3rd party library, endorsed by Angular, [RxJS](https://github.com/ReactiveX/RxJS) ("Reactive Extensions") is a 3rd party library, endorsed by Angular,
that implements the [*asynchronous observable*](https://www.youtube.com/watch?v=UHI0AzD_WfY "Rob Wormald on observables") pattern. that implements the [*asynchronous observable*](https://www.youtube.com/watch?v=UHI0AzD_WfY "Rob Wormald on observables") pattern.
All of our Developer Guide samples have installed the RxJS npm package and loaded via `system.js` All of the Developer Guide samples have installed the RxJS npm package and loaded via `system.js`
because observables are used widely in Angular applications. because observables are used widely in Angular applications.
We certainly need it now when working with the HTTP client.
And we must take a critical extra step to make RxJS observables usable.
### Enable RxJS Operators The app needs it when working with the HTTP client.
The RxJS library is quite large. Additionally, you must take a critical extra step to make RxJS observables usable.
Size matters when we build a production application and deploy it to mobile devices.
We should include only those features that we actually need.
Accordingly, Angular exposes a stripped down version of `Observable` in the `rxjs/Observable` module, ### Enable RxJS operators
a version that lacks most of the operators including some we'd like to use here The RxJS library is large.
such as the `map` method we called above in `getHeroes`. Size matters when building a production application and deploying it to mobile devices.
You should include only necessary features.
It's up to us to add the operators we need. Accordingly, Angular exposes a stripped down version of `Observable` in the `rxjs/Observable`
module that lacks most of the operators such as the `map` method you
called above in `getHeroes`.
We could add _every_ RxJS operators with a single import statement. It's up to you to add the operators you need.
While that is the easiest thing to do, we'd pay a penalty in extended launch time and application size
because the full library is so big. We only use a few operators in our app.
Instead, we'll import each `Observable` operator and static class method, one-by-one, until we have a custom *Observable* implementation tuned You could add _every_ RxJS operator with a single import statement.
precisely to our requirements. We'll put the `import` statements in one `app/rxjs-operators.ts` file. While that is the easiest thing to do, you'd pay a penalty in extended launch time
and application size because the full library is so big.
Since this app only uses a few operators, it's better to import each `Observable`
operator and static class method, one-by-one, for a custom *Observable*
implementation tuned
precisely to the app's requirements. Put the `import` statements in one `app/rxjs-operators.ts` file.
+makeExample('server-communication/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".") +makeExample('server-communication/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".")
:marked :marked
If we forget an operator, the TypeScript compiler will warn that it's missing and we'll update this file. If you forget an operator, the TypeScript compiler warns that it's missing and you'll update this file.
.l-sub-section .l-sub-section
:marked :marked
We don't need _all_ of these particular operators in the `HeroService` &mdash; just `map`, `catch` and `throw`. The app doesn't need _all_ of these particular operators in the `HeroService` &mdash; just `map`, `catch` and `throw`.
We'll need the other operators later, in a *Wiki* example [below](#more-observables). The other operators are for later, in the *Wiki* example [below](#more-observables).
:marked :marked
Finally, we import `rxjs-operator`_itself_ in our `app.component.ts`: Finally, import `rxjs-operator` into `app.component.ts`:
+makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs', 'app/app.component.ts (import rxjs)')(format=".") +makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs', 'app/app.component.ts (import rxjs)')(format=".")
:marked :marked
Let's return to our study of the `HeroService`. Now continue to the next section to return to the `HeroService`.
.l-main-section .l-main-section
a#extract-data a#extract-data
:marked :marked
## Process the response object ## Process the response object
Remember that our `getHeroes()` method mapped the `!{_priv}http.get` response object to heroes with an `!{_priv}extractData` helper method: Remember that the `getHeroes()` method used an `!{_priv}extractData` helper method to map the `!{_priv}http.get` response object to heroes:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'extract-data', 'app/toh/hero.service.ts (excerpt)')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'extract-data', 'app/toh/hero.service.ts (excerpt)')(format=".")
:marked :marked
The `response` object does not hold our data in a form we can use directly. The `response` object doesn't hold the data in a form the app can use directly.
To make it useful in our application we must parse the response data into a JSON object You must parse the response data into a JSON object.
#### Parse to JSON ### Parse to JSON
block parse-json block parse-json
:marked :marked
The response data are in JSON string form. The response data are in JSON string form.
We must parse that string into JavaScript objects which we do by calling `response.json()`. The app must parse that string into JavaScript objects by calling `response.json()`.
.l-sub-section .l-sub-section
:marked :marked
@ -271,69 +273,66 @@ block parse-json
.l-sub-section .l-sub-section
:marked :marked
We shouldn't expect the decoded JSON to be the heroes !{_array} directly. Don't expect the decoded JSON to be the heroes !{_array} directly.
The server we're calling always wraps JSON results in an object with a `data` This server always wraps JSON results in an object with a `data`
property. We have to unwrap it to get the heroes. property. You have to unwrap it to get the heroes.
This is conventional web api behavior, driven by This is conventional web API behavior, driven by
[security concerns](https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside). [security concerns](https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside).
.alert.is-important .alert.is-important
:marked :marked
Make no assumptions about the server API. Make no assumptions about the server API.
Not all servers return an object with a `data` property. Not all servers return an object with a `data` property.
:marked :marked
### Do not return the response object ### Do not return the response object
Our `getHeroes()` could have returned the HTTP response. Bad idea! The `getHeroes()` method _could_ have returned the HTTP response but this wouldn't
be a best practice.
The point of a data service is to hide the server interaction details from consumers. The point of a data service is to hide the server interaction details from consumers.
The component that calls the `HeroService` wants heroes. The component that calls the `HeroService` only wants heroes and is kept separate
It has no interest in what we do to get them. from getting them, the code dealing with where they come from, and the response object.
It doesn't care where they come from.
And it certainly doesn't want to deal with a response object.
+ifDocsFor('ts') +ifDocsFor('ts')
.callout.is-important .callout.is-important
header HTTP GET is delayed header HTTP GET is delayed
:marked :marked
The `!{_priv}http.get` does **not send the request just yet!** This observable is The `!{_priv}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) [*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. which means that the request won't go out until something *subscribes* to the observable.
That *something* is the [HeroListComponent](#subscribe). That *something* is the [HeroListComponent](#subscribe).
a#error-handling a#error-handling
:marked :marked
### Always handle errors ### Always handle errors
Whenever we deal with I/O we must be prepared for something to go wrong as it surely will. An important part of dealing with I/O is anticipating errors by preparing to catch them
We should catch errors in the `HeroService` and do something with them. and do something with them. One way to handle errors is to pass an error message
We may also pass an error message back to the component for presentation to the user back to the component for presentation to the user,
but only if we can say something the user can understand and act upon. but only if it says something that the user can understand and act upon.
In this simple app we provide rudimentary error handling in both the service and the component. This simple app conveys that idea, albeit imperfectly, in the way it handles a `getHeroes` error.
block error-handling
:marked
The eagle-eyed reader may have spotted our use of the `catch` operator in conjunction with a `handleError` method.
We haven't discussed so far how that actually works.
We use the Observable `catch` operator on the service level.
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 (excerpt)')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'error-handling', 'app/toh/hero.service.ts (excerpt)')(format=".")
block error-handling
:marked
The `catch` operator passes the error object from `http` to the `handleError` method.
The `handleError` method transforms the error into a developer-friendly message,
logs it to the console, and returns the message in a new, failed observable via `Observable.throw`.
a#subscribe a#subscribe
a#hero-list-component a#hero-list-component
h4 #[b HeroListComponent] error handling h3 #[b HeroListComponent] error handling
block hlc-error-handling block hlc-error-handling
:marked :marked
Back in the `HeroListComponent`, where we called `!{_priv}heroService.getHeroes()`, Back in the `HeroListComponent`, in `!{_priv}heroService.getHeroes()`,
we supply the `subscribe` function with a second function parameter to handle the error message. the `subscribe` function has a second function parameter to handle the error message.
It sets an `errorMessage` variable which we've bound conditionally in the `HeroListComponent` template. It sets an `errorMessage` variable that's bound conditionally in the `HeroListComponent` template.
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'getHeroes', 'app/toh/hero-list.component.ts (getHeroes)')(format=".") +makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'getHeroes', 'app/toh/hero-list.component.ts (getHeroes)')(format=".")
.l-sub-section .l-sub-section
:marked :marked
Want to see it fail? Reset the api endpoint in the `HeroService` to a bad value. Remember to restore it! Want to see it fail? In the `HeroService`, reset the api endpoint to a bad value. Afterward, remember to restore it.
<a id="update"></a> <a id="update"></a>
@ -342,20 +341,20 @@ block hlc-error-handling
:marked :marked
## Send data to the server ## Send data to the server
So far we've seen how to retrieve data from a remote location using an HTTP service. So far you've seen how to retrieve data from a remote location using an HTTP service.
Let's add the ability to create new heroes and save them in the backend. Now you'll add the ability to create new heroes and save them in the backend.
We'll create an easy method for the `HeroListComponent` to call, an `addHero()` method that takes You'll write a method for the `HeroListComponent` to call, an `addHero()` method, that takes
just the name of a new hero: just the name of a new hero and returns an `Observable` of `Hero`. It begins like this:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero-sig')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero-sig')(format=".")
:marked :marked
To implement it, we need to know some details about the server's api for creating heroes. To implement it, you must know the server's API for creating heroes.
[Our data server](#server) follows typical REST guidelines. [This sample's data server](#server) follows typical REST guidelines.
It expects a [`POST`](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5) request It expects a [`POST`](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5) request
at the same endpoint where we `GET` heroes. at the same endpoint as `GET` heroes.
It expects the new hero data to arrive in the body of the request, It expects the new hero data to arrive in the body of the request,
structured like a `Hero` entity but without the `id` property. structured like a `Hero` entity but without the `id` property.
The body of the request should look like this: The body of the request should look like this:
@ -363,11 +362,11 @@ block hlc-error-handling
code-example(format="." language="javascript"). code-example(format="." language="javascript").
{ "name": "Windstorm" } { "name": "Windstorm" }
:marked :marked
The server will generate the `id` and return the entire `JSON` representation The server generates the `id` and returns the entire `JSON` representation
of the new hero including its generated id. The hero arrives tucked inside a response object of the new hero including its generated id. The hero arrives tucked inside a response object
with its own `data` property. with its own `data` property.
Now that we know how the API works, we implement `addHero()`like this: Now that you know how the API works, implement `addHero()`as follows:
+ifDocsFor('ts') +ifDocsFor('ts')
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'import-request-options', 'app/toh/hero.service.ts (additional imports)')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'import-request-options', 'app/toh/hero.service.ts (additional imports)')(format=".")
@ -376,248 +375,249 @@ code-example(format="." language="javascript").
:marked :marked
### Headers ### Headers
The `Content-Type` header allows us to inform the server that the body will represent JSON. In the `headers` object, the `Content-Type` specifies that the body represents JSON.
+ifDocsFor('ts') +ifDocsFor('ts')
:marked :marked
[Headers](../api/http/index/Headers-class.html) are one of the [RequestOptions](../api/http/index/RequestOptions-class.html). Next, the `headers` object is used to configure the `options` object. The `options`
Compose the options object and pass it in as the *third* parameter of the `post` method, as shown above. object is a new instance of `RequestOptions`, a class that allows you to specify
certain settings when instantiating a request. In this way, [headers](../api/http/index/Headers-class.html) is one of the [RequestOptions](../api/http/index/RequestOptions-class.html).
:marked In the `return` statement, `options` is the *third* argument of the `post` method, as shown above.
### Body
Despite the content type being specified as JSON, the POST body must actually be a *string*.
Hence, we explicitly encode the JSON hero content before passing it in as the body argument.
+ifDocsFor('ts')
.l-sub-section
:marked
We may be able to skip the `JSON.stringify` step in the near future.
:marked :marked
### JSON results ### JSON results
As with `getHeroes()`, we [extract the data](#extract-data) from the response using the As with `getHeroes()`, use the `!{_priv}extractData()` helper to [extract the data](#extract-data)
`!{_priv}extractData()` helper. from the response.
block hero-list-comp-add-hero block hero-list-comp-add-hero
:marked :marked
Back in the `HeroListComponent`, we see that *its* `addHero()` method subscribes to the observable returned by the *service's* `addHero()` method. Back in the `HeroListComponent`, *its* `addHero()` method subscribes to the observable returned by the *service's* `addHero()` method.
When the data arrives, it pushes the new hero object into its `heroes` array for presentation to the user. When the data arrive it pushes the new hero object into its `heroes` array for presentation to the user.
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'addHero', 'app/toh/hero-list.component.ts (addHero)')(format=".") +makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'addHero', 'app/toh/hero-list.component.ts (addHero)')(format=".")
+ifDocsFor('ts') +ifDocsFor('ts')
h2#promises Fall back to Promises h2#promises Fall back to promises
:marked :marked
Although the Angular `http` client API returns an `Observable<Response>` we can turn it into a Although the Angular `http` client API returns an `Observable<Response>` you can turn it into a
[`Promise<Response>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) if we prefer. [`Promise<Response>`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
It's easy to do and a promise-based version looks much like the observable-based version in simple cases. It's easy to do, and in simple cases, a promise-based version looks much
like the observable-based version.
.l-sub-section .l-sub-section
:marked :marked
While promises may be more familiar, observables have many advantages. While promises may be more familiar, observables have many advantages.
Don't rush to promises until you give observables a chance.
:marked :marked
Let's rewrite the `HeroService` using promises , highlighting just the parts that are different. Here is a comparison of the `HeroService` using promises versus observables,
highlighting just the parts that are different.
+makeTabs( +makeTabs(
'server-communication/ts/app/toh/hero.service.promise.ts,server-communication/ts/app/toh/hero.service.ts', 'server-communication/ts/app/toh/hero.service.promise.ts,server-communication/ts/app/toh/hero.service.ts',
'methods, methods', 'methods, methods',
'app/toh/hero.service.promise.ts (promise-based), app/toh/hero.service.ts (observable-based)') 'app/toh/hero.service.promise.ts (promise-based), app/toh/hero.service.ts (observable-based)')
:marked :marked
Converting from an observable to a promise is as simple as calling `toPromise(success, fail)`. You can follow the promise `then(this.extractData).catch(this.handleError)` pattern as in
this example.
We move the observable's `map` callback to the first *success* parameter and its `catch` callback to the second *fail* parameter Alternatively, you can call `toPromise(success, fail)`. The observable's `map` callback moves to the first *success* parameter and its `catch` callback to the second *fail* parameter in this pattern: `.toPromise(this.extractData, this.handleError)`.
and we're done!
Or we can follow the promise `then.catch` pattern as we do in the second `addHero` example.
Our `errorHandler` forwards an error message as a failed promise instead of a failed Observable. The `errorHandler` forwards an error message as a failed promise instead of a failed `observable`.
The diagnostic *log to console* is just one more `then` in the promise chain. The diagnostic *log to console* is just one more `then` in the promise chain.
We have to adjust the calling component to expect a `Promise` instead of an `Observable`. You have to adjust the calling component to expect a `Promise` instead of an `observable`:
+makeTabs( +makeTabs(
'server-communication/ts/app/toh/hero-list.component.promise.ts, server-communication/ts/app/toh/hero-list.component.ts', 'server-communication/ts/app/toh/hero-list.component.promise.ts, server-communication/ts/app/toh/hero-list.component.ts',
'methods, methods', 'methods, methods',
'app/toh/hero-list.component.promise.ts (promise-based), app/toh/hero-list.component.ts (observable-based)') 'app/toh/hero-list.component.promise.ts (promise-based), app/toh/hero-list.component.ts (observable-based)')
:marked :marked
The only obvious difference is that we call `then` on the returned promise instead of `subscribe`. The only obvious difference is that you call `then` on the returned promise instead of `subscribe`.
We give both methods the same functional arguments. Both methods take the same functional arguments.
.l-sub-section .l-sub-section
:marked :marked
The less obvious but critical difference is that these two methods return very different results! The less obvious but critical difference is that these two methods return very different results.
The promise-based `then` returns another promise. We can keep chaining more `then` and `catch` calls, getting a new promise each time. The promise-based `then` returns another promise. You can keep chaining more `then` and `catch` calls, getting a new promise each time.
The `subscribe` method returns a `Subscription`. A `Subscription` is not another `Observable`. The `subscribe` method returns a `Subscription`. A `Subscription` is not another `Observable`.
It's the end of the line for observables. We can't call `map` on it or call `subscribe` again. It's the end of the line for observables. You can't call `map` on it or call `subscribe` again.
The `Subscription` object has a different purpose, signified by its primary method, `unsubscribe`. The `Subscription` object has a different purpose, signified by its primary method, `unsubscribe`.
Learn more about observables to understand the implications and consequences of subscriptions. To understand the implications and consequences of subscriptions, watch [Ben Lesh's talk on observables](https://www.youtube.com/watch?v=3LKMwkuK0ZE) or his video course on [egghead.io](https://egghead.io/lessons/rxjs-rxjs-observables-vs-promises).
h2#cors Cross-origin requests: Wikipedia example h2#cors Cross-origin requests: Wikipedia example
:marked :marked
We just learned how to make `XMLHttpRequests` using the !{_Angular_Http} service. You just learned how to make `XMLHttpRequests` using the !{_Angular_Http} service.
This is the most common approach for server communication. This is the most common approach for server communication, but it doesn't work in all scenarios.
It doesn't work in all scenarios.
For security reasons, web browsers block `XHR` calls to a remote server whose origin is different from the origin of the web page. For security reasons, web browsers block `XHR` calls to a remote server whose origin is different from the origin of the web page.
The *origin* is the combination of URI scheme, hostname and port number. The *origin* is the combination of URI scheme, hostname, and port number.
This is called the [Same-origin Policy](https://en.wikipedia.org/wiki/Same-origin_policy). This is called the [same-origin policy](https://en.wikipedia.org/wiki/Same-origin_policy).
.l-sub-section .l-sub-section
:marked :marked
Modern browsers do allow `XHR` requests to servers from a different origin if the server supports the Modern browsers do allow `XHR` requests to servers from a different origin if the server supports the
[CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) protocol. [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) protocol.
If the server requires user credentials, we'll enable them in the [request headers](#headers). If the server requires user credentials, you'll enable them in the [request headers](#headers).
:marked :marked
Some servers do not support CORS but do support an older, read-only alternative called [JSONP](https://en.wikipedia.org/wiki/JSONP). Some servers do not support CORS but do support an older, read-only alternative called [JSONP](https://en.wikipedia.org/wiki/JSONP).
Wikipedia is one such server. Wikipedia is one such server.
.l-sub-section .l-sub-section
:marked :marked
This [StackOverflow answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) covers many details of JSONP. This [Stack Overflow answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) covers many details of JSONP.
:marked :marked
### Search wikipedia ### Search wikipedia
Let's build a simple search that shows suggestions from wikipedia as we type in a text box. Here is a simple search that shows suggestions from Wikipedia as the user
types in a text box:
figure.image-display figure.image-display
img(src='/resources/images/devguide/server-communication/wiki-1.gif' alt="Wikipedia search app (v.1)" width="250") img(src='/resources/images/devguide/server-communication/wiki-1.gif' alt="Wikipedia search app (v.1)" width="250")
block wikipedia-jsonp+ block wikipedia-jsonp+
:marked :marked
Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API. Let's use the latter for this example. Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API. This example uses the latter.
The Angular `Jsonp` service both extends the `!{_Http}` service for JSONP and restricts us to `GET` requests. The Angular `Jsonp` service both extends the `!{_Http}` service for JSONP and restricts you to `GET` requests.
All other HTTP methods throw an error because JSONP is a read-only facility. All other HTTP methods throw an error because JSONP is a read-only facility.
As always, we wrap our interaction with an Angular data access client service inside a dedicated service, here called `WikipediaService`. As always, wrap the interaction with an Angular data access client service inside a dedicated service, here called `WikipediaService`.
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts',null,'app/wiki/wikipedia.service.ts') +makeExample('server-communication/ts/app/wiki/wikipedia.service.ts',null,'app/wiki/wikipedia.service.ts')
:marked :marked
The constructor expects Angular to inject its `jsonp` service. The constructor expects Angular to inject its `jsonp` service, which
We made that service available by importing the `JsonpModule` into our root NgModule. is available because `JsonpModule` is in the root `@NgModule` `imports` array
in `app.module.ts`.
<a id="query-parameters"></a> <a id="query-parameters"></a>
:marked :marked
### Search parameters ### Search parameters
The [Wikipedia 'opensearch' API](https://www.mediawiki.org/wiki/API:Opensearch) The [Wikipedia "opensearch" API](https://www.mediawiki.org/wiki/API:Opensearch)
expects four parameters (key/value pairs) to arrive in the request URL's query string. expects four parameters (key/value pairs) to arrive in the request URL's query string.
The keys are `search`, `action`, `format`, and `callback`. The keys are `search`, `action`, `format`, and `callback`.
The value of the `search` key is the user-supplied search term to find in Wikipedia. The value of the `search` key is the user-supplied search term to find in Wikipedia.
The other three are the fixed values "opensearch", "json", and "JSONP_CALLBACK" respectively. The other three are the fixed values "opensearch", "json", and "JSONP_CALLBACK" respectively.
.l-sub-section .l-sub-section
:marked :marked
The `JSONP` technique requires that we pass a callback function name to the server in the query string: `callback=JSONP_CALLBACK`. The `JSONP` technique requires that you pass a callback function name to the server in the query string: `callback=JSONP_CALLBACK`.
The server uses that name to build a JavaScript wrapper function in its response which Angular ultimately calls to extract the data. The server uses that name to build a JavaScript wrapper function in its response, which Angular ultimately calls to extract the data.
All of this happens under the hood. All of this happens under the hood.
:marked :marked
If we're looking for articles with the word "Angular", we could construct the query string by hand and call `jsonp` like this: If you're looking for articles with the word "Angular", you could construct the query string by hand and call `jsonp` like this:
+makeExample('server-communication/ts/app/wiki/wikipedia.service.1.ts','query-string')(format='.') +makeExample('server-communication/ts/app/wiki/wikipedia.service.1.ts','query-string')(format='.')
:marked :marked
In more parameterized examples we might prefer to build the query string with the Angular `URLSearchParams` helper as shown here: In more parameterized examples you could build the query string with the Angular `URLSearchParams` helper:
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','search-parameters','app/wiki/wikipedia.service.ts (search parameters)')(format=".") +makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','search-parameters','app/wiki/wikipedia.service.ts (search parameters)')(format=".")
:marked :marked
This time we call `jsonp` with *two* arguments: the `wikiUrl` and an options object whose `search` property is the `params` object. This time you call `jsonp` with *two* arguments: the `wikiUrl` and an options object whose `search` property is the `params` object.
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','call-jsonp','app/wiki/wikipedia.service.ts (call jsonp)')(format=".") +makeExample('server-communication/ts/app/wiki/wikipedia.service.ts','call-jsonp','app/wiki/wikipedia.service.ts (call jsonp)')(format=".")
:marked :marked
`Jsonp` flattens the `params` object into the same query string we saw earlier before putting the request on the wire. `Jsonp` flattens the `params` object into the same query string you saw earlier, putting the request on the wire.
<a id="wikicomponent"></a> <a id="wikicomponent"></a>
:marked :marked
### The WikiComponent ### The WikiComponent
Now that we have a service that can query the Wikipedia API, Now that you have a service that can query the Wikipedia API
we turn to the component that takes user input and displays search results. turn to the component (template and class) that takes user input and displays search results.
+makeExample('server-communication/ts/app/wiki/wiki.component.html', null, 'app/wiki/wiki.component.html')
+makeExample('server-communication/ts/app/wiki/wiki.component.ts', null, 'app/wiki/wiki.component.ts') +makeExample('server-communication/ts/app/wiki/wiki.component.ts', null, 'app/wiki/wiki.component.ts')
:marked :marked
The component presents an `<input>` element *search box* to gather search terms from the user. The template presents an `<input>` element *search box* to gather search terms from the user,
and calls a `search(term)` method after each `keyup` event. and calls a `search(term)` method after each `keyup` event.
+makeExample('server-communication/ts/app/wiki/wiki.component.html', 'keyup', 'wiki/wiki.component.html')(format='.')
The `search(term)` method delegates to our `WikipediaService` which returns an observable array of string results (`Observable<string[]>`). :marked
Instead of subscribing to the observable inside the component as we did in the `HeroListComponent`, The component's `search(term)` method delegates to the `WikipediaService`, which returns an
we forward the observable result to the template (via `items`) where the [async pipe](pipes.html#async-pipe) observable array of string results (`Observable<string[]>`).
in the `ngFor` handles the subscription. Instead of subscribing to the observable inside the component, as in the `HeroListComponent`,
the app forwards the observable result to the template (via `items`) where the `async` pipe
in the `ngFor` handles the subscription. Read more about [async pipes](pipes.html#async-pipe)
in the [Pipes](pipes.html) page.
.l-sub-section .l-sub-section
:marked :marked
We often use the [async pipe](pipes.html#async-pipe) in read-only components where the component has no need to interact with the data. The [async pipe](pipes.html#async-pipe) is a good choice in read-only components where the component has no need to interact with the data.
We couldn't use the pipe in the `HeroListComponent` because the "add hero" feature pushes newly created heroes into the list.
`HeroListComponent` can't use the pipe because `addHero()` pushes newly created heroes into the list.
:marked :marked
## Our wasteful app ## A wasteful app
Our wikipedia search makes too many calls to the server. The wikipedia search makes too many calls to the server.
It is inefficient and potentially expensive on mobile devices with limited data plans. It is inefficient, and potentially expensive on mobile devices with limited data plans.
### 1. Wait for the user to stop typing ### 1. Wait for the user to stop typing
At the moment we call the server after every key stroke. Presently, the code calls the server after every key stroke.
The app should only make requests when the user *stops typing* . It should only make requests when the user *stops typing* .
Here's how it *should* work &mdash; and *will* work &mdash; when we're done refactoring: Here's how it will work after refactoring:
figure.image-display figure.image-display
img(src='/resources/images/devguide/server-communication/wiki-2.gif' alt="Wikipedia search app (v.2)" width="250") img(src='/resources/images/devguide/server-communication/wiki-2.gif' alt="Wikipedia search app (v.2)" width="250")
:marked :marked
### 2. Search when the search term changes ### 2. Search when the search term changes
Suppose the user enters the word *angular* in the search box and pauses for a while. Suppose a user enters the word *angular* in the search box and pauses for a while.
The application issues a search request for *Angular*. The application issues a search request for *angular*.
Then the user backspaces over the last three letters, *lar*, and immediately re-types *lar* before pausing once more. Then the user backspaces over the last three letters, *lar*, and immediately re-types *lar* before pausing once more.
The search term is still "angular". The app shouldn't make another request. The search term is still _angular_. The app shouldn't make another request.
### 3. Cope with out-of-order responses ### 3. Cope with out-of-order responses
The user enters *angular*, pauses, clears the search box, and enters *http*. The user enters *angular*, pauses, clears the search box, and enters *http*.
The application issues two search requests, one for *angular* and one for *http*. The application issues two search requests, one for *angular* and one for *http*.
Which response will arrive first? We can't be sure. Which response arrives first? It's unpredictable.
A load balancer could dispatch the requests to two different servers with different response times. A load balancer could dispatch the requests to two different servers with different response times.
The results from the first *angular* request might arrive after the later *http* results. The results from the first *angular* request might arrive after the later *http* results.
The user will be confused if we display the *angular* results to the *http* query. The user will be confused if the *angular* results display to the *http* query.
When there are multiple requests in-flight, the app should present the responses When there are multiple requests in-flight, the app should present the responses
in the original request order. That won't happen if *angular* results arrive last. in the original request order. That won't happen if *angular* results arrive last.
<a id="more-observables"></a> <a id="more-observables"></a>
## More fun with Observables ## More fun with observables
We can address these problems and improve our app with the help of some nifty observable operators. You can address these problems and improve the app with the help of some nifty observable operators.
We could make our changes to the `WikipediaService`. You could make changes to the `WikipediaService`, but for a better
But we sense that our concerns are driven by the user experience so we update the component class instead. user experience, create a copy of the `WikiComponent` instead and make it smarter.
Here's the `WikiSmartComponent` which uses the same template.
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', null, 'app/wiki/wiki-smart.component.ts') +makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', null, 'app/wiki/wiki-smart.component.ts')
:marked :marked
We made no changes to the template or metadata, confining them all to the component class.
Let's review those changes.
### Create a stream of search terms ### Create a stream of search terms
We're binding to the search box `keyup` event and calling the component's `search` method after each keystroke. The template still binds to the search box `keyup` event and passes the complete search box value
into the component's `search` method after every user keystroke.
We turn these events into an observable stream of search terms using a `Subject` +makeExample('server-communication/ts/app/wiki/wiki.component.html', 'keyup', 'app/wiki/wiki.component.html (input)')(format='.')
which we import from the RxJS observable library:
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'import-subject')
:marked :marked
Each search term is a string, so we create a new `Subject` of type `string` called `searchTermStream`. The `WikiSmartComponent` turns the search box values into an observable _stream of search terms_
After every keystroke, the `search` method adds the search box value to that stream with the help of a `Subject` which you import from the RxJS observable library:
via the subject's `next` method. +makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'import-subject', 'app/wiki/wiki-smart.component.ts')
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'subject')(format='.') :marked
The component creates a `searchTermStream` as a `Subject` of type `string`.
The `search` method adds each new search box value to that stream via the subject's `next` method.
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'subject', 'app/wiki/wiki-smart.component.ts')(format='.')
:marked :marked
### Listen for search terms ### Listen for search terms
Earlier, we passed each search term directly to the service and bound the template to the service results. Earlier, you passed each search term directly to the service and bound the template to the service results.
Now we listen to the *stream of terms*, manipulating the stream before it reaches the `WikipediaService`.
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'observable-operators')(format='.') Now you listen to the *stream of search terms*, manipulating the stream before it reaches the `WikipediaService`.
+makeExample('server-communication/ts/app/wiki/wiki-smart.component.ts', 'observable-operators',
'app/wiki/wiki-smart.component.ts')(format='.')
:marked :marked
We wait for the user to stop typing for at least 300 milliseconds Wait for the user to stop typing for at least 300 milliseconds
([debounceTime](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/debounce.md)). ([_debounceTime_](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/debounce.md)).
Only changed search values make it through to the service Only changed search values make it through to the service
([distinctUntilChanged](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/distinctuntilchanged.md)). ([_distinctUntilChanged_](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/distinctuntilchanged.md)).
The `WikipediaService` returns a separate observable of string arrays (`Observable<string[]>`) for each request. The `WikipediaService` returns a separate observable of string arrays (`Observable<string[]>`) for each request.
We could have multiple requests *in flight*, all awaiting the server's reply, There could be multiple requests *in-flight*, all awaiting the server's reply,
which means multiple *observables-of-strings* could arrive at any moment in any order. which means multiple *observables-of-strings* could arrive at any moment in any order.
The [switchMap](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) The [_switchMap_](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)
(formerly known as `flatMapLatest`) returns a new observable that combines these `WikipediaService` observables, (formerly known as `flatMapLatest`) returns a new observable that combines these `WikipediaService` observables,
re-arranges them in their original request order, re-arranges them in their original request order,
and delivers to subscribers only the most recent search results. and delivers to subscribers only the most recent search results.
@ -625,57 +625,56 @@ block wikipedia-jsonp+
The displayed list of search results stays in sync with the user's sequence of search terms. The displayed list of search results stays in sync with the user's sequence of search terms.
.l-sub-section .l-sub-section
:marked :marked
We added the `debounceTime`, `distinctUntilChanged`, and `switchMap` operators to the RxJS `Observable` class You added the `debounceTime`, `distinctUntilChanged`, and `switchMap` operators to the RxJS `Observable` class
in `rxjs-operators` as [described above](#rxjs) in `rxjs-operators` as [described above](#rxjs).
a#in-mem-web-api a#in-mem-web-api
.l-main-section .l-main-section
:marked :marked
## Appendix: Tour of Heroes in-memory server ## Appendix: Tour of Heroes in-memory server
If we only cared to retrieve data, we could tell Angular to get the heroes from a `heroes.json` file like this one: If the app only needed to retrieve data, you could get the heroes from a `heroes.json` file:
+makeJson('server-communication/ts/app/heroes.json', null, 'app/heroes.json')(format=".") +makeJson('server-communication/ts/app/heroes.json', null, 'app/heroes.json')(format=".")
.l-sub-section .l-sub-section
:marked :marked
We wrap the heroes array in an object with a `data` property for the same reason that a data server does: You wrap the heroes array in an object with a `data` property for the same reason that a data server does:
to mitigate the [security risk](http://stackoverflow.com/questions/3503102/what-are-top-level-json-arrays-and-why-are-they-a-security-risk) to mitigate the [security risk](http://stackoverflow.com/questions/3503102/what-are-top-level-json-arrays-and-why-are-they-a-security-risk)
posed by top-level JSON arrays. posed by top-level JSON arrays.
:marked :marked
We'd set the endpoint to the JSON file like this: You'd set the endpoint to the JSON file like this:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint-json')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint-json', 'app/toh/hero.service.ts')(format=".")
- var _a_ca_class_with = _docsFor === 'ts' ? 'a custom application class with' : '' - var _a_ca_class_with = _docsFor === 'ts' ? 'a custom application class with' : ''
:marked :marked
The *get heroes* scenario would work. The *get heroes* scenario would work,
But we want to *save* data too. We can't save changes to a JSON file. We need a web API server. but since the app can't save changes to a JSON file, it needs a web API server.
We didn't want the hassle of setting up and maintaining a real server for this chapter. Because there isn't a real server for this demo,
So we turned to an *in-memory web API simulator* instead. it uses an *in-memory web API simulator* instead.
.l-sub-section .l-sub-section
:marked :marked
The in-memory web api is not part of the Angular core. The in-memory web api is not part of the Angular core.
It's an optional service in its own `angular-in-memory-web-api` library It's an optional service in its own `angular-in-memory-web-api` library
that we installed with npm (see `package.json`) and installed with npm (see `package.json`) and
registered for module loading by SystemJS (see `systemjs.config.js`) registered for module loading by SystemJS (see `systemjs.config.js`).
:marked :marked
The in-memory web API gets its data from !{_a_ca_class_with} a `createDb()` The in-memory web API gets its data from !{_a_ca_class_with} a `createDb()`
method that returns a map whose keys are collection names and whose values method that returns a map whose keys are collection names and whose values
are !{_array}s of objects in those collections. are !{_array}s of objects in those collections.
Here's the class we created for this sample based on the JSON data: Here's the class for this sample, based on the JSON data:
+makeExample('server-communication/ts/app/hero-data.ts', null, 'app/hero-data.ts')(format=".") +makeExample('server-communication/ts/app/hero-data.ts', null, 'app/hero-data.ts')(format=".")
:marked :marked
Ensure that the `HeroService` endpoint refers to the web API: Ensure that the `HeroService` endpoint refers to the web API:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint')(format=".") +makeExample('server-communication/ts/app/toh/hero.service.ts', 'endpoint', 'app/toh/hero.service.ts')(format=".")
:marked
Finally, redirect client HTTP requests to the in-memory web API.
block redirect-to-web-api block redirect-to-web-api
:marked :marked
This redirection is easy to configure with the in-memory web API service module Finally, redirect client HTTP requests to the in-memory web API by
by adding the `InMemoryWebApiModule` to the `AppModule.imports` list. adding the `InMemoryWebApiModule` to the `AppModule.imports` list.
At the same time, we're calling its `forRoot` configuration method with the `HeroData` class. At the same time, call its `forRoot` configuration method with the `HeroData` class.
+makeExample('server-communication/ts/app/app.module.ts', 'in-mem-web-api')(format=".") +makeExample('server-communication/ts/app/app.module.ts', 'in-mem-web-api', 'app/app.module.ts')(format=".")
:marked :marked
### How it works ### How it works
@ -684,15 +683,18 @@ block redirect-to-web-api
Using standard Angular provider registration techniques, the `InMemoryWebApiModule` Using standard Angular provider registration techniques, the `InMemoryWebApiModule`
replaces the default `XHRBackend` service with its own in-memory alternative. replaces the default `XHRBackend` service with its own in-memory alternative.
The `forRoot` method initialize the in-memory web API with *seed data* from the mock hero dataset at the same time. At the same time, the `forRoot` method initializes the in-memory web API with the *seed data* from the mock hero dataset.
.l-sub-section .l-sub-section
:marked :marked
The `forRoot` method name is a strong reminder that you should only call the `InMemoryWebApiModule` _once_ The `forRoot` method name is a strong reminder that you should only call the `InMemoryWebApiModule` _once_,
while setting the metadata for the root `AppModule`. Don't call it again!. while setting the metadata for the root `AppModule`. Don't call it again.
:marked :marked
Here is the revised (and final) version of <span ngio-ex>app/app.module.ts</span> demonstrating these steps. Here is the final, revised version of <span ngio-ex>app/app.module.ts</span>, demonstrating these steps.
+makeExcerpt('app/app.module.ts') +makeExcerpt('app/app.module.ts')
.alert.is-important
:marked
Import the `InMemoryWebApiModule` _after_ the `HttpModule` to ensure that
the `XHRBackend` provider of the `InMemoryWebApiModule` supersedes all others.
:marked :marked
See the full source code in the <live-example></live-example>. See the full source code in the <live-example></live-example>.