docs(toh-6): refactoring of 'add, edit, delete heroes' (#2170)
* docs(toh-6/dart): refactoring of 'add, edit, delete heroes' Refactoring of "add, edit, delete heroes" section of toh-6 from one big bottom-up step into small independent feature slices, where the user achieves a "milesone" (i.e., can run the full app) after each feature section. The section rewrite is shorter and offers a better UX. Other simplifications: - Error handling is consistent: in the hero service we log to the console, everwhere else we just let errors bubble up. - Hero service methods renamed based on function (create, update) rather then lower-level implementation (post, put). - @Output properties have been eliminated (since they weren't explained). E2E tests now pass on both the TS and Dart sides. * docs(toh-6/ts): refactoring of 'add, edit, delete heroes' Refactoring of "add, edit, delete heroes" section of toh-6 from one big bottom-up step into small independent feature slices, where the user achieves a "milesone" (i.e., can run the full app) after each feature section. The section rewrite is shorter and offers a better UX. Other simplifications: - Error handling is consistent: in the hero service we log to the console, everwhere else we just let errors bubble up. - Hero service methods renamed based on function (create, update) rather then lower-level implementation (post, put). - @Output properties have been eliminated (since they weren't explained). E2E tests now pass on both the TS and Dart sides. Post-Dart-review updates included. * docs(toh-6): ward tweaks
This commit is contained in:
parent
2bd9946bda
commit
907f848c95
|
@ -3,59 +3,38 @@
|
|||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
|
||||
// #docregion import-oninit
|
||||
import 'package:angular2/core.dart';
|
||||
// #enddocregion import-oninit
|
||||
// #docregion import-route-params
|
||||
import 'package:angular2/router.dart';
|
||||
// #enddocregion import-route-params
|
||||
|
||||
import 'hero.dart';
|
||||
// #docregion import-hero-service
|
||||
import 'hero_service.dart';
|
||||
// #enddocregion import-hero-service
|
||||
|
||||
// #docregion extract-template
|
||||
@Component(
|
||||
selector: 'my-hero-detail',
|
||||
// #docregion template-url
|
||||
templateUrl: 'hero_detail_component.html',
|
||||
// #enddocregion template-url, v2
|
||||
styleUrls: const ['hero_detail_component.css']
|
||||
// #docregion v2
|
||||
)
|
||||
// #enddocregion extract-template
|
||||
// #docregion implement
|
||||
class HeroDetailComponent implements OnInit {
|
||||
// #enddocregion implement
|
||||
Hero hero;
|
||||
// #docregion ctor
|
||||
final HeroService _heroService;
|
||||
final RouteParams _routeParams;
|
||||
|
||||
HeroDetailComponent(this._heroService, this._routeParams);
|
||||
// #enddocregion ctor
|
||||
|
||||
// #docregion ng-oninit
|
||||
Future<Null> ngOnInit() async {
|
||||
// #docregion get-id
|
||||
var idString = _routeParams.get('id');
|
||||
var id = int.parse(idString, onError: (_) => null);
|
||||
// #enddocregion get-id
|
||||
if (id != null) hero = await (_heroService.getHero(id));
|
||||
}
|
||||
// #enddocregion ng-oninit
|
||||
|
||||
// #docregion save
|
||||
Future<Null> save() async {
|
||||
await _heroService.save(hero);
|
||||
await _heroService.update(hero);
|
||||
goBack();
|
||||
}
|
||||
// #enddocregion save
|
||||
|
||||
// #docregion go-back
|
||||
void goBack() {
|
||||
window.history.back();
|
||||
}
|
||||
// #enddocregion go-back
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- #docplaster -->
|
||||
<!-- #docregion -->
|
||||
<div *ngIf="hero != null">
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// #docregion
|
||||
// #docplaster
|
||||
// #docregion , imports
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
|
@ -6,12 +7,13 @@ import 'package:angular2/core.dart';
|
|||
import 'package:http/http.dart';
|
||||
|
||||
import 'hero.dart';
|
||||
// #enddocregion imports
|
||||
|
||||
@Injectable()
|
||||
class HeroService {
|
||||
// #docregion post
|
||||
// #docregion update
|
||||
static final _headers = {'Content-Type': 'application/json'};
|
||||
// #enddocregion post
|
||||
// #enddocregion update
|
||||
// #docregion getHeroes
|
||||
static const _heroesUrl = 'app/heroes'; // URL to web API
|
||||
|
||||
|
@ -35,25 +37,20 @@ class HeroService {
|
|||
|
||||
// #docregion extract-data
|
||||
dynamic _extractData(Response resp) => JSON.decode(resp.body)['data'];
|
||||
// #enddocregion extract-data, getHeroes
|
||||
|
||||
Future<Hero> getHero(int id) async =>
|
||||
(await getHeroes()).firstWhere((hero) => hero.id == id);
|
||||
|
||||
// #docregion save
|
||||
Future<Hero> save(dynamic heroOrName) =>
|
||||
heroOrName is Hero ? _put(heroOrName) : _post(heroOrName);
|
||||
// #enddocregion save
|
||||
// #enddocregion extract-data
|
||||
|
||||
// #docregion handleError
|
||||
Exception _handleError(dynamic e) {
|
||||
print(e); // for demo purposes only
|
||||
return new Exception('Server error; cause: $e');
|
||||
}
|
||||
// #enddocregion handleError
|
||||
// #enddocregion handleError, getHeroes
|
||||
|
||||
// #docregion post
|
||||
Future<Hero> _post(String name) async {
|
||||
Future<Hero> getHero(int id) async =>
|
||||
(await getHeroes()).firstWhere((hero) => hero.id == id);
|
||||
|
||||
// #docregion create
|
||||
Future<Hero> create(String name) async {
|
||||
try {
|
||||
final response = await _http.post(_heroesUrl,
|
||||
headers: _headers, body: JSON.encode({'name': name}));
|
||||
|
@ -62,10 +59,10 @@ class HeroService {
|
|||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
// #enddocregion post
|
||||
// #enddocregion create
|
||||
// #docregion update
|
||||
|
||||
// #docregion put
|
||||
Future<Hero> _put(Hero hero) async {
|
||||
Future<Hero> update(Hero hero) async {
|
||||
try {
|
||||
var url = '$_heroesUrl/${hero.id}';
|
||||
final response =
|
||||
|
@ -75,7 +72,7 @@ class HeroService {
|
|||
throw _handleError(e);
|
||||
}
|
||||
}
|
||||
// #enddocregion put
|
||||
// #enddocregion update
|
||||
|
||||
// #docregion delete
|
||||
Future<Null> delete(int id) async {
|
||||
|
|
|
@ -59,9 +59,10 @@ button:hover {
|
|||
background-color: #cfd8dc;
|
||||
}
|
||||
/* #docregion additions */
|
||||
.error {color:red;}
|
||||
button.delete-button {
|
||||
button.delete {
|
||||
float:right;
|
||||
margin-top: 2px;
|
||||
margin-right: .8em;
|
||||
background-color: gray !important;
|
||||
color:white;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
import 'dart:async';
|
||||
|
||||
|
@ -15,45 +14,35 @@ import 'hero_service.dart';
|
|||
styleUrls: const ['heroes_component.css'],
|
||||
directives: const [HeroDetailComponent])
|
||||
class HeroesComponent implements OnInit {
|
||||
final Router _router;
|
||||
final HeroService _heroService;
|
||||
List<Hero> heroes;
|
||||
Hero selectedHero;
|
||||
// #docregion error
|
||||
String errorMessage;
|
||||
// #enddocregion error
|
||||
|
||||
final HeroService _heroService;
|
||||
final Router _router;
|
||||
|
||||
HeroesComponent(this._heroService, this._router);
|
||||
|
||||
// #docregion addHero
|
||||
Future<Null> addHero(String name) async {
|
||||
name = name.trim();
|
||||
if (name.isEmpty) return;
|
||||
try {
|
||||
heroes.add(await _heroService.save(name));
|
||||
} catch (e) {
|
||||
errorMessage = e.toString();
|
||||
}
|
||||
}
|
||||
// #enddocregion addHero
|
||||
|
||||
// #docregion deleteHero
|
||||
Future<Null> deleteHero(int id, event) async {
|
||||
try {
|
||||
event.stopPropagation();
|
||||
await _heroService.delete(id);
|
||||
heroes.removeWhere((hero) => hero.id == id);
|
||||
if (selectedHero?.id == id) selectedHero = null;
|
||||
} catch (e) {
|
||||
errorMessage = e.toString();
|
||||
}
|
||||
}
|
||||
// #enddocregion deleteHero
|
||||
|
||||
Future<Null> getHeroes() async {
|
||||
heroes = await _heroService.getHeroes();
|
||||
}
|
||||
|
||||
// #docregion add
|
||||
Future<Null> add(String name) async {
|
||||
name = name.trim();
|
||||
if (name.isEmpty) return;
|
||||
heroes.add(await _heroService.create(name));
|
||||
selectedHero = null;
|
||||
}
|
||||
// #enddocregion add
|
||||
|
||||
// #docregion delete
|
||||
Future<Null> delete(Hero hero) async {
|
||||
await _heroService.delete(hero.id);
|
||||
heroes.remove(hero);
|
||||
if (selectedHero == hero) selectedHero = null;
|
||||
}
|
||||
// #enddocregion delete
|
||||
|
||||
void ngOnInit() {
|
||||
getHeroes();
|
||||
}
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
<!-- #docplaster -->
|
||||
<!-- #docregion -->
|
||||
<h2>My Heroes</h2>
|
||||
<!-- #docregion add-and-error -->
|
||||
<div class="error" *ngIf="errorMessage != null">{{errorMessage}}</div>
|
||||
<!-- #docregion add -->
|
||||
<div>
|
||||
Name: <input #newHeroName />
|
||||
<button (click)="addHero(newHeroName.value); newHeroName.value=''">
|
||||
Add New Hero
|
||||
<label>Hero name:</label> <input #heroName />
|
||||
<button (click)="add(heroName.value); heroName.value=''">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<!-- #enddocregion add-and-error -->
|
||||
<!-- #enddocregion add -->
|
||||
<ul class="heroes">
|
||||
<li *ngFor="let hero of heroes"
|
||||
[class.selected]="hero === selectedHero"
|
||||
(click)="onSelect(hero)">
|
||||
<span class="badge">{{hero.id}}</span> {{hero.name}}
|
||||
<!-- #docregion li-element -->
|
||||
<li *ngFor="let hero of heroes" (click)="onSelect(hero)"
|
||||
[class.selected]="hero === selectedHero">
|
||||
<span class="badge">{{hero.id}}</span>
|
||||
<span>{{hero.name}}</span>
|
||||
<!-- #docregion delete -->
|
||||
<button class="delete-button" (click)="deleteHero(hero.id, $event)">x</button>
|
||||
<button class="delete"
|
||||
(click)="delete(hero); $event.stopPropagation()">x</button>
|
||||
<!-- #enddocregion delete -->
|
||||
</li>
|
||||
<!-- #enddocregion li-element -->
|
||||
</ul>
|
||||
<!-- #docregion mini-detail -->
|
||||
<div *ngIf="selectedHero != null">
|
||||
<h2>
|
||||
<!-- #docregion pipe -->
|
||||
{{selectedHero.name | uppercase}} is my hero
|
||||
<!-- #enddocregion pipe -->
|
||||
</h2>
|
||||
<button (click)="gotoDetail()">View Details</button>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
// #docregion
|
||||
// #docregion , init
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
// #docregion init
|
||||
import 'package:angular2/core.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/testing.dart';
|
||||
|
@ -26,7 +25,6 @@ class InMemoryDataService extends MockClient {
|
|||
];
|
||||
static final List<Hero> _heroesDb =
|
||||
_initialHeroes.map((json) => new Hero.fromJson(json)).toList();
|
||||
// #enddocregion init
|
||||
static int _nextId = _heroesDb.map((hero) => hero.id).reduce(max) + 1;
|
||||
|
||||
static Future<Response> _handler(Request request) async {
|
||||
|
@ -37,6 +35,7 @@ class InMemoryDataService extends MockClient {
|
|||
final regExp = new RegExp(prefix, caseSensitive: false);
|
||||
data = _heroesDb.where((hero) => hero.name.contains(regExp)).toList();
|
||||
break;
|
||||
// #enddocregion init-disabled
|
||||
case 'POST':
|
||||
var name = JSON.decode(request.body)['name'];
|
||||
var newHero = new Hero(_nextId++, name);
|
||||
|
@ -54,6 +53,7 @@ class InMemoryDataService extends MockClient {
|
|||
_heroesDb.removeWhere((hero) => hero.id == id);
|
||||
// No data, so leave it as null.
|
||||
break;
|
||||
// #docregion init-disabled
|
||||
default:
|
||||
throw 'Unimplemented HTTP method ${request.method}';
|
||||
}
|
||||
|
@ -62,5 +62,4 @@ class InMemoryDataService extends MockClient {
|
|||
}
|
||||
|
||||
InMemoryDataService() : super(_handler);
|
||||
// #docregion init
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import 'package:angular2/core.dart';
|
|||
import 'package:angular2/platform/browser.dart';
|
||||
import 'package:angular2_tour_of_heroes/app_component.dart';
|
||||
// #enddocregion v1
|
||||
import 'package:http/http.dart';
|
||||
import 'package:angular2_tour_of_heroes/in_memory_data_service.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
void main() {
|
||||
bootstrap(AppComponent,
|
||||
|
|
|
@ -1,246 +1,283 @@
|
|||
/// <reference path='../_protractor/e2e.d.ts' />
|
||||
'use strict';
|
||||
describe('TOH Http Chapter', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
browser.get('');
|
||||
});
|
||||
const expectedH1 = 'Tour of Heroes';
|
||||
const expectedTitle = `Angular 2 ${expectedH1}`;
|
||||
const targetHero = { id: 15, name: 'Magneta' };
|
||||
const targetHeroDashboardIndex = 3;
|
||||
const nameSuffix = 'X';
|
||||
const newHeroName = targetHero.name + nameSuffix;
|
||||
|
||||
function getPageStruct() {
|
||||
let hrefEles = element.all(by.css('my-app a'));
|
||||
type WPromise<T> = webdriver.promise.Promise<T>;
|
||||
|
||||
class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
// Factory methods
|
||||
|
||||
// Hero from string formatted as '<id> <name>'.
|
||||
static fromString(s: string): Hero {
|
||||
return {
|
||||
id: +s.substr(0, s.indexOf(' ')),
|
||||
name: s.substr(s.indexOf(' ') + 1),
|
||||
};
|
||||
}
|
||||
|
||||
// Hero from hero list <li> element.
|
||||
static async fromLi(li: protractor.ElementFinder): Promise<Hero> {
|
||||
let strings = await li.all(by.xpath('span')).getText();
|
||||
return { id: +strings[0], name: strings[1] };
|
||||
}
|
||||
|
||||
// Hero id and name from the given detail element.
|
||||
static async fromDetail(detail: protractor.ElementFinder): Promise<Hero> {
|
||||
// Get hero id from the first <div>
|
||||
let _id = await detail.all(by.css('div')).first().getText();
|
||||
// Get name from the h2
|
||||
let _name = await detail.element(by.css('h2')).getText();
|
||||
return {
|
||||
id: +_id.substr(_id.indexOf(' ') + 1),
|
||||
name: _name.substr(0, _name.lastIndexOf(' '))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe('Tutorial part 6', () => {
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
function getPageElts() {
|
||||
let hrefElts = element.all(by.css('my-app a'));
|
||||
|
||||
return {
|
||||
hrefs: hrefEles,
|
||||
myDashboardHref: hrefEles.get(0),
|
||||
myDashboardParent: element(by.css('my-app my-dashboard')),
|
||||
topHeroes: element.all(by.css('my-app my-dashboard .module.hero')),
|
||||
hrefs: hrefElts,
|
||||
|
||||
myHeroesHref: hrefEles.get(1),
|
||||
myHeroesParent: element(by.css('my-app my-heroes')),
|
||||
allHeroes: element.all(by.css('my-app my-heroes li .hero-element')),
|
||||
myDashboardHref: hrefElts.get(0),
|
||||
myDashboard: element(by.css('my-app my-dashboard')),
|
||||
topHeroes: element.all(by.css('my-app my-dashboard > div h4')),
|
||||
|
||||
firstDeleteButton: element.all(by.buttonText('Delete')).get(0),
|
||||
myHeroesHref: hrefElts.get(1),
|
||||
myHeroes: element(by.css('my-app my-heroes')),
|
||||
allHeroes: element.all(by.css('my-app my-heroes li')),
|
||||
selectedHero: element(by.css('my-app li.selected')),
|
||||
selectedHeroSubview: element(by.css('my-app my-heroes > div:last-child')),
|
||||
|
||||
addButton: element.all(by.buttonText('Add New Hero')).get(0),
|
||||
|
||||
heroDetail: element(by.css('my-app my-hero-detail')),
|
||||
heroDetail: element(by.css('my-app my-hero-detail > div')),
|
||||
|
||||
searchBox: element(by.css('#search-box')),
|
||||
searchResults: element.all(by.css('.search-result'))
|
||||
};
|
||||
}
|
||||
|
||||
it('should search for hero and navigate to details view', function() {
|
||||
let page = getPageStruct();
|
||||
describe('Initial page', () => {
|
||||
|
||||
return sendKeys(page.searchBox, 'Magneta').then(function () {
|
||||
it(`has title '${expectedTitle}'`, () => {
|
||||
expect(browser.getTitle()).toEqual(expectedTitle);
|
||||
});
|
||||
|
||||
it(`has h1 '${expectedH1}'`, () => {
|
||||
expectHeading(1, expectedH1);
|
||||
});
|
||||
|
||||
const expectedViewNames = ['Dashboard', 'Heroes'];
|
||||
it(`has views ${expectedViewNames}`, () => {
|
||||
let viewNames = getPageElts().hrefs.map(el => el.getText());
|
||||
expect(viewNames).toEqual(expectedViewNames);
|
||||
});
|
||||
|
||||
it('has dashboard as the active view', () => {
|
||||
let page = getPageElts();
|
||||
expect(page.myDashboard.isPresent()).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Dashboard tests', () => {
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it('has top heroes', () => {
|
||||
let page = getPageElts();
|
||||
expect(page.topHeroes.count()).toEqual(4);
|
||||
});
|
||||
|
||||
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`cancels and shows ${targetHero.name} in Dashboard`, () => {
|
||||
element(by.buttonText('Back')).click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to past for toh-6
|
||||
|
||||
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(targetHero.name);
|
||||
});
|
||||
|
||||
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`saves and shows ${newHeroName} in Dashboard`, () => {
|
||||
element(by.buttonText('Save')).click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to past for toh-6
|
||||
|
||||
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(newHeroName);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Heroes tests', () => {
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it('can switch to Heroes view', () => {
|
||||
getPageElts().myHeroesHref.click();
|
||||
let page = getPageElts();
|
||||
expect(page.myHeroes.isPresent()).toBeTruthy();
|
||||
expect(page.allHeroes.count()).toEqual(10, 'number of heroes');
|
||||
});
|
||||
|
||||
it(`selects and shows ${targetHero.name} as selected in list`, () => {
|
||||
getHeroLiEltById(targetHero.id).click();
|
||||
expect(Hero.fromLi(getPageElts().selectedHero)).toEqual(targetHero);
|
||||
});
|
||||
|
||||
it('shows selected hero subview', () => {
|
||||
let page = getPageElts();
|
||||
let title = page.selectedHeroSubview.element(by.css('h2')).getText();
|
||||
let expectedTitle = `${targetHero.name.toUpperCase()} is my hero`;
|
||||
expect(title).toEqual(expectedTitle);
|
||||
});
|
||||
|
||||
it('can route to hero details', () => {
|
||||
element(by.buttonText('View Details')).click();
|
||||
|
||||
let page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
let hero = Hero.fromDetail(page.heroDetail);
|
||||
expect(hero).toEqual(targetHero);
|
||||
});
|
||||
|
||||
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
|
||||
|
||||
it(`shows ${newHeroName} in Heroes list`, () => {
|
||||
element(by.buttonText('Save')).click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to past for toh-6
|
||||
let expectedHero = {id: targetHero.id, name: newHeroName};
|
||||
expect(Hero.fromLi(getHeroLiEltById(targetHero.id))).toEqual(expectedHero);
|
||||
});
|
||||
|
||||
it(`deletes ${newHeroName} from Heroes list`, async () => {
|
||||
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
|
||||
const li = getHeroLiEltById(targetHero.id);
|
||||
li.element(by.buttonText('x')).click();
|
||||
|
||||
const page = getPageElts();
|
||||
expect(page.myHeroes.isPresent()).toBeTruthy();
|
||||
expect(page.allHeroes.count()).toEqual(9, 'number of heroes');
|
||||
const heroesAfter = await toHeroArray(page.allHeroes);
|
||||
const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName);
|
||||
expect(heroesAfter).toEqual(expectedHeroes);
|
||||
expect(page.selectedHeroSubview.isPresent()).toBeFalsy();
|
||||
});
|
||||
|
||||
it(`adds back ${targetHero.name}`, async () => {
|
||||
const newHeroName = 'Alice';
|
||||
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
|
||||
const numHeroes = heroesBefore.length;
|
||||
|
||||
sendKeys(element(by.css('input')), newHeroName);
|
||||
element(by.buttonText('Add')).click();
|
||||
|
||||
let page = getPageElts();
|
||||
let heroesAfter = await toHeroArray(page.allHeroes);
|
||||
expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes');
|
||||
|
||||
expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there');
|
||||
|
||||
const maxId = heroesBefore[heroesBefore.length - 1].id;
|
||||
expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: newHeroName});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Progressive hero search', () => {
|
||||
|
||||
beforeAll(() => browser.get(''));
|
||||
|
||||
it(`searches for 'Ma'`, async () => {
|
||||
sendKeys(getPageElts().searchBox, 'Ma');
|
||||
browser.sleep(1000);
|
||||
expect(getPageElts().searchResults.count()).toBe(4);
|
||||
});
|
||||
|
||||
it(`continues search with 'g'`, async () => {
|
||||
sendKeys(getPageElts().searchBox, 'g');
|
||||
browser.sleep(1000);
|
||||
expect(getPageElts().searchResults.count()).toBe(2);
|
||||
});
|
||||
|
||||
it(`continues search with 'n' and gets ${targetHero.name}`, async () => {
|
||||
sendKeys(getPageElts().searchBox, 'n');
|
||||
browser.sleep(1000);
|
||||
let page = getPageElts();
|
||||
expect(page.searchResults.count()).toBe(1);
|
||||
let hero = page.searchResults.get(0);
|
||||
return hero.click();
|
||||
})
|
||||
.then(function() {
|
||||
browser.waitForAngular();
|
||||
let inputEle = page.heroDetail.element(by.css('input'));
|
||||
return inputEle.getAttribute('value');
|
||||
})
|
||||
.then(function(value) {
|
||||
expect(value).toBe('Magneta');
|
||||
expect(hero.getText()).toEqual(targetHero.name);
|
||||
});
|
||||
|
||||
it(`navigates to ${targetHero.name} details view`, async () => {
|
||||
let hero = getPageElts().searchResults.get(0);
|
||||
expect(hero.getText()).toEqual(targetHero.name);
|
||||
hero.click();
|
||||
|
||||
let page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
expect(Hero.fromDetail(page.heroDetail)).toEqual(targetHero);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to add a hero from the "Heroes" view', function(){
|
||||
let page = getPageStruct();
|
||||
let heroCount: webdriver.promise.Promise<number>;
|
||||
function dashboardSelectTargetHero() {
|
||||
let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
|
||||
expect(targetHeroElt.getText()).toEqual(targetHero.name);
|
||||
targetHeroElt.click();
|
||||
browser.waitForAngular(); // seems necessary to gets tests to past for toh-6
|
||||
|
||||
page.myHeroesHref.click().then(function() {
|
||||
browser.waitForAngular();
|
||||
heroCount = page.allHeroes.count();
|
||||
expect(heroCount).toBe(10, 'should show 10');
|
||||
}).then(function() {
|
||||
return page.addButton.click();
|
||||
}).then(function(){
|
||||
return save(page, '', 'The New Hero');
|
||||
}).then(function(){
|
||||
browser.waitForAngular();
|
||||
|
||||
heroCount = page.allHeroes.count();
|
||||
expect(heroCount).toBe(11, 'should show 11');
|
||||
|
||||
let newHero = element(by.xpath('//span[@class="hero-element" and contains(text(),"The New Hero")]'));
|
||||
expect(newHero).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to delete hero from "Heroes" view', function(){
|
||||
let page = getPageStruct();
|
||||
let heroCount: webdriver.promise.Promise<number>;
|
||||
|
||||
page.myHeroesHref.click().then(function() {
|
||||
browser.waitForAngular();
|
||||
heroCount = page.allHeroes.count();
|
||||
expect(heroCount).toBe(10, 'should show 10');
|
||||
}).then(function() {
|
||||
return page.firstDeleteButton.click();
|
||||
}).then(function(){
|
||||
browser.waitForAngular();
|
||||
heroCount = page.allHeroes.count();
|
||||
expect(heroCount).toBe(9, 'should show 9');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to save details from "Dashboard" view', function () {
|
||||
let page = getPageStruct();
|
||||
expect(page.myDashboardParent.isPresent()).toBe(true, 'dashboard element should be available');
|
||||
let heroEle = page.topHeroes.get(2);
|
||||
let heroDescrEle = heroEle.element(by.css('h4'));
|
||||
let heroDescr: string;
|
||||
|
||||
return heroDescrEle.getText().then(function(text) {
|
||||
heroDescr = text;
|
||||
return heroEle.click();
|
||||
}).then(function() {
|
||||
return save(page, heroDescr, '-foo');
|
||||
})
|
||||
.then(function(){
|
||||
return page.myDashboardHref.click();
|
||||
})
|
||||
.then(function() {
|
||||
expect(page.myDashboardParent.isPresent()).toBe(true, 'dashboard element should be back');
|
||||
expect(heroDescrEle.getText()).toEqual(heroDescr + '-foo');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to save details from "Heroes" view', function () {
|
||||
let page = getPageStruct();
|
||||
|
||||
let viewDetailsButtonEle = page.myHeroesParent.element(by.cssContainingText('button', 'View Details'));
|
||||
let heroEle: protractor.ElementFinder, heroDescr: string;
|
||||
|
||||
page.myHeroesHref.click().then(function() {
|
||||
expect(page.myDashboardParent.isPresent()).toBe(false, 'dashboard element should NOT be present');
|
||||
expect(page.myHeroesParent.isPresent()).toBe(true, 'myHeroes element should be present');
|
||||
expect(viewDetailsButtonEle.isPresent()).toBe(false, 'viewDetails button should not yet be present');
|
||||
heroEle = page.allHeroes.get(0);
|
||||
return heroEle.getText();
|
||||
}).then(function(text) {
|
||||
// remove leading 'id' from the element
|
||||
heroDescr = text.substr(text.indexOf(' ') + 1);
|
||||
return heroEle.click();
|
||||
}).then(function() {
|
||||
expect(viewDetailsButtonEle.isDisplayed()).toBe(true, 'viewDetails button should now be visible');
|
||||
return viewDetailsButtonEle.click();
|
||||
}).then(function() {
|
||||
return save(page, heroDescr, '-bar');
|
||||
})
|
||||
.then(function(){
|
||||
return page.myHeroesHref.click();
|
||||
})
|
||||
.then(function() {
|
||||
expect(heroEle.getText()).toContain(heroDescr + '-bar');
|
||||
});
|
||||
});
|
||||
|
||||
function save(page: any, origValue: string, textToAdd: string) {
|
||||
let inputEle = page.heroDetail.element(by.css('input'));
|
||||
expect(inputEle.isDisplayed()).toBe(true, 'should be able to see the input box');
|
||||
let saveButtonEle = page.heroDetail.element(by.buttonText('Save'));
|
||||
let backButtonEle = page.heroDetail.element(by.buttonText('Back'));
|
||||
expect(backButtonEle.isDisplayed()).toBe(true, 'should be able to see the back button');
|
||||
let detailTextEle = page.heroDetail.element(by.css('div h2'));
|
||||
expect(detailTextEle.getText()).toContain(origValue);
|
||||
return sendKeys(inputEle, textToAdd).then(function () {
|
||||
expect(detailTextEle.getText()).toContain(origValue + textToAdd);
|
||||
return saveButtonEle.click();
|
||||
});
|
||||
let page = getPageElts();
|
||||
expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
|
||||
let hero = Hero.fromDetail(page.heroDetail);
|
||||
expect(hero).toEqual(targetHero);
|
||||
}
|
||||
|
||||
it('should be able to see the start screen', function () {
|
||||
let page = getPageStruct();
|
||||
expect(page.hrefs.count()).toEqual(2, 'should be two dashboard choices');
|
||||
expect(page.myDashboardHref.getText()).toEqual('Dashboard');
|
||||
expect(page.myHeroesHref.getText()).toEqual('Heroes');
|
||||
});
|
||||
async function updateHeroNameInDetailView() {
|
||||
// Assumes that the current view is the hero details view.
|
||||
addToHeroName(nameSuffix);
|
||||
|
||||
it('should be able to see dashboard choices', function () {
|
||||
let page = getPageStruct();
|
||||
expect(page.topHeroes.count()).toBe(4, 'should be 4 dashboard hero choices');
|
||||
});
|
||||
|
||||
it('should be able to toggle the views', function () {
|
||||
let page = getPageStruct();
|
||||
|
||||
expect(page.myDashboardParent.element(by.css('h3')).getText()).toEqual('Top Heroes');
|
||||
page.myHeroesHref.click().then(function() {
|
||||
expect(page.myDashboardParent.isPresent()).toBe(false, 'should no longer see dashboard element');
|
||||
expect(page.allHeroes.count()).toBeGreaterThan(4, 'should be more than 4 heroes shown');
|
||||
return page.myDashboardHref.click();
|
||||
}).then(function() {
|
||||
expect(page.myDashboardParent.isPresent()).toBe(true, 'should once again see the dashboard element');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should be able to edit details from "Dashboard" view', function () {
|
||||
let page = getPageStruct();
|
||||
expect(page.myDashboardParent.isPresent()).toBe(true, 'dashboard element should be available');
|
||||
let heroEle = page.topHeroes.get(3);
|
||||
let heroDescrEle = heroEle.element(by.css('h4'));
|
||||
let heroDescr: string;
|
||||
return heroDescrEle.getText().then(function(text) {
|
||||
heroDescr = text;
|
||||
return heroEle.click();
|
||||
}).then(function() {
|
||||
return editDetails(page, heroDescr, '-foo');
|
||||
}).then(function() {
|
||||
expect(page.myDashboardParent.isPresent()).toBe(true, 'dashboard element should be back');
|
||||
expect(heroDescrEle.getText()).toEqual(heroDescr + '-foo');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to edit details from "Heroes" view', function () {
|
||||
let page = getPageStruct();
|
||||
expect(page.myDashboardParent.isPresent()).toBe(true, 'dashboard element should be present');
|
||||
let viewDetailsButtonEle = page.myHeroesParent.element(by.cssContainingText('button', 'View Details'));
|
||||
let heroEle: protractor.ElementFinder, heroDescr: string;
|
||||
page.myHeroesHref.click().then(function() {
|
||||
expect(page.myDashboardParent.isPresent()).toBe(false, 'dashboard element should NOT be present');
|
||||
expect(page.myHeroesParent.isPresent()).toBe(true, 'myHeroes element should be present');
|
||||
expect(viewDetailsButtonEle.isPresent()).toBe(false, 'viewDetails button should not yet be present');
|
||||
heroEle = page.allHeroes.get(2);
|
||||
return heroEle.getText();
|
||||
}).then(function(text) {
|
||||
// remove leading 'id' from the element
|
||||
heroDescr = text.substr(text.indexOf(' ') + 1);
|
||||
return heroEle.click();
|
||||
}).then(function() {
|
||||
expect(viewDetailsButtonEle.isDisplayed()).toBe(true, 'viewDetails button should now be visible');
|
||||
return viewDetailsButtonEle.click();
|
||||
}).then(function() {
|
||||
return editDetails(page, heroDescr, '-bar');
|
||||
}).then(function() {
|
||||
expect(page.myHeroesParent.isPresent()).toBe(true, 'myHeroes element should be back');
|
||||
expect(heroEle.getText()).toContain(heroDescr + '-bar');
|
||||
expect(viewDetailsButtonEle.isPresent()).toBe(false, 'viewDetails button should again NOT be present');
|
||||
});
|
||||
});
|
||||
|
||||
function editDetails(page: any, origValue: string, textToAdd: string) {
|
||||
expect(page.myDashboardParent.isPresent()).toBe(false, 'dashboard element should NOT be present');
|
||||
expect(page.myHeroesParent.isPresent()).toBe(false, 'myHeroes element should NOT be present');
|
||||
expect(page.heroDetail.isDisplayed()).toBe(true, 'should be able to see hero-details');
|
||||
let inputEle = page.heroDetail.element(by.css('input'));
|
||||
expect(inputEle.isDisplayed()).toBe(true, 'should be able to see the input box');
|
||||
let buttons = page.heroDetail.all(by.css('button'));
|
||||
let backButtonEle = buttons.get(0);
|
||||
let saveButtonEle = buttons.get(1);
|
||||
expect(backButtonEle.isDisplayed()).toBe(true, 'should be able to see the back button');
|
||||
expect(saveButtonEle.isDisplayed()).toBe(true, 'should be able to see the save button');
|
||||
let detailTextEle = page.heroDetail.element(by.css('div h2'));
|
||||
expect(detailTextEle.getText()).toContain(origValue);
|
||||
return sendKeys(inputEle, textToAdd).then(function () {
|
||||
expect(detailTextEle.getText()).toContain(origValue + textToAdd);
|
||||
return saveButtonEle.click();
|
||||
});
|
||||
let hero = await Hero.fromDetail(getPageElts().heroDetail);
|
||||
expect(hero).toEqual({id: targetHero.id, name: newHeroName});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function addToHeroName(text: string): WPromise<void> {
|
||||
let input = element(by.css('input'));
|
||||
return sendKeys(input, text);
|
||||
}
|
||||
|
||||
function expectHeading(hLevel: number, expectedText: string): void {
|
||||
let hTag = `h${hLevel}`;
|
||||
let hText = element(by.css(hTag)).getText();
|
||||
expect(hText).toEqual(expectedText, hTag);
|
||||
};
|
||||
|
||||
function getHeroLiEltById(id: number): protractor.ElementFinder {
|
||||
let spanForId = element(by.cssContainingText('li span.badge', id.toString()));
|
||||
return spanForId.element(by.xpath('..'));
|
||||
}
|
||||
|
||||
async function toHeroArray(allHeroes: protractor.ElementArrayFinder): Promise<Hero[]> {
|
||||
let promisedHeroes: Array<Promise<Hero>> = await allHeroes.map(Hero.fromLi);
|
||||
// The cast is necessary to get around issuing with the signature of Promise.all()
|
||||
return <Promise<any>> Promise.all(promisedHeroes);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
// #docregion rxjs-extensions
|
||||
import './rxjs-extensions';
|
||||
// #enddocregion rxjs-extensions
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
// #docplaster
|
||||
// #docregion , v1, v2
|
||||
// #docregion
|
||||
// #docregion rxjs-extensions
|
||||
import './rxjs-extensions';
|
||||
// #enddocregion rxjs-extensions
|
||||
|
||||
// #docregion v1, v2
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!-- #docplaster -->
|
||||
<!-- #docregion -->
|
||||
<div *ngIf="hero">
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
// #docplaster
|
||||
// #docregion, variables-imports
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
// #enddocregion variables-imports
|
||||
// #docregion
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
|
||||
import { Hero } from './hero';
|
||||
|
@ -13,50 +10,30 @@ import { HeroService } from './hero.service';
|
|||
templateUrl: 'app/hero-detail.component.html',
|
||||
styleUrls: ['app/hero-detail.component.css']
|
||||
})
|
||||
// #docregion variables-imports
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
@Input() hero: Hero;
|
||||
@Output() close = new EventEmitter();
|
||||
error: any;
|
||||
navigated = false; // true if navigated here
|
||||
// #enddocregion variables-imports
|
||||
hero: Hero;
|
||||
|
||||
constructor(
|
||||
private heroService: HeroService,
|
||||
private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
// #docregion ngOnInit
|
||||
ngOnInit(): void {
|
||||
this.route.params.forEach((params: Params) => {
|
||||
if (params['id'] !== undefined) {
|
||||
let id = +params['id'];
|
||||
this.navigated = true;
|
||||
this.heroService.getHero(id)
|
||||
.then(hero => this.hero = hero);
|
||||
} else {
|
||||
this.navigated = false;
|
||||
this.hero = new Hero();
|
||||
}
|
||||
let id = +params['id'];
|
||||
this.heroService.getHero(id)
|
||||
.then(hero => this.hero = hero);
|
||||
});
|
||||
}
|
||||
// #enddocregion ngOnInit
|
||||
|
||||
// #docregion save
|
||||
save(): void {
|
||||
this.heroService
|
||||
.save(this.hero)
|
||||
.then(hero => {
|
||||
this.hero = hero; // saved hero, w/ id if new
|
||||
this.goBack(hero);
|
||||
})
|
||||
.catch(error => this.error = error); // TODO: Display error message
|
||||
this.heroService.update(this.hero)
|
||||
.then(this.goBack);
|
||||
}
|
||||
// #enddocregion save
|
||||
// #docregion goBack
|
||||
goBack(savedHero: Hero = null): void {
|
||||
this.close.emit(savedHero);
|
||||
if (this.navigated) { window.history.back(); }
|
||||
|
||||
goBack(): void {
|
||||
window.history.back();
|
||||
}
|
||||
// #enddocregion goBack
|
||||
}
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
// #docregion , imports
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Headers, Http, Response } from '@angular/http';
|
||||
import { Headers, Http } from '@angular/http';
|
||||
|
||||
// #docregion rxjs
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
// #enddocregion rxjs
|
||||
|
||||
import { Hero } from './hero';
|
||||
// #enddocregion imports
|
||||
|
||||
@Injectable()
|
||||
export class HeroService {
|
||||
|
||||
// #docregion update
|
||||
private headers = new Headers({'Content-Type': 'application/json'});
|
||||
// #enddocregion update
|
||||
// #docregion getHeroes
|
||||
private heroesUrl = 'app/heroes'; // URL to web api
|
||||
|
||||
|
@ -36,62 +40,40 @@ export class HeroService {
|
|||
.then(heroes => heroes.find(hero => hero.id === id));
|
||||
}
|
||||
|
||||
// #docregion save
|
||||
save(hero: Hero): Promise<Hero> {
|
||||
if (hero.id) {
|
||||
return this.put(hero);
|
||||
}
|
||||
return this.post(hero);
|
||||
}
|
||||
// #enddocregion save
|
||||
|
||||
// #docregion delete
|
||||
delete(hero: Hero): Promise<Response> {
|
||||
let headers = new Headers();
|
||||
headers.append('Content-Type', 'application/json');
|
||||
|
||||
let url = `${this.heroesUrl}/${hero.id}`;
|
||||
|
||||
return this.http
|
||||
.delete(url, {headers: headers})
|
||||
.toPromise()
|
||||
.catch(this.handleError);
|
||||
delete(id: number): Promise<void> {
|
||||
let url = `${this.heroesUrl}/${id}`;
|
||||
return this.http.delete(url, {headers: this.headers})
|
||||
.toPromise()
|
||||
.then(() => null)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
// #enddocregion delete
|
||||
|
||||
// #docregion post
|
||||
// Add new Hero
|
||||
private post(hero: Hero): Promise<Hero> {
|
||||
let headers = new Headers({
|
||||
'Content-Type': 'application/json'});
|
||||
|
||||
// #docregion create
|
||||
create(name: string): Promise<Hero> {
|
||||
return this.http
|
||||
.post(this.heroesUrl, JSON.stringify(hero), {headers: headers})
|
||||
.toPromise()
|
||||
.then(res => res.json().data)
|
||||
.catch(this.handleError);
|
||||
.post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
|
||||
.toPromise()
|
||||
.then(res => res.json().data)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
// #enddocregion post
|
||||
|
||||
// #docregion put
|
||||
// Update existing Hero
|
||||
private put(hero: Hero): Promise<Hero> {
|
||||
let headers = new Headers();
|
||||
headers.append('Content-Type', 'application/json');
|
||||
|
||||
let url = `${this.heroesUrl}/${hero.id}`;
|
||||
// #enddocregion create
|
||||
// #docregion update
|
||||
|
||||
update(hero: Hero): Promise<Hero> {
|
||||
const url = `${this.heroesUrl}/${hero.id}`;
|
||||
return this.http
|
||||
.put(url, JSON.stringify(hero), {headers: headers})
|
||||
.toPromise()
|
||||
.then(() => hero)
|
||||
.catch(this.handleError);
|
||||
.put(url, JSON.stringify(hero), {headers: this.headers})
|
||||
.toPromise()
|
||||
.then(() => hero)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
// #enddocregion put
|
||||
// #enddocregion put, update
|
||||
|
||||
// #docregion handleError
|
||||
private handleError(error: any): Promise<any> {
|
||||
console.error('An error occurred', error);
|
||||
console.error('An error occurred', error); // for demo purposes only
|
||||
return Promise.reject(error.message || error);
|
||||
}
|
||||
// #enddocregion handleError
|
||||
|
|
|
@ -59,9 +59,10 @@ button:hover {
|
|||
background-color: #cfd8dc;
|
||||
}
|
||||
/* #docregion additions */
|
||||
.error {color:red;}
|
||||
button.delete-button{
|
||||
button.delete {
|
||||
float:right;
|
||||
margin-top: 2px;
|
||||
margin-right: .8em;
|
||||
background-color: gray !important;
|
||||
color:white;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
<!-- #docregion -->
|
||||
<h2>My Heroes</h2>
|
||||
<ul class="heroes">
|
||||
<li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero">
|
||||
<span class="hero-element">
|
||||
<span class="badge">{{hero.id}}</span> {{hero.name}}
|
||||
</span>
|
||||
<!-- #docregion delete -->
|
||||
<button class="delete-button" (click)="deleteHero(hero, $event)">Delete</button>
|
||||
<!-- #enddocregion delete -->
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- #docregion add-and-error -->
|
||||
<div class="error" *ngIf="error">{{error}}</div>
|
||||
<button (click)="addHero()">Add New Hero</button>
|
||||
<div *ngIf="addingHero">
|
||||
<my-hero-detail (close)="close($event)"></my-hero-detail>
|
||||
<!-- #docregion add -->
|
||||
<div>
|
||||
<label>Hero name:</label> <input #heroName />
|
||||
<button (click)="add(heroName.value); heroName.value=''">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<!-- #enddocregion add-and-error -->
|
||||
|
||||
<!-- #enddocregion add -->
|
||||
<ul class="heroes">
|
||||
<!-- #docregion li-element -->
|
||||
<li *ngFor="let hero of heroes" (click)="onSelect(hero)"
|
||||
[class.selected]="hero === selectedHero">
|
||||
<span class="badge">{{hero.id}}</span>
|
||||
<span>{{hero.name}}</span>
|
||||
<!-- #docregion delete -->
|
||||
<button class="delete"
|
||||
(click)="delete(hero); $event.stopPropagation()">x</button>
|
||||
<!-- #enddocregion delete -->
|
||||
</li>
|
||||
<!-- #enddocregion li-element -->
|
||||
</ul>
|
||||
<div *ngIf="selectedHero">
|
||||
<h2>
|
||||
{{selectedHero.name | uppercase}} is my hero
|
||||
|
|
|
@ -4,57 +4,48 @@ import { Router } from '@angular/router';
|
|||
|
||||
import { Hero } from './hero';
|
||||
import { HeroService } from './hero.service';
|
||||
// #docregion hero-detail-component
|
||||
|
||||
@Component({
|
||||
selector: 'my-heroes',
|
||||
templateUrl: 'app/heroes.component.html',
|
||||
styleUrls: ['app/heroes.component.css']
|
||||
})
|
||||
// #enddocregion hero-detail-component
|
||||
export class HeroesComponent implements OnInit {
|
||||
heroes: Hero[];
|
||||
selectedHero: Hero;
|
||||
addingHero = false;
|
||||
// #docregion error
|
||||
error: any;
|
||||
// #enddocregion error
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private heroService: HeroService) { }
|
||||
private heroService: HeroService,
|
||||
private router: Router) { }
|
||||
|
||||
getHeroes(): void {
|
||||
this.heroService
|
||||
.getHeroes()
|
||||
.then(heroes => this.heroes = heroes)
|
||||
.catch(error => this.error = error);
|
||||
.then(heroes => this.heroes = heroes);
|
||||
}
|
||||
|
||||
// #docregion addHero
|
||||
addHero(): void {
|
||||
this.addingHero = true;
|
||||
this.selectedHero = null;
|
||||
// #docregion add
|
||||
add(name: string): void {
|
||||
name = name.trim();
|
||||
if (!name) { return; }
|
||||
this.heroService.create(name)
|
||||
.then(hero => {
|
||||
this.heroes.push(hero);
|
||||
this.selectedHero = null;
|
||||
});
|
||||
}
|
||||
// #enddocregion add
|
||||
|
||||
close(savedHero: Hero): void {
|
||||
this.addingHero = false;
|
||||
if (savedHero) { this.getHeroes(); }
|
||||
}
|
||||
// #enddocregion addHero
|
||||
|
||||
// #docregion deleteHero
|
||||
deleteHero(hero: Hero, event: any): void {
|
||||
event.stopPropagation();
|
||||
// #docregion delete
|
||||
delete(hero: Hero): void {
|
||||
this.heroService
|
||||
.delete(hero)
|
||||
.then(res => {
|
||||
.delete(hero.id)
|
||||
.then(() => {
|
||||
this.heroes = this.heroes.filter(h => h !== hero);
|
||||
if (this.selectedHero === hero) { this.selectedHero = null; }
|
||||
})
|
||||
.catch(error => this.error = error);
|
||||
});
|
||||
}
|
||||
// #enddocregion deleteHero
|
||||
// #enddocregion delete
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getHeroes();
|
||||
|
@ -62,7 +53,6 @@ export class HeroesComponent implements OnInit {
|
|||
|
||||
onSelect(hero: Hero): void {
|
||||
this.selectedHero = hero;
|
||||
this.addingHero = false;
|
||||
}
|
||||
|
||||
gotoDetail(): void {
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
"nextable": true
|
||||
},
|
||||
"toh-pt6": {
|
||||
"title": "Http",
|
||||
"intro": "We convert our service and components to use Http",
|
||||
"title": "HTTP",
|
||||
"intro": "We convert our service and components to use Angular's HTTP service",
|
||||
"nextable": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ block includes
|
|||
block start-server-and-watch
|
||||
:marked
|
||||
### Keep the app compiling and running
|
||||
|
||||
Open a terminal/console window.
|
||||
Start the Dart compiler, watch for changes, and start our server by entering the command:
|
||||
|
||||
|
@ -25,7 +26,7 @@ block http-library
|
|||
|
||||
### Pubspec updates
|
||||
|
||||
We need to add package dependencies for the
|
||||
Update package dependencies by adding the
|
||||
`stream_transformers` and !{_Angular_http_library}s.
|
||||
|
||||
We also need to add a `resolved_identifiers` entry, to inform the [angular2
|
||||
|
@ -79,30 +80,7 @@ block get-heroes-details
|
|||
:marked
|
||||
To get the list of heroes, we first make an asynchronous call to
|
||||
`http.get()`. Then we use the `_extractData` helper method to decode the
|
||||
response payload (`body`).
|
||||
|
||||
block hero-detail-comp-extra-imports-and-vars
|
||||
//- N/A
|
||||
|
||||
block hero-detail-comp-updates
|
||||
:marked
|
||||
### Edit in the *HeroDetailComponent*
|
||||
|
||||
We already have `HeroDetailComponent` for viewing details about a specific hero.
|
||||
Supporting edit functionality is a natural extension of the detail view,
|
||||
so we are able to reuse `HeroDetailComponent` with a few tweaks.
|
||||
|
||||
block hero-detail-comp-save-and-goback
|
||||
//- N/A
|
||||
|
||||
block add-new-hero-via-detail-comp
|
||||
//- N/A
|
||||
|
||||
block heroes-comp-add
|
||||
//- N/A
|
||||
|
||||
block review
|
||||
//- Not showing animated gif due to differences between TS and Dart implementations.
|
||||
response body.
|
||||
|
||||
block observables-section-intro
|
||||
:marked
|
||||
|
@ -181,8 +159,9 @@ block file-summary
|
|||
toh-6/dart/lib/hero_detail_component.html,
|
||||
toh-6/dart/lib/hero_service.dart,
|
||||
toh-6/dart/lib/heroes_component.css,
|
||||
toh-6/dart/lib/heroes_component.dart`,
|
||||
null,
|
||||
toh-6/dart/lib/heroes_component.dart,
|
||||
toh-6/dart/lib/in_memory_data_service.dart`,
|
||||
',,,,,,,,',
|
||||
`lib/dashboard_component.dart,
|
||||
lib/dashboard_component.html,
|
||||
lib/hero.dart,
|
||||
|
@ -190,7 +169,8 @@ block file-summary
|
|||
lib/hero_detail_component.html,
|
||||
lib/hero_service.dart,
|
||||
lib/heroes_component.css,
|
||||
lib/heroes_component.dart`)
|
||||
lib/heroes_component.dart,
|
||||
lib/in_memory_data_service.dart`)
|
||||
|
||||
+makeTabs(
|
||||
`toh-6/dart/lib/hero_search_component.css,
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
"nextable": true
|
||||
},
|
||||
"toh-pt6": {
|
||||
"title": "Http",
|
||||
"intro": "We convert our service and components to use Http",
|
||||
"title": "HTTP",
|
||||
"intro": "We convert our service and components to use Angular's HTTP service",
|
||||
"nextable": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,11 @@ block includes
|
|||
- var _HttpModule = 'HttpModule'
|
||||
- var _JSON_stringify = 'JSON.stringify'
|
||||
|
||||
//- Shared var definitions
|
||||
- var _promise = _Promise.toLowerCase()
|
||||
|
||||
:marked
|
||||
# Getting and Saving Data with HTTP
|
||||
# Getting and Saving Data using HTTP
|
||||
|
||||
Our stakeholders appreciate our progress.
|
||||
Now they want to get the hero data from a server, let users add, edit, and delete heroes,
|
||||
|
@ -22,12 +25,14 @@ block includes
|
|||
.l-main-section
|
||||
:marked
|
||||
## Where We Left Off
|
||||
|
||||
In the [previous chapter](toh-pt5.html), we learned to navigate between the dashboard and the fixed heroes list, editing a selected hero along the way.
|
||||
That's our starting point for this chapter.
|
||||
|
||||
block start-server-and-watch
|
||||
:marked
|
||||
### Keep the app transpiling and running
|
||||
|
||||
Open a terminal/console window and enter the following command to
|
||||
start the TypeScript compiler, start the server, and watch for changes:
|
||||
|
||||
|
@ -48,7 +53,7 @@ block http-library
|
|||
Fortunately we're ready to import from `@angular/http` because `systemjs.config` configured *SystemJS* to load that library when we need it.
|
||||
|
||||
:marked
|
||||
### Register (provide) *HTTP* services
|
||||
### Register (provide) HTTP services
|
||||
|
||||
block http-providers
|
||||
:marked
|
||||
|
@ -59,7 +64,7 @@ block http-providers
|
|||
So we register them in the `imports` array of `app.module.ts` where we
|
||||
bootstrap the application and its root `AppComponent`.
|
||||
|
||||
+makeExcerpt('app/app.module.ts (v1)')
|
||||
+makeExample('app/app.module.ts', 'v1','app/app.module.ts (v1)')
|
||||
|
||||
:marked
|
||||
Notice that we supply `!{_HttpModule}` as part of the *imports* !{_array} in root NgModule `AppModule`.
|
||||
|
@ -88,10 +93,18 @@ block http-providers
|
|||
|
||||
block backend
|
||||
:marked
|
||||
We're replacing the default `XHRBackend`, the service that talks to the remote server,
|
||||
with the in-memory web API service after priming it as follows:
|
||||
We're importing the `InMemoryWebApiModule` and adding it to the module `imports`.
|
||||
The `InMemoryWebApiModule` replaces the default `Http` client backend —
|
||||
the supporting service that talks to the remote server —
|
||||
with an _in-memory web API alternative service_.
|
||||
|
||||
+makeExample('app/in-memory-data.service.ts', 'init')
|
||||
+makeExcerpt(_appModuleTsVsMainTs, 'in-mem-web-api', '')
|
||||
|
||||
:marked
|
||||
The `forRoot` configuration method takes an `InMemoryDataService` class
|
||||
that primes the in-memory database as follows:
|
||||
|
||||
+makeExample('app/in-memory-data.service.ts', 'init')(format='.')
|
||||
|
||||
p This file replaces the #[code #[+adjExPath('mock-heroes.ts')]] which is now safe to delete.
|
||||
|
||||
|
@ -118,12 +131,21 @@ block dont-be-distracted-by-backend-subst
|
|||
It may have seemed like overkill at the time, but we were anticipating the
|
||||
day when we fetched heroes with an HTTP client and we knew that would have to be an asynchronous operation.
|
||||
|
||||
That day has arrived! Let's convert `getHeroes()` to use HTTP:
|
||||
That day has arrived! Let's convert `getHeroes()` to use HTTP.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts (new constructor and revised getHeroes)', 'getHeroes')
|
||||
+makeExcerpt('app/hero.service.ts (updated getHeroes and new class members)', 'getHeroes')
|
||||
|
||||
:marked
|
||||
### HTTP !{_Promise}
|
||||
Our updated import statements are now:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts (updated imports)', 'imports')
|
||||
|
||||
- var _h3id = `http-${_promise}`
|
||||
:marked
|
||||
Refresh the browser, and the hero data should be successfully loaded from the
|
||||
mock server.
|
||||
|
||||
<h3 id="!{_h3id}">HTTP !{_Promise}</h3>
|
||||
|
||||
We're still returning a !{_Promise} but we're creating it differently.
|
||||
|
||||
|
@ -135,18 +157,23 @@ block get-heroes-details
|
|||
|
||||
For *now* we get back on familiar ground by immediately by
|
||||
converting that `Observable` to a `Promise` using the `toPromise` operator.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'to-promise', '')
|
||||
|
||||
:marked
|
||||
Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ... not out of the box.
|
||||
Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ...
|
||||
not out of the box.
|
||||
The Angular `Observable` is a bare-bones implementation.
|
||||
|
||||
There are scores of operators like `toPromise` that extend `Observable` with useful capabilities.
|
||||
If we want those capabilities, we have to add the operators ourselves.
|
||||
That's as easy as importing them from the RxJS library like this:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'rxjs', '')
|
||||
|
||||
:marked
|
||||
### Extracting the data in the *then* callback
|
||||
|
||||
In the *promise*'s `then` callback we call the `json` method of the http `Response` to extract the
|
||||
data within the response.
|
||||
+makeExcerpt('app/hero.service.ts', 'to-data', '')
|
||||
|
@ -160,15 +187,14 @@ block get-heroes-details
|
|||
:marked
|
||||
Pay close attention to the shape of the data returned by the server.
|
||||
This particular *in-memory web API* example happens to return an object with a `data` property.
|
||||
Your API might return something else.
|
||||
Your API might return something else. Adjust the code to match *your web API*.
|
||||
|
||||
Adjust the code to match *your web API*.
|
||||
:marked
|
||||
The caller is unaware of these machinations. It receives a !{_Promise} of *heroes* just as it did before.
|
||||
It has no idea that we fetched the heroes from the (mock) server.
|
||||
It knows nothing of the twists and turns required to convert the HTTP response into heroes.
|
||||
Such is the beauty and purpose of delegating data access to a service like this `HeroService`.
|
||||
:marked
|
||||
|
||||
### Error Handling
|
||||
|
||||
At the end of `getHeroes()` we `catch` server failures and pass them to an error handler:
|
||||
|
@ -183,173 +209,139 @@ block get-heroes-details
|
|||
|
||||
- var rejected_promise = _docsFor == 'dart' ? 'propagated exception' : 'rejected promise';
|
||||
:marked
|
||||
In this demo service we log the error to the console; we should do better in real life.
|
||||
In this demo service we log the error to the console; we would do better in real life.
|
||||
|
||||
We've also decided to return a user friendly form of the error to
|
||||
the caller in a !{rejected_promise} so that the caller can display a proper error message to the user.
|
||||
|
||||
### !{_Promise}s are !{_Promise}s
|
||||
### Unchanged `getHeroes` API
|
||||
|
||||
Although we made significant *internal* changes to `getHeroes()`, the public signature did not change.
|
||||
We still return a !{_Promise}. We won't have to update any of the components that call `getHeroes()`.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Add, Edit, Delete
|
||||
Our stakeholders are incredibly pleased with the added flexibility from the API integration, but it doesn't stop there. Next, we want the ability to create new heroes and delete heroes.
|
||||
|
||||
Our stakeholders are incredibly pleased with the added flexibility from the API integration, but it doesn't stop there. Next we want to add the capability to add, edit and delete heroes.
|
||||
|
||||
We'll complete `HeroService` by creating `post`, `put` and `delete` methods to meet our new requirements.
|
||||
|
||||
:marked
|
||||
### Post
|
||||
|
||||
We will be using `post` to add new heroes. Post requests require a little bit more setup than Get requests:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'post')
|
||||
|
||||
:marked
|
||||
For Post requests we create a header and set the content type to `application/json`. We'll call `!{_JSON_stringify}` before we post to convert the hero object to a string.
|
||||
|
||||
### Put
|
||||
|
||||
Put will be used to update an individual hero. Its structure is very similar to Post requests. The only difference is that we have to change the URL slightly by appending the id of the hero we want to update.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'put')
|
||||
|
||||
:marked
|
||||
### Delete
|
||||
Delete will be used to delete heroes and its format is like `put` except for the function name.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'delete')
|
||||
|
||||
:marked
|
||||
We add a `catch` to handle errors for all three methods.
|
||||
|
||||
:marked
|
||||
### Save
|
||||
|
||||
We combine the call to the private `post` and `put` methods in a single `save` method. This simplifies the public API and makes the integration with `HeroDetailComponent` easier. `HeroService` determines which method to call based on the state of the `hero` object. If the hero already has an id we know it's an edit. Otherwise we know it's an add.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'save')
|
||||
|
||||
:marked
|
||||
After these additions our `HeroService` looks like this:
|
||||
|
||||
+makeExample('app/hero.service.ts')
|
||||
But first, let's see what happens now when we try to update a hero's details.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Updating Components
|
||||
## Update hero details
|
||||
|
||||
Loading heroes using `Http` required no changes outside of `HeroService`, but we added a few new features as well.
|
||||
In the following section we will update our components to use our new methods to add, edit and delete heroes.
|
||||
The hero detail view already allows us to edit a hero's name. Go ahead, try
|
||||
it now. As we type, the hero name is updated in the view heading, but
|
||||
notice what happens when we hit the `Back` button: the changes are lost!
|
||||
|
||||
block hero-detail-comp-extra-imports-and-vars
|
||||
.l-sub-section
|
||||
:marked
|
||||
Before we can add those methods, we need to initialize some variables with their respective imports.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts ()', 'variables-imports')
|
||||
|
||||
block hero-detail-comp-updates
|
||||
:marked
|
||||
### Add/Edit in the *HeroDetailComponent*
|
||||
|
||||
We already have `HeroDetailComponent` for viewing details about a specific hero.
|
||||
Add and Edit are natural extensions of the detail view, so we are able to reuse `HeroDetailComponent` with a few tweaks.
|
||||
|
||||
The original component was created to render existing data, but to add new data we have to initialize the `hero` property to an empty `Hero` object.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'ngOnInit')
|
||||
|
||||
:marked
|
||||
In order to differentiate between add and edit we are adding a check to see if an id is passed in the URL. If the id is absent we bind `HeroDetailComponent` to an empty `Hero` object. In either case, any edits made through the UI will be bound back to the same `hero` property.
|
||||
Updates weren't lost before, what's happening?
|
||||
When the app used a list of mock heroes, changes were made directly to the
|
||||
hero objects in the single, app-wide shared list. Now that we are fetching data
|
||||
from a server, if we want changes to persist, we'll need to write them back to
|
||||
the server.
|
||||
|
||||
:marked
|
||||
Add a save method to `HeroDetailComponent` and call the corresponding save method in `HeroesService`.
|
||||
### Save hero details
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'save')
|
||||
|
||||
block hero-detail-comp-save-and-goback
|
||||
:marked
|
||||
The same save method is used for both add and edit since `HeroService` will know when to call `post` vs `put` based on the state of the `Hero` object.
|
||||
|
||||
After we save a hero, we redirect the browser back to the previous page using the `goBack()` method.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'goBack')
|
||||
|
||||
:marked
|
||||
Here we call `emit` to notify that we just added or modified a hero. `HeroesComponent` is listening for this notification and will automatically refresh the list of heroes to include our recent updates.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `emit` "handshake" between `HeroDetailComponent` and `HeroesComponent` is an example of component to component communication. This is a topic for another day, but we have detailed information in our <a href="/docs/ts/latest/cookbook/component-communication.html#!#child-to-parent">Component Interaction Cookbook</a>
|
||||
|
||||
:marked
|
||||
Here is `HeroDetailComponent` with its new save button and the corresponding HTML.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/hero-details-save-button.png' alt="Hero Details With Save Button")
|
||||
Let's ensure that edits to a hero's name aren't lost. Start by adding,
|
||||
to the end of the hero detail template, a save button with a `click` event
|
||||
binding that invokes a new component method named `save`:
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.html', 'save')
|
||||
|
||||
:marked
|
||||
### Add/Delete in the *HeroesComponent*
|
||||
The `save` method persists hero name changes using the hero service
|
||||
`update` method and then navigates back to the previous view:
|
||||
|
||||
We'll be reporting propagated HTTP errors, let's start by adding the following
|
||||
field to the `HeroesComponent` class:
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'error', '')
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'save')
|
||||
|
||||
:marked
|
||||
The user can *add* a new hero by clicking a button and entering a name.
|
||||
### Hero service `update` method
|
||||
|
||||
block add-new-hero-via-detail-comp
|
||||
:marked
|
||||
When the user clicks the *Add New Hero* button, we display the `HeroDetailComponent`.
|
||||
We aren't navigating to the component so it won't receive a hero `id`;
|
||||
as we noted above, that is the component's cue to create and present an empty hero.
|
||||
The overall structure of the `update` method is similar to that of
|
||||
`getHeroes`, although we'll use an HTTP _put_ to persist changes
|
||||
server-side:
|
||||
|
||||
- var _below = _docsFor == 'dart' ? 'before' : 'below';
|
||||
:marked
|
||||
Add the following to the heroes component HTML, just !{_below} the hero list (`<ul class="heroes">...</ul>`).
|
||||
+makeExcerpt('app/heroes.component.html', 'add-and-error')
|
||||
:marked
|
||||
The first line will display an error message if there is any. The remaining HTML is for adding heroes.
|
||||
+makeExcerpt('app/hero.service.ts', 'update')
|
||||
|
||||
The user can *delete* an existing hero by clicking a delete button next to the hero's name.
|
||||
Add the following to the heroes component HTML right after the hero name in the repeated `<li>` tag:
|
||||
+makeExcerpt('app/heroes.component.html', 'delete')
|
||||
:marked
|
||||
Add the following to the bottom of the `HeroesComponent` CSS file:
|
||||
We identify _which_ hero the server should update by encoding the hero id in
|
||||
the URL. The put body is the JSON string encoding of the hero, obtained by
|
||||
calling `!{_JSON_stringify}`. We identify the body content type
|
||||
(`application/json`) in the request header.
|
||||
|
||||
Refresh the browser and give it a try. Changes to hero names should now persist.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Add a hero
|
||||
|
||||
To add a new hero we need to know the hero's name. Let's use an input
|
||||
element for that, paired with an add button.
|
||||
|
||||
Insert the following into the heroes component HTML, first thing after
|
||||
the heading:
|
||||
|
||||
+makeExcerpt('app/heroes.component.html', 'add')
|
||||
|
||||
:marked
|
||||
In response to a click event, we call the component's click handler and then
|
||||
clear the input field so that it will be ready to use for another name.
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'add')
|
||||
|
||||
:marked
|
||||
When the given name is non-blank, the handler delegates creation of the
|
||||
named hero to the hero service, and then adds the new hero to our !{_array}.
|
||||
|
||||
Go ahead, refresh the browser and create some new heroes!
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Delete a hero
|
||||
|
||||
Too many heroes?
|
||||
Let's add a delete button to each hero in the heroes view.
|
||||
|
||||
Add this button element to the heroes component HTML, right after the hero
|
||||
name in the repeated `<li>` tag:
|
||||
|
||||
+makeExcerpt('app/heroes.component.html', 'delete', '')
|
||||
|
||||
:marked
|
||||
The `<li>` element should now look like this:
|
||||
|
||||
+makeExcerpt('app/heroes.component.html', 'li-element')
|
||||
|
||||
:marked
|
||||
In addition to calling the component's `delete` method, the delete button
|
||||
click handling code stops the propagation of the click event — we
|
||||
don't want the `<li>` click handler to be triggered because that would
|
||||
select the hero that we are going to delete!
|
||||
|
||||
The logic of the `delete` handler is a bit trickier:
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'delete')
|
||||
|
||||
:marked
|
||||
Of course, we delegate hero deletion to the hero service, but the component
|
||||
is still responsible for updating the display: it removes the deleted hero
|
||||
from the !{_array} and resets the selected hero if necessary.
|
||||
|
||||
:marked
|
||||
We want our delete button to be placed at the far right of the hero entry.
|
||||
This extra CSS accomplishes that:
|
||||
|
||||
+makeExcerpt('app/heroes.component.css', 'additions')
|
||||
:marked
|
||||
Now let's fix-up the `HeroesComponent` to support the *add* and *delete* actions used in the template.
|
||||
Let's start with *add*.
|
||||
|
||||
Implement the click handler for the *Add New Hero* button.
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'addHero')
|
||||
|
||||
block heroes-comp-add
|
||||
:marked
|
||||
The `HeroDetailComponent` does most of the work. All we do is toggle an `*ngIf` flag that
|
||||
swaps it into the DOM when we add a hero and removes it from the DOM when the user is done.
|
||||
|
||||
:marked
|
||||
The *delete* logic is a bit trickier.
|
||||
+makeExcerpt('app/heroes.component.ts', 'deleteHero')
|
||||
### Hero service `delete` method
|
||||
|
||||
The hero service's `delete` method uses the _delete_ HTTP method to remove the hero from the server:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'delete')
|
||||
|
||||
:marked
|
||||
Of course we delegate the persistence of hero deletion to the `HeroService`.
|
||||
But the component is still responsible for updating the display.
|
||||
So the *delete* method removes the deleted hero from the list.
|
||||
|
||||
block review
|
||||
:marked
|
||||
### Let's see it
|
||||
Here are the fruits of labor in action:
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editing w/ HTTP")
|
||||
Refresh the browser and try the new delete functionality.
|
||||
|
||||
:marked
|
||||
## !{_Observable}s
|
||||
|
@ -500,24 +492,26 @@ block observable-transformers
|
|||
We take a different approach in this example.
|
||||
We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file.
|
||||
|
||||
+makeExample('app/rxjs-extensions.ts')
|
||||
+makeExample('app/rxjs-extensions.ts')(format='.')
|
||||
|
||||
:marked
|
||||
We load them all at once by importing `rxjs-extensions` in `AppComponent`.
|
||||
|
||||
+makeExcerpt('app/app.component.ts', 'rxjs-extensions')
|
||||
+makeExcerpt('app/app.component.ts', 'rxjs-extensions')(format='.')
|
||||
|
||||
:marked
|
||||
### Add the search component to the dashboard
|
||||
|
||||
We add the hero search HTML element to the bottom of the `DashboardComponent` template.
|
||||
|
||||
+makeExample('app/dashboard.component.html')
|
||||
+makeExample('app/dashboard.component.html')(format='.')
|
||||
|
||||
- var _declarations = _docsFor == 'dart' ? 'directives' : 'declarations'
|
||||
- var declFile = _docsFor == 'dart' ? 'app/dashboard.component.ts' : 'app/app.module.ts'
|
||||
:marked
|
||||
And finally, we import the `HeroSearchComponent` and add it to the `!{_declarations}` !{_array}:
|
||||
And finally, we import `HeroSearchComponent` from
|
||||
<span ngio-ex>hero-search.component.ts</span>
|
||||
and add it to the `!{_declarations}` !{_array}:
|
||||
|
||||
+makeExcerpt(declFile, 'search')
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
"nextable": true
|
||||
},
|
||||
"toh-pt6": {
|
||||
"title": "Http",
|
||||
"intro": "We convert our service and components to use Http",
|
||||
"title": "HTTP",
|
||||
"intro": "We convert our service and components to use Angular's HTTP service",
|
||||
"nextable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,11 @@ block includes
|
|||
- var _HttpModule = 'HttpModule'
|
||||
- var _JSON_stringify = 'JSON.stringify'
|
||||
|
||||
//- Shared var definitions
|
||||
- var _promise = _Promise.toLowerCase()
|
||||
|
||||
:marked
|
||||
# Getting and Saving Data with HTTP
|
||||
# Getting and Saving Data
|
||||
|
||||
Our stakeholders appreciate our progress.
|
||||
Now they want to get the hero data from a server, let users add, edit, and delete heroes,
|
||||
|
@ -22,12 +25,14 @@ block includes
|
|||
.l-main-section
|
||||
:marked
|
||||
## Where We Left Off
|
||||
|
||||
In the [previous chapter](toh-pt5.html), we learned to navigate between the dashboard and the fixed heroes list, editing a selected hero along the way.
|
||||
That's our starting point for this chapter.
|
||||
|
||||
block start-server-and-watch
|
||||
:marked
|
||||
### Keep the app transpiling and running
|
||||
|
||||
Open a terminal/console window and enter the following command to
|
||||
start the TypeScript compiler, start the server, and watch for changes:
|
||||
|
||||
|
@ -41,22 +46,22 @@ block start-server-and-watch
|
|||
h1 Providing HTTP Services
|
||||
block http-library
|
||||
:marked
|
||||
`Http` is ***not*** a core Angular module.
|
||||
The `HttpModule` is ***not*** a core Angular module.
|
||||
It's Angular's optional approach to web access and it exists as a separate add-on module called `@angular/http`,
|
||||
shipped in a separate script file as part of the Angular npm package.
|
||||
|
||||
Fortunately we're ready to import from `@angular/http` because `systemjs.config` configured *SystemJS* to load that library when we need it.
|
||||
|
||||
:marked
|
||||
### Register (provide) *HTTP* services
|
||||
### Register for HTTP services
|
||||
|
||||
block http-providers
|
||||
:marked
|
||||
Our app will depend upon the Angular `http` service which itself depends upon other supporting services.
|
||||
The `HttpModule` from `@angular/http` library holds providers for the complete set of `http` services.
|
||||
The `HttpModule` from `@angular/http` library holds providers for a complete set of HTTP services.
|
||||
|
||||
We should be able to access `http` services from anywhere in the application.
|
||||
So we register them in the `imports` array of `app.module.ts` where we
|
||||
We should be able to access these services from anywhere in the application.
|
||||
So we register them all by adding `HttpModule` to the `imports` list of the `AppModule` where we
|
||||
bootstrap the application and its root `AppComponent`.
|
||||
|
||||
+makeExample('app/app.module.ts', 'v1','app/app.module.ts (v1)')
|
||||
|
@ -92,10 +97,12 @@ block backend
|
|||
The `InMemoryWebApiModule` replaces the default `Http` client backend —
|
||||
the supporting service that talks to the remote server —
|
||||
with an _in-memory web API alternative service_.
|
||||
|
||||
+makeExcerpt(_appModuleTsVsMainTs, 'in-mem-web-api', '')
|
||||
|
||||
:marked
|
||||
The `forRoot` configuration method takes an `InMemoryDataService` class
|
||||
that will prime the in-memory database as follows:
|
||||
The `forRoot` configuration method takes an `InMemoryDataService` class
|
||||
that primes the in-memory database as follows:
|
||||
|
||||
+makeExample('app/in-memory-data.service.ts', 'init')(format='.')
|
||||
|
||||
|
@ -124,12 +131,21 @@ block dont-be-distracted-by-backend-subst
|
|||
It may have seemed like overkill at the time, but we were anticipating the
|
||||
day when we fetched heroes with an HTTP client and we knew that would have to be an asynchronous operation.
|
||||
|
||||
That day has arrived! Let's convert `getHeroes()` to use HTTP:
|
||||
That day has arrived! Let's convert `getHeroes()` to use HTTP.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts (new constructor and revised getHeroes)', 'getHeroes')
|
||||
+makeExcerpt('app/hero.service.ts (updated getHeroes and new class members)', 'getHeroes')
|
||||
|
||||
:marked
|
||||
### HTTP !{_Promise}
|
||||
Our updated import statements are now:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts (updated imports)', 'imports')
|
||||
|
||||
- var _h3id = `http-${_promise}`
|
||||
:marked
|
||||
Refresh the browser, and the hero data should be successfully loaded from the
|
||||
mock server.
|
||||
|
||||
<h3 id="!{_h3id}">HTTP !{_Promise}</h3>
|
||||
|
||||
We're still returning a !{_Promise} but we're creating it differently.
|
||||
|
||||
|
@ -141,19 +157,24 @@ block get-heroes-details
|
|||
|
||||
For *now* we get back on familiar ground by immediately by
|
||||
converting that `Observable` to a `Promise` using the `toPromise` operator.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'to-promise', '')
|
||||
|
||||
:marked
|
||||
Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ... not out of the box.
|
||||
Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ...
|
||||
not out of the box.
|
||||
The Angular `Observable` is a bare-bones implementation.
|
||||
|
||||
There are scores of operators like `toPromise` that extend `Observable` with useful capabilities.
|
||||
If we want those capabilities, we have to add the operators ourselves.
|
||||
That's as easy as importing them from the RxJS library like this:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'rxjs', '')
|
||||
|
||||
:marked
|
||||
### Extracting the data in the *then* callback
|
||||
In the *promise*'s `then` callback we call the `json` method of the http `Response` to extract the
|
||||
|
||||
In the *promise*'s `then` callback we call the `json` method of the HTTP `Response` to extract the
|
||||
data within the response.
|
||||
+makeExcerpt('app/hero.service.ts', 'to-data', '')
|
||||
|
||||
|
@ -166,15 +187,14 @@ block get-heroes-details
|
|||
:marked
|
||||
Pay close attention to the shape of the data returned by the server.
|
||||
This particular *in-memory web API* example happens to return an object with a `data` property.
|
||||
Your API might return something else.
|
||||
Your API might return something else. Adjust the code to match *your web API*.
|
||||
|
||||
Adjust the code to match *your web API*.
|
||||
:marked
|
||||
The caller is unaware of these machinations. It receives a !{_Promise} of *heroes* just as it did before.
|
||||
It has no idea that we fetched the heroes from the (mock) server.
|
||||
It knows nothing of the twists and turns required to convert the HTTP response into heroes.
|
||||
Such is the beauty and purpose of delegating data access to a service like this `HeroService`.
|
||||
:marked
|
||||
|
||||
### Error Handling
|
||||
|
||||
At the end of `getHeroes()` we `catch` server failures and pass them to an error handler:
|
||||
|
@ -189,180 +209,147 @@ block get-heroes-details
|
|||
|
||||
- var rejected_promise = _docsFor == 'dart' ? 'propagated exception' : 'rejected promise';
|
||||
:marked
|
||||
In this demo service we log the error to the console; we should do better in real life.
|
||||
In this demo service we log the error to the console; we would do better in real life.
|
||||
|
||||
We've also decided to return a user friendly form of the error to
|
||||
the caller in a !{rejected_promise} so that the caller can display a proper error message to the user.
|
||||
|
||||
### !{_Promise}s are !{_Promise}s
|
||||
### Unchanged `getHeroes` API
|
||||
|
||||
Although we made significant *internal* changes to `getHeroes()`, the public signature did not change.
|
||||
We still return a !{_Promise}. We won't have to update any of the components that call `getHeroes()`.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Add, Edit, Delete
|
||||
Our stakeholders are thrilled with the added flexibility from the API integration.
|
||||
Now they want the ability to create and delete heroes.
|
||||
|
||||
Our stakeholders are incredibly pleased with the added flexibility from the API integration, but it doesn't stop there. Next we want to add the capability to add, edit and delete heroes.
|
||||
|
||||
We'll complete `HeroService` by creating `post`, `put` and `delete` methods to meet our new requirements.
|
||||
|
||||
:marked
|
||||
### Post
|
||||
|
||||
We will be using `post` to add new heroes. Post requests require a little bit more setup than Get requests:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'post')
|
||||
|
||||
:marked
|
||||
For Post requests we create a header and set the content type to `application/json`. We'll call `!{_JSON_stringify}` before we post to convert the hero object to a string.
|
||||
|
||||
### Put
|
||||
|
||||
Put will be used to update an individual hero. Its structure is very similar to Post requests. The only difference is that we have to change the URL slightly by appending the id of the hero we want to update.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'put')
|
||||
|
||||
:marked
|
||||
### Delete
|
||||
Delete will be used to delete heroes and its format is like `put` except for the function name.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'delete')
|
||||
|
||||
:marked
|
||||
We add a `catch` to handle errors for all three methods.
|
||||
|
||||
:marked
|
||||
### Save
|
||||
|
||||
We combine the call to the private `post` and `put` methods in a single `save` method. This simplifies the public API and makes the integration with `HeroDetailComponent` easier. `HeroService` determines which method to call based on the state of the `hero` object. If the hero already has an id we know it's an edit. Otherwise we know it's an add.
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'save')
|
||||
|
||||
:marked
|
||||
After these additions our `HeroService` looks like this:
|
||||
|
||||
+makeExample('app/hero.service.ts')
|
||||
Let's see first what happens when we try to update a hero's details.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Updating Components
|
||||
## Update hero details
|
||||
|
||||
Loading heroes using `Http` required no changes outside of `HeroService`, but we added a few new features as well.
|
||||
In the following section we will update our components to use our new methods to add, edit and delete heroes.
|
||||
We can edit a hero's name already in the hero detail view. Go ahead and try
|
||||
it. As we type, the hero name is updated in the view heading.
|
||||
But when we hit the `Back` button, the changes are lost!
|
||||
|
||||
block hero-detail-comp-extra-imports-and-vars
|
||||
.l-sub-section
|
||||
:marked
|
||||
Before we can add those methods, we need to initialize some variables with their respective imports.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts ()', 'variables-imports')
|
||||
|
||||
block hero-detail-comp-updates
|
||||
:marked
|
||||
### Add/Edit in the *HeroDetailComponent*
|
||||
|
||||
We already have `HeroDetailComponent` for viewing details about a specific hero.
|
||||
Add and Edit are natural extensions of the detail view, so we are able to reuse `HeroDetailComponent` with a few tweaks.
|
||||
|
||||
The original component was created to render existing data, but to add new data we have to initialize the `hero` property to an empty `Hero` object.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'ngOnInit')
|
||||
|
||||
:marked
|
||||
In order to differentiate between add and edit we are adding a check to see if an id is passed in the URL. If the id is absent we bind `HeroDetailComponent` to an empty `Hero` object. In either case, any edits made through the UI will be bound back to the same `hero` property.
|
||||
Updates weren't lost before, what's happening?
|
||||
When the app used a list of mock heroes, changes were made directly to the
|
||||
hero objects in the single, app-wide shared list. Now that we are fetching data
|
||||
from a server, if we want changes to persist, we'll need to write them back to
|
||||
the server.
|
||||
|
||||
:marked
|
||||
Add a save method to `HeroDetailComponent` and call the corresponding save method in `HeroesService`.
|
||||
### Save hero details
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'save')
|
||||
|
||||
block hero-detail-comp-save-and-goback
|
||||
:marked
|
||||
The same save method is used for both add and edit since `HeroService` will know when to call `post` vs `put` based on the state of the `Hero` object.
|
||||
|
||||
After we save a hero, we redirect the browser back to the previous page using the `goBack()` method.
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'goBack')
|
||||
|
||||
:marked
|
||||
Here we call `emit` to notify that we just added or modified a hero. `HeroesComponent` is listening for this notification and will automatically refresh the list of heroes to include our recent updates.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The `emit` "handshake" between `HeroDetailComponent` and `HeroesComponent` is an example of component to component communication. This is a topic for another day, but we have detailed information in our <a href="/docs/ts/latest/cookbook/component-communication.html#!#child-to-parent">Component Interaction Cookbook</a>
|
||||
|
||||
:marked
|
||||
Here is `HeroDetailComponent` with its new save button and the corresponding HTML.
|
||||
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/hero-details-save-button.png' alt="Hero Details With Save Button")
|
||||
Let's ensure that edits to a hero's name aren't lost. Start by adding,
|
||||
to the end of the hero detail template, a save button with a `click` event
|
||||
binding that invokes a new component method named `save`:
|
||||
|
||||
+makeExcerpt('app/hero-detail.component.html', 'save')
|
||||
|
||||
:marked
|
||||
### Add/Delete in the *HeroesComponent*
|
||||
The `save` method persists hero name changes using the hero service
|
||||
`update` method and then navigates back to the previous view:
|
||||
|
||||
We'll be reporting propagated HTTP errors, let's start by adding the following
|
||||
field to the `HeroesComponent` class:
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'error', '')
|
||||
+makeExcerpt('app/hero-detail.component.ts', 'save')
|
||||
|
||||
:marked
|
||||
The user can *add* a new hero by clicking a button and entering a name.
|
||||
### Hero service `update` method
|
||||
|
||||
block add-new-hero-via-detail-comp
|
||||
:marked
|
||||
When the user clicks the *Add New Hero* button, we display the `HeroDetailComponent`.
|
||||
We aren't navigating to the component so it won't receive a hero `id`;
|
||||
as we noted above, that is the component's cue to create and present an empty hero.
|
||||
The overall structure of the `update` method is similar to that of
|
||||
`getHeroes`, although we'll use an HTTP _put_ to persist changes
|
||||
server-side:
|
||||
|
||||
- var _below = _docsFor == 'dart' ? 'before' : 'below';
|
||||
:marked
|
||||
Add the following to the heroes component HTML, just !{_below} the hero list (`<ul class="heroes">...</ul>`).
|
||||
+makeExcerpt('app/heroes.component.html', 'add-and-error')
|
||||
:marked
|
||||
The first line will display an error message if there is any. The remaining HTML is for adding heroes.
|
||||
+makeExcerpt('app/hero.service.ts', 'update')
|
||||
|
||||
The user can *delete* an existing hero by clicking a delete button next to the hero's name.
|
||||
Add the following to the heroes component HTML right after the hero name in the repeated `<li>` tag:
|
||||
+makeExcerpt('app/heroes.component.html', 'delete')
|
||||
:marked
|
||||
Add the following to the bottom of the `HeroesComponent` CSS file:
|
||||
We identify _which_ hero the server should update by encoding the hero id in
|
||||
the URL. The put body is the JSON string encoding of the hero, obtained by
|
||||
calling `!{_JSON_stringify}`. We identify the body content type
|
||||
(`application/json`) in the request header.
|
||||
|
||||
Refresh the browser and give it a try. Changes to hero names should now persist.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Add a hero
|
||||
|
||||
To add a new hero we need to know the hero's name. Let's use an input
|
||||
element for that, paired with an add button.
|
||||
|
||||
Insert the following into the heroes component HTML, first thing after
|
||||
the heading:
|
||||
|
||||
+makeExcerpt('app/heroes.component.html', 'add')
|
||||
|
||||
:marked
|
||||
In response to a click event, we call the component's click handler and then
|
||||
clear the input field so that it will be ready to use for another name.
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'add')
|
||||
|
||||
:marked
|
||||
When the given name is non-blank, the handler delegates creation of the
|
||||
named hero to the hero service, and then adds the new hero to our !{_array}.
|
||||
|
||||
Go ahead, refresh the browser and create some new heroes!
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Delete a hero
|
||||
|
||||
Too many heroes?
|
||||
Let's add a delete button to each hero in the heroes view.
|
||||
|
||||
Add this button element to the heroes component HTML, right after the hero
|
||||
name in the repeated `<li>` tag:
|
||||
|
||||
+makeExcerpt('app/heroes.component.html', 'delete', '')
|
||||
|
||||
:marked
|
||||
The `<li>` element should now look like this:
|
||||
|
||||
+makeExcerpt('app/heroes.component.html', 'li-element')
|
||||
|
||||
:marked
|
||||
In addition to calling the component's `delete` method, the delete button
|
||||
click handling code stops the propagation of the click event — we
|
||||
don't want the `<li>` click handler to be triggered because that would
|
||||
select the hero that we are going to delete!
|
||||
|
||||
The logic of the `delete` handler is a bit trickier:
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'delete')
|
||||
|
||||
:marked
|
||||
Of course, we delegate hero deletion to the hero service, but the component
|
||||
is still responsible for updating the display: it removes the deleted hero
|
||||
from the !{_array} and resets the selected hero if necessary.
|
||||
|
||||
:marked
|
||||
We want our delete button to be placed at the far right of the hero entry.
|
||||
This extra CSS accomplishes that:
|
||||
|
||||
+makeExcerpt('app/heroes.component.css', 'additions')
|
||||
:marked
|
||||
Now let's fix-up the `HeroesComponent` to support the *add* and *delete* actions used in the template.
|
||||
Let's start with *add*.
|
||||
|
||||
Implement the click handler for the *Add New Hero* button.
|
||||
|
||||
+makeExcerpt('app/heroes.component.ts', 'addHero')
|
||||
|
||||
block heroes-comp-add
|
||||
:marked
|
||||
The `HeroDetailComponent` does most of the work. All we do is toggle an `*ngIf` flag that
|
||||
swaps it into the DOM when we add a hero and removes it from the DOM when the user is done.
|
||||
|
||||
:marked
|
||||
The *delete* logic is a bit trickier.
|
||||
+makeExcerpt('app/heroes.component.ts', 'deleteHero')
|
||||
### Hero service `delete` method
|
||||
|
||||
The hero service's `delete` method uses the _delete_ HTTP method to remove the hero from the server:
|
||||
|
||||
+makeExcerpt('app/hero.service.ts', 'delete')
|
||||
|
||||
:marked
|
||||
Of course we delegate the persistence of hero deletion to the `HeroService`.
|
||||
But the component is still responsible for updating the display.
|
||||
So the *delete* method removes the deleted hero from the list.
|
||||
|
||||
block review
|
||||
:marked
|
||||
### Let's see it
|
||||
Here are the fruits of labor in action:
|
||||
figure.image-display
|
||||
img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editing w/ HTTP")
|
||||
Refresh the browser and try the new delete functionality.
|
||||
|
||||
:marked
|
||||
## !{_Observable}s
|
||||
|
||||
block observables-section-intro
|
||||
:marked
|
||||
Each `Http` method returns an `Observable` of HTTP `Response` objects.
|
||||
Each `Http` service method returns an `Observable` of HTTP `Response` objects.
|
||||
|
||||
Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller.
|
||||
In this section we learn to return the `Observable` directly and discuss when and why that might be
|
||||
|
@ -378,7 +365,7 @@ block observables-section-intro
|
|||
Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`.
|
||||
That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller.
|
||||
|
||||
Converting to a promise is often a good choice. We typically ask `http` to fetch a single chunk of data.
|
||||
Converting to a promise is often a good choice. We typically ask `http.get` to fetch a single chunk of data.
|
||||
When we receive the data, we're done.
|
||||
A single result in the form of a promise is easy for the calling component to consume
|
||||
and it helps that promises are widely understood by JavaScript programmers.
|
||||
|
@ -470,21 +457,21 @@ block observable-transformers
|
|||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The [switchMap operator](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/flatmaplatest.md)
|
||||
The [switchMap operator](http://www.learnrxjs.io/operators/transformation/switchmap.html)
|
||||
(formerly known as "flatMapLatest") is very clever.
|
||||
|
||||
Every qualifying key event can trigger an http call.
|
||||
Even with a 300ms pause between requests, we could have multiple http requests in flight
|
||||
Every qualifying key event can trigger an `http` method call.
|
||||
Even with a 300ms pause between requests, we could have multiple HTTP requests in flight
|
||||
and they may not return in the order sent.
|
||||
|
||||
`switchMap` preserves the original request order while returning
|
||||
only the observable from the most recent http call.
|
||||
only the observable from the most recent `http` method call.
|
||||
Results from prior calls are canceled and discarded.
|
||||
|
||||
We also short-circuit the http call and return an observable containing an empty array
|
||||
We also short-circuit the `http` method call and return an observable containing an empty array
|
||||
if the search text is empty.
|
||||
|
||||
Note that _canceling_ the `HeroSearchService` observable won't actually abort a pending http request
|
||||
Note that _canceling_ the `HeroSearchService` observable won't actually abort a pending HTTP request
|
||||
until the service supports that feature, a topic for another day.
|
||||
We are content for now to discard unwanted results.
|
||||
:marked
|
||||
|
@ -509,9 +496,9 @@ block observable-transformers
|
|||
+makeExample('app/rxjs-extensions.ts')(format='.')
|
||||
|
||||
:marked
|
||||
We load them all at once by importing `rxjs-extensions` in `AppComponent`.
|
||||
We load them all at once by importing `rxjs-extensions` at the top of `AppModule`.
|
||||
|
||||
+makeExcerpt('app/app.component.ts', 'rxjs-extensions')(format='.')
|
||||
+makeExcerpt('app/app.module.ts', 'rxjs-extensions')(format='.')
|
||||
|
||||
:marked
|
||||
### Add the search component to the dashboard
|
||||
|
@ -523,7 +510,8 @@ block observable-transformers
|
|||
- var _declarations = _docsFor == 'dart' ? 'directives' : 'declarations'
|
||||
- var declFile = _docsFor == 'dart' ? 'app/dashboard.component.ts' : 'app/app.module.ts'
|
||||
:marked
|
||||
And finally, we import the `HeroSearchComponent` from `'./hero-search.component.ts'`
|
||||
Finally, we import `HeroSearchComponent` from
|
||||
<span ngio-ex>hero-search.component.ts</span>
|
||||
and add it to the `!{_declarations}` !{_array}:
|
||||
|
||||
+makeExcerpt(declFile, 'search')
|
||||
|
|
Loading…
Reference in New Issue