docs(guide/server-communication): adds http chapter

closes #662
This commit is contained in:
Christoph Burgdorf 2015-12-01 12:15:14 +01:00 committed by Ward Bell
parent f18d2a1b07
commit 77db0755c8
26 changed files with 844 additions and 2 deletions

View File

@ -24,7 +24,10 @@
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.0",
"zone.js": "0.5.10",
"a2-in-memory-web-api": "^0.0.4",
"bootstrap": "^3.3.6"
},
"devDependencies": {
"concurrently": "^1.0.0",

View File

@ -0,0 +1 @@
**/*.js

View File

@ -0,0 +1,12 @@
// #docregion
export class HeroData {
createDb() {
let heroes = [
{ "id": "1", "name": "Windstorm" },
{ "id": "2", "name": "Bombasto" },
{ "id": "3", "name": "Magneta" },
{ "id": "4", "name": "Tornado" }
];
return {heroes};
}
}

View File

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

View File

@ -0,0 +1,15 @@
//#docregion
import {bootstrap} from 'angular2/platform/browser';
// #docregion import-rxjs
// Add all operators to Observable
import 'rxjs/Rx';
// #enddocregion import-rxjs
import {WikiComponent} from './wiki/wiki.component';
import {WikiFormComponent} from './wiki/wiki-form.component';
import {TohComponent} from './toh/toh.component';
bootstrap(WikiComponent);
bootstrap(WikiFormComponent);
bootstrap(TohComponent);

View File

@ -0,0 +1,51 @@
// #docregion
import {Component, OnInit} from 'angular2/core';
import {Hero} from './hero';
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>
`,
// #enddocregion template
})
// #docregion component
export class HeroListComponent implements OnInit {
constructor (private _heroService: HeroService) {}
heroes:Hero[];
// #docregion ngOnInit
ngOnInit() {
this._heroService.getHeroes()
.then(
heroes => this.heroes = heroes,
error => alert(`Server error. Try again later`));
}
// #enddocregion ngOnInit
// #docregion addHero
addHero (name: string) {
if (!name) {return;}
this._heroService.addHero(name)
.then(
hero => this.heroes.push(hero),
error => alert(error));
}
// #enddocregion addHero
}
// #enddocregion component

View File

@ -0,0 +1,49 @@
// #docregion
import {Component, OnInit} from 'angular2/core';
import {Hero} from './hero';
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>
`,
})
// #docregion component
export class HeroListComponent implements OnInit {
constructor (private _heroService: HeroService) {}
heroes:Hero[];
// #docregion ngOnInit
ngOnInit() {
this._heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => alert(`Server error. Try again later`));
}
// #enddocregion ngOnInit
// #docregion addHero
addHero (name: string) {
if (!name) {return;}
this._heroService.addHero(name)
.subscribe(
hero => this.heroes.push(hero),
error => alert(error));
}
// #enddocregion addHero
}
// #enddocregion component

View File

@ -0,0 +1,37 @@
/* Promise version */
// #docplaster
// #docregion
// #docregion v1
import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
import {Hero} from './hero';
@Injectable()
export class HeroService {
constructor (private http: Http) {}
private _heroesUrl = 'app/heroes.json';
getHeroes () {
// TODO: Error handling
// #docregion http-get
return this.http.get(this._heroesUrl)
.toPromise()
.then(res => <Hero[]> res.json().data);
// #enddocregion http-get
}
// #enddocregion v1
// #docregion addhero
addHero (name: string) : Promise<Hero> {
// TODO: Error handling
return this.http.post(this._heroesUrl, JSON.stringify({ name }))
.toPromise()
.then(res => <Hero> res.json().data);
}
// #enddocregion addhero
// #docregion v1
}
// #enddocregion v1
// #enddocregion

View File

@ -0,0 +1,46 @@
// #docplaster
// #docregion
// #docregion v1
import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
import {Hero} from './hero';
import {Observable} from 'rxjs/Observable';
@Injectable()
export class HeroService {
constructor (private http: Http) {}
private _heroesUrl = 'app/heroes.json';
// #docregion error-handling
getHeroes () {
// #docregion http-get
return this.http.get(this._heroesUrl)
.map(res => <Hero[]> res.json().data)
.catch(this.logAndPassOn);
// #enddocregion http-get
}
// #docregion logAndPassOn
private logAndPassOn (error: Error) {
// in a real world app, we may send the server to some remote logging infrastructure
// instead of just logging it to the console
console.error(error);
return Observable.throw(error);
}
// #enddocregion logAndPassOn
// #enddocregion error-handling
// #enddocregion v1
// #docregion addhero
addHero (name: string) : Observable<Hero> {
return this.http.post(this._heroesUrl, JSON.stringify({ name }))
.map(res => <Hero> res.json().data)
.catch(this.logAndPassOn)
}
// #enddocregion addhero
// #docregion v1
}
// #enddocregion v1
// #enddocregion

View File

@ -0,0 +1,6 @@
// #docregion
export class Hero {
constructor(
public id:number,
public name:string) { }
}

View File

@ -0,0 +1,45 @@
// #docplaster
// #docregion
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
//#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

@ -0,0 +1,37 @@
// #docregion
import {Component, OnInit} from 'angular2/core';
import {Control} from 'angular2/common';
import {Observable} from 'rxjs/Observable';
import {JSONP_PROVIDERS, Jsonp, URLSearchParams} from 'angular2/http';
import {WikipediaService} from './wikipedia.service';
@Component({
selector: 'my-wiki-form',
template: `
<h1>Wikipedia Form Demo</h1>
<p><i>Fetches when typing stops</i></p>
<input [ngFormControl]="inputs"/>
<ul>
<li *ngFor="#item of items | async">{{item}}</li>
</ul>
`,
providers:[JSONP_PROVIDERS, WikipediaService]
})
export class WikiFormComponent implements OnInit {
constructor (private _wikipediaService: WikipediaService) {}
items: Observable<string>;
// #docregion control
inputs = new Control();
// #enddocregion control
ngOnInit() {
// #docregion distinctdebounce
this.items = this.inputs.valueChanges
.debounceTime(300)
.distinctUntilChanged()
// #enddocregion distinctdebounce
.switchMap((term:string) => this._wikipediaService.search(term));
}
}

View File

@ -0,0 +1,28 @@
// #docregion
import {Component} from 'angular2/core';
import {JSONP_PROVIDERS} from 'angular2/http';
import {Observable} from 'rxjs/Observable';
import {WikipediaService} from './wikipedia.service';
@Component({
selector: 'my-wiki',
template: `
<h1>Wikipedia Demo</h1>
<p><i>Fetches after each keystroke</i></p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="#item of items | async">{{item}}</li>
</ul>
`,
providers:[JSONP_PROVIDERS, WikipediaService]
})
export class WikiComponent {
constructor (private _wikipediaService: WikipediaService) {}
items: Observable<string>;
search (term: string) {
this.items = this._wikipediaService.search(term);
}
}

View File

@ -0,0 +1,23 @@
// #docregion
import {Injectable} from 'angular2/core';
import {Jsonp, URLSearchParams} from 'angular2/http';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) {}
search (term: string) {
var params = new URLSearchParams();
params.set('search', term);
params.set('action', 'opensearch');
params.set('format', 'json');
let wikiUrl = 'http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK';
// TODO: Error handling
return this.jsonp
.get(wikiUrl, { search: params })
.map(request => request.json()[1]);
}
}

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<title>Angular 2 Http Demo</title>
<!-- IE required polyfills, in this exact order -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<!-- #docregion rxjs -->
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<!-- #enddocregion rxjs -->
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<!-- #docregion http -->
<script src="node_modules/angular2/bundles/http.dev.js"></script>
<!-- #enddocregion http -->
<!-- #docregion in-mem-web-api -->
<script src="node_modules/a2-in-memory-web-api/web-api.js"></script>
<!-- #enddocregion in-mem-web-api -->
<script>
System.config({
packages: {
app: {
format: 'register',
defaultExtension: 'js'
}
}
});
System.import('app/main')
.then(null, console.error.bind(console));
</script>
</head>
<body>
<my-toh>ToH Loading...</my-toh>
<my-wiki>Wiki Loading...</my-wiki>
<my-wiki-form>Wikiform Loading...</my-wiki-form>
</body>
</html>

View File

@ -0,0 +1,9 @@
{
"description": "Http",
"files":[
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1].*"
],
"tags": ["http"]
}

View File

@ -1,5 +1,6 @@
// #docplaster
// #docregion
// #docregion just-get-heroes
import {Hero} from './hero';
import {HEROES} from './mock-heroes';
import {Injectable} from 'angular2/core';
@ -11,7 +12,7 @@ export class HeroService {
return Promise.resolve(HEROES);
}
//#enddocregion get-heroes
// #enddocregion just-get-heroes
// See the "Take it slow" appendix
//#docregion get-heroes-slowly
getHeroesSlowly() {
@ -20,5 +21,7 @@ export class HeroService {
);
}
//#enddocregion get-heroes-slowly
// #docregion just-get-heroes
}
// #enddocregion just-get-heroes
// #enddocregion

View File

@ -48,6 +48,11 @@
"intro": "Discover the basics of screen navigation with the Angular 2 router."
},
"server-communication": {
"title": "Server Communication",
"intro": "Learn to build applications that talk to a server."
},
"lifecycle-hooks": {
"title": "Lifecycle Hooks",
"intro": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."

View File

@ -193,6 +193,7 @@ figure.image-display
Pipes that retrieve or request data should be used cautiously, since working with network data tends to introduce error conditions that are better handled in JavaScript than in a template.
We can mitigate this risk by creating a custom pipe for a particular backend and bake-in the essential error-handling.
<a id="async-pipe"></a>
## The stateful `AsyncPipe`
The Angular Async pipe is a remarkable example of a stateful pipe.
The Async pipe can receive a Promise or Observable as input

View File

@ -0,0 +1,412 @@
include ../../../../_includes/_util-fns
:marked
Whew! We already learned quite a lot about how Angular 2 works. One thing that we haven't looked into yet is how to
communicate with a backend. Most applications will come to a point where they either want to read from or write to
a remote location. Angular 2 has excellent support for such scenarios so let's dive right into it.
Try the [live example](/resources/live-examples/server-communication/ts/plnkr.html).
.l-main-section
:marked
## The Basics
When we look at the broad task of connecting multiple devices to talk to each other, there really are plenty of different options.
If we zoom in a little and restrict us to only look at the options that allow a web application that runs in a browser to communicate with
a backend we are basically left with communication via the [`HTTP`](https://tools.ietf.org/html/rfc2616) or [`WebSocket`](https://tools.ietf.org/html/rfc6455) protocol.
For web apps to communicate via `HTTP` there are basically two different techniques which are `XMLHttpRequest (XHR)` and `JSONP`.
They are quite different from each other both in purpose and implementation.
Angular comes with its own built-in abstractions that allow us to exploit both techniques in a nicely well integrated way.
## Http Client
We use the Angular `Http` client to communicate via `XMLHttpRequest (XHR)`.
We'll illustrate with a mini-version of the [tutorial](tutorial.html)'s "Tour of Heroes" (ToH) application.
This one gets some heroes from the server, displays them in a list, lets us add new heroes, and save them to the server.
It works like this.
figure.image-display
img(src='/resources/images/devguide/server-communication/http-toh.gif' alt="ToH mini app" width="250")
:marked
It's implemented with two components &mdash; a parent `TohComponent` shell and the `HeroListComponent` child.
We've seen these kinds of component in many other documentation samples.
Let's see how they change to support communication with a server.
.l-sub-section
:marked
We're overdoing the "separation of concerns" by creating two components for a tiny demo.
We're making a point about application structure that is easier to justify when the app grows.
:marked
Here is the `TohComponent` shell:
+makeExample('server-communication/ts/app/toh/toh.component.ts', null, 'app/toh.component.ts')
:marked
As usual, we import the symbols we need. The newcomer is `HTTP_Providers`,
an array of service providers from the Angular HTTP library.
We'll be using that library to access the server.
We also import a `HeroService` that we'll look at shortly.
The component specifies both the `HTTP_Providers` and the `HeroService` in the metadata `providers` array,
making them available to the child components of this "Tour of Heroes" application.
.l-sub-section
: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')
:marked
The component template displays a list of heroes with the `NgFor` repeater directive.
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
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
clear it to make ready for a new hero name.
### The `HeroListComponent` class
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*.
Although the component should request heroes immediately,
we do **not** call the service `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
:marked
This is a "best practice".
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
Now that the component is square away, we can turn to development of the backend data source
and the client-side `HeroService` that talks to it.
### Fetching data
In many of our previous samples we faked the interaction with the server by
returning mock heroes in a service like this one:
+makeExample('toh-4/ts/app/hero.service.ts', 'just-get-heroes')(format=".")
:marked
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=".")
:marked
We begin by importing Angular's `Http` client service and
[inject it](dependency-injection.html) into the `HeroService` constructor.
`Http` is not part of the Angular core. It's an optional service in its own `angular2/http` library.
Moreover, this library isn't even part of the main Angular script file.
It's in its own script file (included in the Angular npm bundle) which we must load in `index.html`.
+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', 'app/toh/hero.service.ts (http.get)')(format=".")
:marked
We pass the resource URL to `get` and it calls the server which returns
data from the `heroes.json` file.
The return value may surprise us. Many of us would expect a
[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.
Instead we're calling a `map()` method.
Clearly this is not a promise.
### RxJS Observables
The `http.get` method returns an **Observable** from the [RxJS library](https://github.com/ReactiveX/RxJS).
.l-sub-section
:marked
We cover the rudiments of RxJS in the [Observables](observables.html) chapter.
:marked
RxJS is a 3rd party library endorsed by Angular.
All of our documenations samples have installed the RxJS npm package and loaded the script in `index.html`
because observables are used widely in Angular applications.
+makeExample('server-communication/ts/index.html', 'rxjs', 'index.html')(format=".")
:marked
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 RxJS library is quite large.
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,
a version that lacks almost all operators including the ones we'd like to use here
such as the `map` method we called above in `getHeroes`.
It's up to us to add the operators we need.
We could add each operator, one-by-one, until we had a custom *Observable* implementation tuned
precisely to our requirements.
That would be a distraction today. We're learning HTTP, not counting bytes.
So we'll make it easy on ourselves and enrich *Observable* with the full set of operators.
It only takes one `import` statement.
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=".")
:marked
### 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', 'app/toh/hero.service.ts (http.get)')(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.
.l-sub-section
:marked
This is not Angular's own design.
The Angular HTTP client follows the ES2015 specification for the
[response object](https://fetch.spec.whatwg.org/#response-class) returned by the `Fetch` function.
That spec defines a `json()` method that parses the response body into a JavaScript object.
We must also know the shape of the data returned by the server API.
We shouldn't expect `json()` to return the heroes array directly.
The server we're calling always wraps JSON results in an object with a `data`
property. We have to unwrap it to get the heroes.
This is conventional web api behavior, driven by security concerns.
:marked
### Do not return the response object
Our `getHeroes()` could have returned returned the `Observable<Response>`.
Bad idea! The point of a data service is to hide the server interaction details from consumers.
The component that calls the `HeroService` wants heroes.
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.
### Always handle errors
The eagle-eyed reader may have spotted that we used the `catch` operator in conjunction with the `logAndPassOn` method that we
defined in our service. We haven't discussed so far how that actually works. Generally speaking, whenever we deal with I/O we have to account
for the cases where something goes wrong. It may just be an unreachable server or something more subtile.
We could just ignore any errors in our `HeroService` and rely on the component to handle such cases. But it's often better to
handle errors on both levels: Service and Component. In our case that may be as simple as logging the errors to the console at the service level
and blowing up an alert box in the user's face at the component level.
The `catch` operator lets us do exactly that. This operator reacts to the error case of an Observable. It takes a function that can handle the error
and then return a new Observable to continue with. Because we like to keep things simple here, we just defined a function `logAndPassOn` that calls `console.error(error)` and returns a
new Observable that re-emits the error with `Observable.throw(error)`.
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'error-handling', 'app/toh/hero.service.ts')(format=".")
:marked
Back in the `HeroListComponent` where we call `heroService.get`,
we pass a second parameter to the `subscribe` function to handle the error case at this level too.
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'ngOnInit', 'app/toh/hero-list.component.ts (ngOnInit)')
:marked
### Sending data to the server
So far we've seen how to retrieve data from a remote location using Angular's built-in `Http` service.
Let's 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
just the name of a new hero and returns an observable holding the newly-saved hero:
code-example(format="." language="javascript").
addHero (name: string) : Observable&lt;Hero>
:marked
To implement it, we need to know some details about the server's api for creating heroes.
[Our data server](#server) follows typical REST guidelines.
It expects a [`POST`](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5) request
at the same endpoint where we `GET` heroes.
It expects the new hero data to arrive in the body of the request,
structured like a `Hero` entity but without the `id` property.
The body of the request should look like this:
code-example(format="." language="javascript").
{ "name": "Windstorm" }
:marked
The server will generate the `id` and return the entire `JSON` representation
of the new hero including its generated id for our convenience.
Now that we know how the API works, we implement `addHero`like this:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'addhero', 'app/toh/hero.service.ts (addHero)')(format=".")
:marked
Notice that the second *body* parameter of the `post` method requires a JSON ***string***
so we have to `JSON.stringify` the hero content first.
.l-sub-section
:marked
We may be able to skip stringify in the near future.
:marked
Back in the `HeroListComponent`, we see that *its* `addHero` method subscribes to the observable returned by the *service's* `addHero` method.
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=".")
:marked
## Communication with `JSONP`
We just learned how to make `XMLHttpRequests` using Angulars built-in `Http` service. This is the most common approach for
server Communication. It doesn't work in all scenarios though.
For security reasons web browser do not permit to make `XHR` calls if the origin of the remote server is different from the one the web page runs in.
The origin is defined as the combination of URI scheme, hostname and port number. The policy that prevents such `XHR` requests is called [Same-origin Policy](https://en.wikipedia.org/wiki/Same-origin_policy) accordingly.
.l-sub-section
:marked
That's not entirely true. Modern browsers do allow `XHR` requests against foreign origins if the server sends the appropriate [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) headers.
In such cases there's nothing specific to do for the client unless we want our `XHR` request to include credentials (e.g Cookies) which we then have to explicitly enable for the request.
:marked
But let's not go too deep into the rabbit hole of `JSONP`. If you like to learn how this technique works under the cover, you may like to read up on this [answer](http://stackoverflow.com/questions/2067472/what-is-jsonp-all-about/2067584#2067584) on StackOverflow.
For now, let's focus on how we can use `JSONP` in Angular.
### Let's fetch some data from wikipedia
Wikipedia offers a `JSONP` search api. Let's build a simple search that shows suggestions from wikipedia as we type in a text box.
figure.image-display
img(src='/resources/images/devguide/server-communication/wiki-1.gif' alt="Wikipedia search app (v.1)" width="250")
:marked
Angular provides us with a `Jsonp` services which has the same API surface as the `Http` service with the only difference that it restricts us to use `GET` requests only. This is
because the nature of `JSONP` does not allow any other kind of requests. In order to use the `Jsonp` service we have to specify the `JSONP_PROVIDERS`.
Again, we'll make sure to do the server communication in a dedicated service that we call `WikipediaService`.
+makeExample('server-communication/ts/app/wiki/wikipedia.service.ts',null,'app/wiki/wikipedia.service.ts')
:marked
We use the `URLSearchParams` helper to define a `params` object with the key/value pairs that define the wikipedia query.
The keys are `search`, `action`, and `format`.
The value of the `search` key is the user-supplied search term that we'll lookup in wikipedia.
We call `Jsonp` with two arguments: the `wikiUrl` and an options object with a `search` property whose value is the `params` object.
`Jsonp` flattens the `params` object into a query string such as
code-example.
&search=foo&action=opensearch&format=json`
:marked
and appends it to the `wikiUrl`. Notice that the `wikiUrl` contains `callback=JSONP_CALLBACK`. The nature of `JSONP` requires that
we pass a unique function name to the server so that the server can pick up that name in the response. Many JSONP APIs pass this function name
through an URL parameter called they call `callback`. But that's more common sense than a strict rule. Since Angular doesn't assume a fixed name
for that parameter it requires *us* to put it in the URL and asign the placeholder `JSONP_CALLBACK` to it. Angular will make sure to replace
the placeholder with a unique function name for each request for us.
Now that we have the service ready to query the Wikipedia API, let's look at the actual component that should accept the user input
and shows the search results.
+makeExample('server-communication/ts/app/wiki/wiki.component.ts', null, 'app/wiki/wiki.component.ts')
:marked
There shouldn't be much of a surprise here. Our component defines an `<input>` and calls a `search(term)` method for each `keyup` event.
The `search(term)` method delegates to our `WikipediaService` which returns an `Observable<Array<string>>` to us. Instead of subscribing to it
manually we use the [async pipe](pipes.html#async-pipe) in the `ngFor` to let the view subscribe to the Observable directly.
.l-sub-section
:marked
As a rule of thumb we can use the [async pipe](pipes.html#async-pipe) whenever we don't need to interact with the unwrapped payload from the `Observable`/`Promise`
other than from the view directly. In our previous example we couldn't use it since we needed to keep a reference to the bare array of heros so that we can push
new objects into the array as we create new heros.
:marked
There are a bunch of things in our wikipedia demo that we could do better. This is a perfect opportunity to show off some nifty
`Observable` tricks that can make server communication much simpler and more fun.
## Taking advantage of Observables
If you ever wrote a search-as-you-type control yourself before, you are probably aware of some typical corner cases that arise with this task.
### 1. Don't hit the search endpoint on every key stroke
Treat the search endpoint as if you pay for it on a per-request basis. No matter if it's your own hardware or not. We shouldn't be hammering
the search enpoint more often than needed.
What we want is to hit the search endpoint as soon as the user *stops typing* instead of after every keystroke.
Here's how it *should* work &mdash; and *will* work &mdash; when we're done refactoring:
figure.image-display
img(src='/resources/images/devguide/server-communication/wiki-2.gif' alt="Wikipedia search app (v.2)" width="250")
:marked
### 2. Don't hit the search endpoint for the same term again
Consider you type *foo*, stop, type another *o*, hit return and stop back at *foo*. That should be just one request with the term *foo*
and not two even if we technically stopped twice after we had *foo* in the search box.
### 3. Cope with out-of-order responses
When we have multiple requests in-flight at the same time we must account for cases where they come back in unexpected order. Consider we first typed
*computer*, stop, a request goes out, we type *car*, stop, a request goes out but then the request that carries the results for *computer* comes back
after the request that carries the results for *car*. If we don't deal with such cases properly we can get a buggy application that shows
results for *computer* even if the search box reads *car*.
Now that we identified the problems that need to be solved, let's make a couple of trivial changes to our code to fix them in a *functional reactive* way. Here is the
entire example with all changes.
There are no changes for our `WikipediaService` so we can skip that one. We'll go over each change to briefly describe what
it does.
+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', null, 'app/wiki/wiki-form.component.ts')
:marked
The first thing we need to do to unveil the full magic of a observable-based solution is to get an `Observable<string>` for our `<input>` control.
The best way to do that is to change `<input #term (keyup)="search(term.value)"/>` into `<input [ng-form-control]="inputs"/>` and to create an `inputs` `Control` accordingly.
+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', 'control')
:marked
We now have an `Observable<string>` at `this.inputs.valueChanges`. We can simply use the `debounceTime(ms)` and `distinctUntilChanged()` operators to fix the first two problems. We basically get a new `Observable<string>` that emits
new values exactly the way we want them to be emitted.
+makeExample('server-communication/ts/app/wiki/wiki-form.component.ts', 'distinctdebounce')(format=".")
:marked
With the previous change we tamed the input but we still need to deal with the out-of-order cases. At this point we have an `Observable<string>` and a
`search(term)` method on the `WikipediaService` that returns an `Observable<Array<string>>`. What we want is an `Observable<Array<string>>` that
carries the results of the *last* term that was emitted from the `Observable<string>`.
If we would just map our current `Observable<string>` like `.map(term => wikipediaService.search(term))` we would transform it into an `Observable<Observable<Array<string>>`
which isn't quite what we want. Enter [`switchMap`](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md) to the rescue. Basically `switchMap` flattens from an `Observable<Observable<T>` into an `Observable<T>` by
emitting values only from the most recent `Observable<T>` that was produced from the outer `Observable`.
This may sound a lot like black magic for people unfamiliar with Observables but as soon as the coin sinks in it's starting to make a whole world of difficult programming tasks appear much simpler.
<a id="server"></a>
.l-main-section
:marked
## 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:
+makeJson('server-communication/ts/app/heroes.json', null, 'app/heroes.json')(format=".")
.l-sub-section
:marked
We 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)
posed by top-level JSON arrays.
:marked
Our sample saves data. We can't save to a JSON file. We'll need a server ... or a server simulation.
This sample uses an in-memory web api simulator that we first installed with `npm`:
code-example.
npm install a2-in-memory-web-api --save
:marked
and then loaded it in our html below the angular script:
+makeExample('server-communication/ts/index.html', 'in-mem-web-api', 'index.html')(format=".")
:marked
The in-memory web api gets its data from a class with a `createDb()` method that returns
a "database" object whose keys are collection names ("heroes")
and whose values are arrays of objects in those collections.
Here's the class we created for this sample by copy-and-pasting the JSON data:
+makeExample('server-communication/ts/app/hero-data.ts', null, 'app/hero-data.ts')(format=".")
:marked
Finally, we tell Angular to direct its http requests to the in-memory web api service rather
than to a remote server.
This redirection is easy in Angular because `http` delegates the client/server communication tasks
to a helper service called the `XHRBackend`.
To enable our server simulation, we replace the `XHRBackend` service with
the in-memory web api *backend* using standard Angular provider registration
in the `TohComponent`. We supply the hero data to the in-memory web api in the same way at the same time.
Here are the pertinent details, excerpted from `TohComponent`, starting with the imports:
+makeExample('server-communication/ts/app/toh/toh.component.ts', 'in-mem-web-api-imports', 'toh.component.ts (web api imports)')(format=".")
:marked
Then add these two provider definitions to the component's `providers` array in metadata:
+makeExample('server-communication/ts/app/toh/toh.component.ts', 'in-mem-web-api-providers', 'toh.component.ts (web api providers')(format=".")
:marked
See the full source code in the [live example](/resources/live-examples/server-communication/ts/plnkr.html).

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@ -88,6 +88,11 @@ var _rxData = [
from: 'node_modules/systemjs/dist/system-polyfills.js',
to: 'https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.16/system-polyfills.js'
},
{
pattern: 'script',
from: 'node_modules/a2-in-memory-web-api/web-api.js',
to: 'https://npmcdn.com/a2-in-memory-web-api/web-api.js'
},
{
pattern: 'link',
from: 'node_modules/bootstrap/dist/css/bootstrap.min.css',