docs(server-communication): heavily refactored (TS & Dart)

This commit is contained in:
Ward Bell 2016-05-17 21:25:41 -07:00
parent a4bc455030
commit 29511831cd
27 changed files with 391 additions and 307 deletions

View File

@ -0,0 +1,31 @@
// #docplaster
// #docregion
import "package:angular2/core.dart" show Component;
import "toh/hero_list_component.dart" show HeroListComponent;
import "wiki/wiki_component.dart" show WikiComponent;
import "wiki/wiki_smart_component.dart" show WikiSmartComponent;
@Component(
selector: "my-app",
template: '''
<hero-list></hero-list>
<my-wiki></my-wiki>
<my-wiki-smart></my-wiki-smart>
''',
// #enddocregion
/*
// #docregion http-providers
providers: const [
// in-memory web api provider
const Provider(BrowserClient,
useFactory: HttpClientBackendServiceFactory, deps: const [])],
// #enddocregion http-providers
*/
// #docregion
directives: const [
HeroListComponent,
WikiComponent,
WikiSmartComponent
])
class AppComponent {}

View File

@ -2,7 +2,7 @@
import 'package:http/browser_client.dart';
import 'package:http_in_memory_web_api/http_in_memory_web_api.dart';
CreateDb createDb = () => {
CreateDb _createDb = () => {
'heroes': [
{"id": "1", "name": "Windstorm"},
{"id": "2", "name": "Bombasto"},
@ -12,4 +12,4 @@ CreateDb createDb = () => {
};
BrowserClient HttpClientBackendServiceFactory() =>
new HttpClientInMemoryBackendService(createDb);
new HttpClientInMemoryBackendService(_createDb);

View File

@ -9,7 +9,7 @@ import 'hero_service.dart';
@Component(
selector: 'hero-list',
templateUrl: 'hero_list_component.html',
styles: const ['.error {color:red;}'])
providers: const [HeroService])
// #docregion component
class HeroListComponent implements OnInit {
final HeroService _heroService;

View File

@ -1,4 +1,5 @@
<!-- #docregion -->
<h1>Tour of Heroes</h1>
<h3>Heroes:</h3>
<ul>
<li *ngFor="let hero of heroes">

View File

@ -15,7 +15,9 @@ class HeroService {
// #enddocregion endpoint, http-get
final BrowserClient _http;
// #docregion ctor
HeroService(this._http);
// #enddocregion ctor
// #docregion methods, error-handling, http-get
Future<List<Hero>> getHeroes() async {
@ -57,6 +59,7 @@ class HeroService {
Exception _handleError(dynamic e) {
// 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
print(e); // log to console instead
return new Exception('Server error; cause: $e');
}

View File

@ -1,36 +0,0 @@
// #docplaster
// #docregion
import 'package:angular2/core.dart';
import 'package:http/browser_client.dart';
import 'hero_list_component.dart';
import 'hero_service.dart';
// #enddocregion
// #docregion in-mem-web-api
/* ... */
import 'package:server_communication/hero_data.dart';
// #docregion
@Component(
// #enddocregion in-mem-web-api
selector: 'my-toh',
// #docregion template
template: '''
<h1>Tour of Heroes</h1>
<hero-list></hero-list>
''',
// #enddocregion template
directives: const [HeroListComponent],
// #enddocregion
// #docregion in-mem-web-api
/* ... */
// #docregion
providers: const [
HeroService,
// #enddocregion
// in-memory web api providers
const Provider(BrowserClient,
useFactory: HttpClientBackendServiceFactory)
// #docregion
])
class TohComponent {}

View File

@ -12,11 +12,15 @@ dependencies:
jsonpadding: ^0.1.0
stream_transformers: ^0.3.0+3
http_in_memory_web_api: ^0.0.1
# #docregion transformers
transformers:
- angular2:
platform_directives: 'package:angular2/common.dart#CORE_DIRECTIVES'
platform_pipes: 'package:angular2/common.dart#COMMON_PIPES'
platform_directives:
- 'package:angular2/common.dart#CORE_DIRECTIVES'
platform_pipes:
- 'package:angular2/common.dart#COMMON_PIPES'
entry_points: 'web/main.dart'
resolved_identifiers:
BrowserClient: 'package:http/browser_client.dart'
- dart_to_js_script_rewriter
# #enddocregion transformers

View File

@ -6,15 +6,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="sample.css">
<script defer src="main.dart" type="application/dart"></script>
<script defer src="packages/browser/dart.js"></script>
</head>
<body>
<my-toh>ToH Loading...</my-toh>
<my-wiki>Wiki Loading...</my-wiki>
<my-wiki-smart>WikiSmart Loading...</my-wiki-smart>
<my-app>Loading...</my-app>
</body>
</html>

View File

@ -1,12 +1,34 @@
// #docregion
import 'package:angular2/platform/browser.dart';
// #docplaster
// #docregion final
import 'package:angular2/core.dart' show Provider;
// #docregion v1
import 'package:angular2/platform/browser.dart' show bootstrap;
// #docregion http-providers
import 'package:http/browser_client.dart' show BrowserClient;
// #enddocregion http-providers
import 'package:server_communication/toh/toh_component.dart';
import 'package:server_communication/wiki/wiki_component.dart';
import 'package:server_communication/wiki/wiki_smart_component.dart';
import 'package:server_communication/app_component.dart';
// #enddocregion v1
// #docregion in-mem-web-api-imports
import "package:server_communication/hero_data.dart";
main() {
bootstrap(TohComponent);
bootstrap(WikiComponent);
bootstrap(WikiSmartComponent);
// #enddocregion in-mem-web-api-imports
// #docregion in-mem-web-api-providers
void main() {
bootstrap(AppComponent, const [
// in-memory web api provider
const Provider(BrowserClient,
useFactory: HttpClientBackendServiceFactory, deps: const [])
// TODO: drop `deps` once fix lands for
// https://github.com/angular/angular/issues/5266
]);
}
// #enddocregion final, in-mem-web-api-providers
/*
// #docregion v1
void main() {
bootstrap(AppComponent, const [BrowserClient]);
}
// #enddocregion v1
*/

View File

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

View File

@ -4,69 +4,62 @@ describe('Server Communication', function () {
browser.get('');
});
describe('Tour of Heroes e2e tests', function () {
var _initialHeroCount = 4;
var _newHeroName = 'Mr. IQ';
var _heroCountAfterAdd = 5;
it('should display ' + _initialHeroCount + ' heroes after init', function () {
var myTohComp = element(by.tagName('my-toh'));
expect(myTohComp).toBeDefined('<my-toh> must exist');
var heroListComp = myTohComp.element(by.tagName('hero-list'));
describe('Tour of Heroes (Observable)', function () {
var initialHeroCount = 4;
var newHeroName = 'Mr. IQ';
var heroCountAfterAdd = 5;
var heroListComp = element(by.tagName('hero-list'));
var addButton = heroListComp.element(by.tagName('button'));
var heroTags = heroListComp.all(by.tagName('li'));
var heroNameInput = heroListComp.element(by.tagName('input'));
it('should exist', function() {
expect(heroListComp).toBeDefined('<hero-list> must exist');
var heroTags = heroListComp.all(by.tagName('li'));
expect(heroTags.count()).toBe(_initialHeroCount);
});
it('should display ' + initialHeroCount + ' heroes after init', function () {
expect(heroTags.count()).toBe(initialHeroCount);
});
it('should not add hero with empty name', function () {
var myTohComp = element(by.tagName('my-toh'));
expect(myTohComp).toBeDefined('<my-toh> must exist');
var addButton = myTohComp.element(by.tagName('button'));
expect(addButton).toBeDefined('"Add Hero" button must be defined');
addButton.click().then(function() {
var heroListComp = myTohComp.element(by.tagName('hero-list'));
var heroTags = heroListComp.all(by.tagName('li'));
expect(heroTags.count()).toBe(_initialHeroCount, 'No new hero should be added');
expect(heroTags.count()).toBe(initialHeroCount, 'No new hero should be added');
});
})
it('should add a new hero to the list', function () {
var myTohComp = element(by.tagName('my-toh'));
expect(myTohComp).toBeDefined('<my-toh> must exist');
var heroNameInput = myTohComp.element(by.tagName('input'));
expect(heroNameInput).toBeDefined('<input> for hero name must exist');
var addButton = myTohComp.element(by.tagName('button'));
expect(addButton).toBeDefined('"Add Hero" button must be defined');
sendKeys(heroNameInput, _newHeroName);
sendKeys(heroNameInput, newHeroName);
addButton.click().then(function() {
var heroListComp = myTohComp.element(by.tagName('hero-list'));
var heroTags = heroListComp.all(by.tagName('li'));
expect(heroTags.count()).toBe(_heroCountAfterAdd, 'A new hero should be added');
var newHeroInList = heroTags.get(_heroCountAfterAdd - 1).getText();
expect(newHeroInList).toBe(_newHeroName, 'The hero should be added to the end of the list');
expect(heroTags.count()).toBe(heroCountAfterAdd, 'A new hero should be added');
var newHeroInList = heroTags.get(heroCountAfterAdd - 1).getText();
expect(newHeroInList).toBe(newHeroName, 'The hero should be added to the end of the list');
});
})
});
describe('Wikipedia Demo e2e tests', function () {
describe('Wikipedia Demo', function () {
it('should initialize the demo with empty result list', function () {
var myWikiComp = element(by.tagName('my-wiki'));
expect(myWikiComp).toBeDefined('<my-wiki> must exist');
var resultList = myWikiComp.all(by.tagName('li'));
expect(resultList.count()).toBe(0, 'result list must be empty');
});
describe('Fetches after each keystroke', function () {
it('should fetch results after "B"', function(done) {
testForRefreshedResult('B', done);
});
it('should fetch results after "Ba"', function(done) {
testForRefreshedResult('a', done);
});
it('should fetch results after "Bas"', function(done) {
testForRefreshedResult('s', done);
});
@ -75,13 +68,13 @@ describe('Server Communication', function () {
testForRefreshedResult('ic', done);
});
});
function testForRefreshedResult(keyPressed, done) {
testForResult('my-wiki', keyPressed, false, done)
}
});
describe('Smarter Wikipedia Demo e2e tests', function () {
describe('Smarter Wikipedia Demo', function () {
it('should initialize the demo with empty result list', function () {
var myWikiSmartComp = element(by.tagName('my-wiki-smart'));
@ -93,40 +86,40 @@ describe('Server Communication', function () {
it('should fetch results after "Java"', function(done) {
testForNewResult('Java', done);
});
it('should fetch results after "JavaS"', function(done) {
testForStaleResult('S', done);
});
it('should fetch results after "JavaSc"', function(done) {
testForStaleResult('c', done);
});
it('should fetch results after "JavaScript"', function(done) {
testForStaleResult('ript', done);
});
function testForNewResult(keyPressed, done) {
testForResult('my-wiki-smart', keyPressed, false, done)
}
function testForStaleResult(keyPressed, done) {
testForResult('my-wiki-smart', keyPressed, true, done)
testForResult('my-wiki-smart', keyPressed, true, done)
}
});
function testForResult(componentTagName, keyPressed, hasListBeforeSearch, done) {
var searchWait = 1000; // Wait for wikipedia but not so long that tests timeout
var wikiComponent = element(by.tagName(componentTagName));
expect(wikiComponent).toBeDefined('<' + componentTagName + '> must exist');
var searchBox = wikiComponent.element(by.tagName('input'));
expect(searchBox).toBeDefined('<input> for search must exist');
searchBox.sendKeys(keyPressed).then(function () {
var resultList = wikiComponent.all(by.tagName('li'));
if (hasListBeforeSearch) {
expect(resultList.count()).toBeGreaterThan(0, 'result list should not be empty before search');
}
@ -137,5 +130,5 @@ describe('Server Communication', function () {
}, searchWait);
});
}
});

View File

@ -1,9 +0,0 @@
// #docregion
// import 'rxjs/Rx'; // adds ALL RxJS operators to Observable
// Just the Observable operators we need for THIS app.
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';

View File

@ -0,0 +1,37 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
// #docregion import-rxjs
// Add the RxJS Observable operators we need in this app.
import './rxjs-operators';
// #enddocregion import-rxjs
import { HeroListComponent } from './toh/hero-list.component';
import { HeroListPromiseComponent } from './toh/hero-list.component.promise';
import { WikiComponent } from './wiki/wiki.component';
import { WikiSmartComponent } from './wiki/wiki-smart.component';
@Component({
selector: 'my-app',
template: `
<hero-list></hero-list>
<hero-list-promise></hero-list-promise>
<my-wiki></my-wiki>
<my-wiki-smart></my-wiki-smart>
`,
// #enddocregion
/*
// #docregion http-providers
providers: [ HTTP_PROVIDERS ]
// #enddocregion http-providers
*/
// #docregion
directives: [
HeroListComponent, HeroListPromiseComponent,
WikiComponent, WikiSmartComponent
]
})
export class AppComponent { }
// #enddocregion

View File

@ -1,21 +1,30 @@
// #docplaster
// #docregion
// #docregion final
// Imports for loading & configuring the in-memory web api
import { provide } from '@angular/core';
import { XHRBackend } from '@angular/http';
import { InMemoryBackendService,
SEED_DATA } from 'angular2-in-memory-web-api/core';
import { HeroData } from './hero-data';
// The usual bootstrapping imports
// #docregion v1
import { bootstrap } from '@angular/platform-browser-dynamic';
// #docregion http-providers
import { HTTP_PROVIDERS } from '@angular/http';
// #enddocregion http-providers
// #docregion import-rxjs
// Add the RxJS Observable operators we need in this app.
import './add-rxjs-operators';
// #enddocregion import-rxjs
import { AppComponent } from './app.component';
import { TohComponent } from './toh/toh.component';
import { WikiComponent } from './wiki/wiki.component';
import { WikiSmartComponent } from './wiki/wiki-smart.component';
// #docregion http-providers
bootstrap(TohComponent, [HTTP_PROVIDERS]);
// #enddocregion http-providers
bootstrap(WikiComponent);
bootstrap(WikiSmartComponent);
// #enddocregion v1, final
/*
// #docregion v1
bootstrap(AppComponent, [ HTTP_PROVIDERS ]);
// #enddocregion v1
*/
// #docregion final
bootstrap(AppComponent, [
HTTP_PROVIDERS,
provide(XHRBackend, { useClass: InMemoryBackendService }), // in-mem server
provide(SEED_DATA, { useClass: HeroData }) // in-mem server data
]);
// #enddocregion final

View File

@ -0,0 +1,16 @@
// #docregion
// import 'rxjs/Rx'; // adds ALL RxJS statics & operators to Observable
// See node_module/rxjs/Rxjs.js
// Import just the rxjs statics and operators we need for THIS app.
// Statics
import 'rxjs/add/observable/throw';
// Operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/toPromise';

View File

@ -1,4 +1,5 @@
<!-- #docregion -->
<h1>Tour of Heroes ({{mode}})</h1>
<h3>Heroes:</h3>
<ul>
<li *ngFor="let hero of heroes">

View File

@ -1,21 +1,22 @@
// ToH Promise Version
// #docregion
// Promise Version
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service.1';
import { HeroService } from './hero.service.promise';
@Component({
selector: 'hero-list',
selector: 'hero-list-promise',
templateUrl: 'app/toh/hero-list.component.html',
styles: ['.error {color:red;}']
providers: [ HeroService ]
})
// #docregion component
export class HeroListComponent implements OnInit {
export class HeroListPromiseComponent implements OnInit {
constructor (private heroService: HeroService) {}
errorMessage: string;
heroes: Hero[];
mode = 'Promise';
ngOnInit() { this.getHeroes(); }

View File

@ -1,13 +1,13 @@
// #docregion
// Observable Version
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'hero-list',
templateUrl: 'app/toh/hero-list.component.html',
styles: ['.error {color:red;}']
providers: [ HeroService ]
})
// #docregion component
export class HeroListComponent implements OnInit {
@ -15,7 +15,8 @@ export class HeroListComponent implements OnInit {
constructor (private heroService: HeroService) {}
errorMessage: string;
heroes:Hero[];
heroes: Hero[];
mode = 'Observable';
ngOnInit() { this.getHeroes(); }

View File

@ -1,7 +1,6 @@
// ToH Promise Version
// #docplaster
// #docregion
// Promise Version
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Headers, RequestOptions } from '@angular/http';
@ -41,7 +40,8 @@ export class HeroService {
private handleError (error: any) {
// 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 = error.message || error.statusText || 'Server error';
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg); // log to console instead
return Promise.reject(errMsg);
}

View File

@ -1,5 +1,6 @@
// #docplaster
// #docregion
// Observable Version
// #docregion v1
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
@ -14,7 +15,9 @@ import { Observable } from 'rxjs/Observable';
@Injectable()
export class HeroService {
// #docregion ctor
constructor (private http: Http) {}
// #enddocregion ctor
// #docregion endpoint
private heroesUrl = 'app/heroes'; // URL to web API
@ -52,7 +55,8 @@ export class HeroService {
private handleError (error: any) {
// 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 = error.message || error.statusText || 'Server error';
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}

View File

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

View File

@ -1,43 +0,0 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
import { HTTP_PROVIDERS } from '@angular/http';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from './hero.service';
// #enddocregion
// #docregion in-mem-web-api-imports
import { provide } from '@angular/core';
import { XHRBackend } from '@angular/http';
// in-memory web api imports
import { InMemoryBackendService,
SEED_DATA } from 'angular2-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

@ -6,6 +6,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="sample.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
@ -21,9 +23,7 @@
</head>
<body>
<my-toh>ToH Loading...</my-toh>
<my-wiki>Wiki Loading...</my-wiki>
<my-wiki-smart>WikiSmart Loading...</my-wiki-smart>
<my-app>Loading...</my-app>
</body>
</html>

View File

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

View File

@ -2,10 +2,40 @@ extends ../../../ts/latest/guide/server-communication.jade
block includes
include ../_util-fns
- var _Http = 'BrowserClient';
- var _Angular_Http = 'Dart <code>BrowserClient</code>'
- var _httpUrl = 'https://pub.dartlang.org/packages/http'
- var _Angular_http_library = 'Dart <a href="!{_httpUrl}"><b>http</b> library</a>'
block demos-list
li HTTP client: Tour of Heroes
li JSONP client: Wikipedia to fetch data from a service that doesn't support CORS (under development)
block rxjs-import
//- N/A
block http-client
block system-config-of-http
//- N/A
block http-providers
//- TODO(chalin): mention the Angular transformer resolved_identifiers.
//- Maybe not yet at this point in the chapter.
:marked
Actually, it is unnecessary to include `BrowserClient` in the list of providers.
***But*** as is mentioned in the *Angular 2 Dart Transformer* [wiki page][ng2dtri],
the template compiler _generates_ dependency injection code, hence all the
identifiers used in DI have to be collected by the Angular 2 transformer
so that the libraries containing these identifiers can be transformed.
Unless special steps are taken, Dart libraries like `http`
are not transformed. To ensure that the `BrowserClient` identifier is available
for DI, we must add a `resolved_identifiers` parameter to the `angular2`
transformer in `pubspec.yaml`:
[ng2dtri]: https://github.com/angular/angular/wiki/Angular-2-Dart-Transformer#resolved_identifiers
- var stylePattern = { pnk: /(resolved_identifiers:|Browser.*)/gm, otl: /(- angular2:)|(transformers:)/g };
+makeExcerpt('pubspec.yaml', 'transformers', 'pubspec.yaml (transformers)', stylePattern)
block getheroes-and-addhero
:marked
@ -20,27 +50,13 @@ block getheroes-and-addhero
programming in Dart, or the tutorial on
[_Asynchronous Programming: Futures_](https://www.dartlang.org/docs/tutorials/futures/).
block http-client-service
:marked
The imported `BrowserClient` client service gets
[injected](dependency-injection.html) into the `HeroService` constructor.
Note that `BrowserClient` is not part of the Angular core.
It's an optional service provided by the Dart
[`http` package](https://pub.dartlang.org/packages/http).
block rxjs
//- N/A
block non-success-status-codes
:marked
Because a status code outside the 200-299 range _is an error_ from the
application point of view, we test for this condition and throw an
exception when detected.
block parse-json
:marked
The response data are in JSON string form.
We must parse that string into JavaScript objects which we do by calling
We must parse that string into Objects which we do by calling
the `JSON.decode()` method from the `dart:convert` library.
block error-handling
@ -58,14 +74,13 @@ block hero-list-comp-add-hero
Back in the `HeroListComponent`, we see that *its* `addHero()`
awaits for the *service's* asynchronous `addHero()` to return, and when it does,
the new hero is added to the `heroes` list for presentation to the user.
+makeExample('server-communication/ts/app/toh/hero-list.component.ts', 'addHero', 'app/toh/hero-list.component.ts (addHero)')(format=".")
block promises
//- N/A
block wikipedia-jsonp+
:marked
Wikipedia offers both `CORS` and `JSONP` search APIs.
Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API.
.alert.is-important
:marked
The remaining content of this section is coming soon.
@ -77,6 +92,4 @@ block redirect-to-web-api
:marked
To achieve this, we have Angular inject an in-memory web API server
instance as a provider for the `BrowserClient`. This is possible because
the in-memory web API server class extends `BrowserClient`. Here are the
pertinent details, excerpt from `TohComponent`:
+makeExample('server-communication/ts/app/toh/toh.component.ts', 'in-mem-web-api', 'app/toh.component.ts (excerpt)')(format=".")
the in-memory web API server class extends `BrowserClient`.

View File

@ -1,5 +1,9 @@
block includes
include ../_util-fns
- var _Http = 'Http'; // Angular `Http` library name.
- var _Angular_Http = 'Angular <code>Http</code>'
- var _Angular_http_library = 'Angular HTTP library'
:marked
[HTTP](https://tools.ietf.org/html/rfc2616) is the primary protocol for browser/server communication.
.l-sub-section
@ -16,7 +20,7 @@ block includes
as we'll learn in this chapter covering:
ul
li #[a(href="#http-client") Http client sample overview]
li #[a(href="#http-client") HTTP client sample overview]
li #[a(href="#fetch-data") Fetch data with http.get]
+ifDocsFor('ts')
li #[a(href="#rxjs") RxJS Observable of HTTP Responses]
@ -35,79 +39,110 @@ ul
p.
We illustrate these topics with code that you can
#[+liveExampleLink2('run live in a browser', 'server-communication')].
.l-main-section
h1 Demos
p This chapter describes server communication with the help of the following demos
ul
block demos-list
li HTTP client: Tour of Heroes with Observables
li HTTP client: Tour of Heroes with #{_Promise}s
li JSONP client: Wikipedia to fetch data from a service that doesn't support CORS
li JSONP client: Wikipedia using observable operators to reduce server calls
:marked
These demos are orchestrated by the root `AppComponent`
+makeExample('server-communication/ts/app/app.component.ts', null, 'app/app.component.ts')
block rxjs-import
:marked
There is nothing remarkable here _except_ for the import of RxJS operators.
+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
h1#http-providers Providing HTTP Services
:marked
We use the !{_Angular_Http} client to communicate with a server using a familiar HTTP request/response protocol.
The `!{_Http}` client is one of a family of services in the !{_Angular_http_library}.
block system-config-of-http
.l-sub-section
:marked
SystemJS knows how to load services from the !{_Angular_http_library} when we import from the `@angular/http` module
because we registered that module name in the `system.config` file.
:marked
Before we can use the `!{_Http}` client , we'll have to register it as a service provider with the Dependency Injection system.
.l-sub-section
:marked
Learn about providers in the [Dependency Injection](dependency-injection.html) chapter.
p In this demo, we register providers in the #[code bootstrap] method of #[code #[+adjExPath('app/main.ts')]].
+makeExample('server-communication/ts/app/main.ts', 'v1', 'app/main.ts (v1)')(format='.')
block http-providers
:marked
We begin by importing the symbols we need, most of them familiar by now. The newcomer is `HTTP_PROVIDERS`,
a collection of service providers from the Angular HTTP library.
We register HTTP providers in the bootstrap method by passing them in an array as the second parameter after the root component.
### Why register in *bootstrap*?
We prefer to register application-wide providers in the metadata `providers` array
of the root `AppComponent` like this:
+makeExample('server-communication/ts/app/app.component.ts','http-providers')(format='.')
:marked
Here we register the providers in the `bootstrap` method in the `main.ts` file. Why?
This is a *sample application* that doesn't talk to a real server.
We're going to reconfigure the (typically-hidden) `XhrBackend` service with a fake provider
that fetches and saves sample data from an in-memory data store.
This replacement service is called the [*in-memory web api*](#in-mem-web-api).
Such sleight-of-hand is something the root application component should *not* know about.
For this reason, and this reason *only*, we hide it *above* the `AppComponent` in `main.ts`.
.l-main-section
a#http-client
:marked
## The *Http* Client Demo
# The Tour of Heroes *HTTP* Client Demo
We use the Angular `Http` client to communicate via `XMLHttpRequest (XHR)`.
We'll demonstrate with a mini-version of the [tutorial](../tutorial)'s "Tour of Heroes" (ToH) application.
Our 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.
We use the !{_Angular_Http} client to communicate via `XMLHttpRequest (XHR)`.
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', '', 'app/toh/toh.component.ts')
block http-providers
: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
Alternatively, we may choose to add the `HTTP_PROVIDERS` while bootstrapping the app:
+makeExample('server-communication/ts/app/main.ts','http-providers','app/main.ts')(format='.')
:marked
Learn about providers in the [Dependency Injection](dependency-injection.html) chapter.
:marked
This sample only has one child, the `HeroListComponent`. Here's its template:
This demo has a single component, the `HeroListComponent`. Here's its template:
+makeExample('server-communication/ts/app/toh/hero-list.component.html', null, 'app/toh/hero-list.component.html (template)')
:marked
The component template displays a list of heroes with the `ngFor` repeater directive.
figure.image-display
img(src='/resources/images/devguide/server-communication/hero-list.png' alt="Hero List")
:marked
Beneath the heroes is an input box and an *Add Hero* button where we can enter the names of new heroes
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
and add them to the database.
We use a [template reference variable](template-syntax.html#ref-vars), `newHeroName`, 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.
clear it to make it ready for a new hero name.
Below the button is an area for an error message.
a(id="oninit")
a(id="HeroListComponent")
a#oninit
a#HeroListComponent
:marked
### The *HeroListComponent* class
## The *HeroListComponent* class
Here's the component class:
+makeExample('server-communication/ts/app/toh/hero-list.component.ts','component', 'app/toh/hero-list.component.ts (class)')
:marked
We [inject](dependency-injection.html) the `HeroService` into the constructor.
That's the instance of the `HeroService` that we provided in the parent shell `TohComponent`.
Angular [injects](dependency-injection.html) a `HeroService` into the constructor
and the component calls that service to fetch and save data.
Notice that the component **does not talk to the server directly!**
The component **does not talk directly to the !{_Angular_Http} client**!
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).
It delegates to the `HeroService`.
This is a golden rule: **always delegate data access to a supporting service class**.
@ -117,40 +152,39 @@ a(id="HeroListComponent")
and count on Angular to call `ngOnInit` when it instantiates this component.
.l-sub-section
: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
(especially calling a remote server) is handled in a separate method.
block getheroes-and-addhero
:marked
The service's `getHeroes()` and `addHero()` methods return an `Observable` of HTTP hero data.
We subscribe to this `Observable`,
specifying the actions to take when the request succeeds or fails.
We'll get to observables and subscription shortly.
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.
But we need to know a little about them to appreciate what is going on here.
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).
:marked
With our basic intuitions about the component squared away, we can turn to development of the backend data source
and the client-side `HeroService` that talks to it.
### Fetch data
With our basic intuitions about the component squared away, we're ready to look inside the `HeroService`.
.l-main-section
a#HeroService
h2#fetch-data Fetch data with the #[b HeroService]
:marked
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 a (browser-based) HTTP client service.
Here's the new `HeroService`:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'v1', 'app/toh/hero.service.ts')
block http-client-service
:marked
The imported `Http` client service gets
[injected](dependency-injection.html) into the `HeroService` constructor.
.l-sub-section
:marked
`Http` is not part of the Angular core. It's an optional service in its own `@angular/http` library
that we installed with npm (see the `package.json`) and
registered for module loading by SystemJS (see `systemjs.config.js`)
In this chapter, we 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)')
:marked
Notice that the Angular `!{_Http}` client service is
[injected](dependency-injection.html) into the `HeroService` constructor.
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'ctor')
:marked
Look closely at how we call `#{_priv}http.get`
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'http-get', 'app/toh/hero.service.ts (getHeroes)')(format=".")
@ -166,7 +200,8 @@ block http-client-service
block rxjs
:marked
<a id="rxjs"></a>
The return value may surprise us. Many of us would expect a
The return value may surprise us.
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).
We'd expect to chain a call to `then()` and extract the heroes.
Instead we're calling a `map()` method.
@ -177,7 +212,7 @@ block rxjs
.l-main-section
:marked
### RxJS Library
# RxJS Library
[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.
@ -201,22 +236,25 @@ block rxjs
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 operator, one-by-one, until we have a custom *Observable* implementation tuned
Instead, we'll import each `Observable` operator and static class method, one-by-one, until we have a custom *Observable* implementation tuned
precisely to our requirements. We'll put the `import` statements in one `app/add-rxjs-operators.ts` file.
+makeExample('server-communication/ts/app/add-rxjs-operators.ts', null, 'app/add-rxjs-operators.ts')(format=".")
+makeExample('server-communication/ts/app/rxjs-operators.ts', null, 'app/rxjs-operators.ts')(format=".")
:marked
If we forget an operator, the compiler will warn that it's missing and we'll update this file.
If we forget an operator, the TypeScript compiler will warn that it's missing and we'll update this file.
.l-sub-section
:marked
We don't need _all_ of these particular operators in the `HeroService` &mdash; just `map` and `catch`.
We'll need the others later, in a *Wiki* example [below](#more-observables).
We don'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).
:marked
Finally, we import `add-rxjs-operator`_itself_ in our `main.ts`:
+makeExample('server-communication/ts/app/main.ts', 'import-rxjs', 'app/main.ts (import rxjs)')(format=".")
Finally, we import `rxjs-operator`_itself_ in our `app.component.ts`:
+makeExample('server-communication/ts/app/app.component.ts', 'import-rxjs', 'app/app.component.ts (import rxjs)')(format=".")
:marked
Let's return to our study of the `HeroService`.
l-main-section
a#extract-data
: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:
+makeExample('server-communication/ts/app/toh/hero.service.ts', 'extract-data', 'app/toh/hero.service.ts (excerpt)')(format=".")
:marked
@ -293,8 +331,8 @@ h4 #[b HeroListComponent] error handling
block hlc-error-handling
:marked
Back in the `HeroListComponent`, where we called `#{_priv}heroService.getHeroes()`,
we supply the `subscribe` function with a second function to handle the error message.
It sets an `errorMessage` variable which we've bound conditionally in the template.
we supply the `subscribe` function with a second function parameter to handle the error message.
It sets an `errorMessage` variable which we've 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=".")
@ -370,8 +408,8 @@ code-example(format="." language="javascript").
block hero-list-comp-add-hero
: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=".")
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=".")
block promises
a#promises
@ -388,9 +426,9 @@ block promises
:marked
Let's rewrite the `HeroService` using promises , highlighting just the parts that are different.
+makeTabs(
'server-communication/ts/app/toh/hero.service.1.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',
'app/toh/hero.service.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
Converting from an observable to a promise is as simple as calling `toPromise(success, fail)`.
@ -405,9 +443,9 @@ block promises
We have to adjust the calling component to expect a `Promise` instead of an `Observable`.
+makeTabs(
'server-communication/ts/app/toh/hero-list.component.1.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',
'app/toh/hero-list.component.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
The only obvious difference is that we call `then` on the returned promise instead of `subscribe`.
We give both methods the same functional arguments.
@ -457,7 +495,7 @@ figure.image-display
block wikipedia-jsonp+
:marked
Wikipedia offers both `CORS` and `JSONP` search APIs, let's use the latter for this example.
Wikipedia offers a modern `CORS` API and a legacy `JSONP` search API. Let's use the latter for this example.
The Angular `Jsonp` service both extends the `Http` service for JSONP and restricts us to `GET` requests.
All other HTTP methods throw an error because JSONP is a read-only facility.
@ -637,7 +675,7 @@ a#in-mem-web-api
method that returns a map whose keys are collection names and whose values
are #{_array}s of objects in those collections.
Here's the class we created for this sample by copy-and-pasting the JSON data:
Here's the class we created for this sample based on the JSON data:
+makeExample('server-communication/ts/app/hero-data.ts', null, 'app/hero-data.ts')(format=".")
:marked
Ensure that the `HeroService` endpoint refers to the web API:
@ -650,13 +688,10 @@ block redirect-to-web-api
to a helper service called the `XHRBackend`.
To enable our server simulation, we replace the default `XHRBackend` service with
the in-memory web API service using standard Angular provider registration
in `TohComponent`. We initialize the in-memory web API with mock hero data at the same time.
the in-memory web API service using standard Angular provider registration techniques.
We initialize the in-memory web API with *seed data* from the mock hero dataset at the same time.
Here are the pertinent details, excerpt 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 we add the following two provider definitions to the `providers` array in component metadata:
+makeExample('server-communication/ts/app/toh/toh.component.ts', 'in-mem-web-api-providers', 'toh.component.ts (web API providers)')(format=".")
p Here is the revised (and final) version of the #[code #[+adjExPath('app/main.ts')]] demonstrating these steps.
+makeExample('server-communication/ts/app/main.ts', 'final', 'app/main.ts (final)')(format=".")
p See the full source code in the #[+liveExampleLink2('live example', 'server-communication')].