diff --git a/public/docs/_examples/toh-6/dart/example-config.json b/public/docs/_examples/toh-6/dart/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/toh-6/dart/lib/hero_detail_component.dart b/public/docs/_examples/toh-6/dart/lib/hero_detail_component.dart index 39f9f531f5..5e7a1b6168 100644 --- a/public/docs/_examples/toh-6/dart/lib/hero_detail_component.dart +++ b/public/docs/_examples/toh-6/dart/lib/hero_detail_component.dart @@ -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 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 save() async { - await _heroService.save(hero); + await _heroService.update(hero); goBack(); } // #enddocregion save - // #docregion go-back void goBack() { window.history.back(); } - // #enddocregion go-back } diff --git a/public/docs/_examples/toh-6/dart/lib/hero_detail_component.html b/public/docs/_examples/toh-6/dart/lib/hero_detail_component.html index d15546af74..161dc2246e 100644 --- a/public/docs/_examples/toh-6/dart/lib/hero_detail_component.html +++ b/public/docs/_examples/toh-6/dart/lib/hero_detail_component.html @@ -1,4 +1,3 @@ -

{{hero.name}} details!

diff --git a/public/docs/_examples/toh-6/dart/lib/hero_service.dart b/public/docs/_examples/toh-6/dart/lib/hero_service.dart index 7a15c6473f..38e8357968 100644 --- a/public/docs/_examples/toh-6/dart/lib/hero_service.dart +++ b/public/docs/_examples/toh-6/dart/lib/hero_service.dart @@ -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 getHero(int id) async => - (await getHeroes()).firstWhere((hero) => hero.id == id); - - // #docregion save - Future 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 + + Future getHero(int id) async => + (await getHeroes()).firstWhere((hero) => hero.id == id); - // #docregion post - Future _post(String name) async { + // #docregion create + Future 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 _put(Hero hero) async { + Future 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 delete(int id) async { diff --git a/public/docs/_examples/toh-6/dart/lib/heroes_component.css b/public/docs/_examples/toh-6/dart/lib/heroes_component.css index 15efef53e4..d2c958a911 100644 --- a/public/docs/_examples/toh-6/dart/lib/heroes_component.css +++ b/public/docs/_examples/toh-6/dart/lib/heroes_component.css @@ -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; } diff --git a/public/docs/_examples/toh-6/dart/lib/heroes_component.dart b/public/docs/_examples/toh-6/dart/lib/heroes_component.dart index 4cfc0c427e..19b8f4e0ba 100644 --- a/public/docs/_examples/toh-6/dart/lib/heroes_component.dart +++ b/public/docs/_examples/toh-6/dart/lib/heroes_component.dart @@ -1,4 +1,3 @@ -// #docplaster // #docregion import 'dart:async'; @@ -15,44 +14,34 @@ import 'hero_service.dart'; styleUrls: const ['heroes_component.css'], directives: const [HeroDetailComponent]) class HeroesComponent implements OnInit { - final Router _router; - final HeroService _heroService; List heroes; Hero selectedHero; - // #docregion error - String errorMessage; - // #enddocregion error + + final HeroService _heroService; + final Router _router; HeroesComponent(this._heroService, this._router); - // #docregion addHero - Future addHero(String name) async { - name = name.trim(); - if (name.isEmpty) return; - try { - heroes.add(await _heroService.save(name)); - } catch (e) { - errorMessage = e.toString(); - } + Future getHeroes() async { + heroes = await _heroService.getHeroes(); } - // #enddocregion addHero - // #docregion deleteHero - Future 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(); - } + // #docregion add + Future add(String name) async { + name = name.trim(); + if (name.isEmpty) return; + heroes.add(await _heroService.create(name)); + selectedHero = null; } - // #enddocregion deleteHero + // #enddocregion add - Future getHeroes() async { - heroes = await _heroService.getHeroes(); + // #docregion delete + Future delete(Hero hero) async { + await _heroService.delete(hero.id); + heroes.remove(hero); + if (selectedHero == hero) selectedHero = null; } + // #enddocregion delete void ngOnInit() { getHeroes(); diff --git a/public/docs/_examples/toh-6/dart/lib/heroes_component.html b/public/docs/_examples/toh-6/dart/lib/heroes_component.html index 98f3db8442..3ffb597fbc 100644 --- a/public/docs/_examples/toh-6/dart/lib/heroes_component.html +++ b/public/docs/_examples/toh-6/dart/lib/heroes_component.html @@ -1,31 +1,30 @@

My Heroes

- -
{{errorMessage}}
+
- Name: -
- +
    -
  • - {{hero.id}} {{hero.name}} + +
  • + {{hero.id}} + {{hero.name}} - +
  • +
-

- {{selectedHero.name | uppercase}} is my hero -

diff --git a/public/docs/_examples/toh-6/dart/lib/in_memory_data_service.dart b/public/docs/_examples/toh-6/dart/lib/in_memory_data_service.dart index 927b93bc70..5be0f9e8c4 100644 --- a/public/docs/_examples/toh-6/dart/lib/in_memory_data_service.dart +++ b/public/docs/_examples/toh-6/dart/lib/in_memory_data_service.dart @@ -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 _heroesDb = _initialHeroes.map((json) => new Hero.fromJson(json)).toList(); - // #enddocregion init static int _nextId = _heroesDb.map((hero) => hero.id).reduce(max) + 1; static Future _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 } diff --git a/public/docs/_examples/toh-6/dart/web/main.dart b/public/docs/_examples/toh-6/dart/web/main.dart index 7e41d5d227..614e95be10 100644 --- a/public/docs/_examples/toh-6/dart/web/main.dart +++ b/public/docs/_examples/toh-6/dart/web/main.dart @@ -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, diff --git a/public/docs/_examples/toh-6/e2e-spec.ts b/public/docs/_examples/toh-6/e2e-spec.ts index e2ab2602dc..3b4ec4c805 100644 --- a/public/docs/_examples/toh-6/e2e-spec.ts +++ b/public/docs/_examples/toh-6/e2e-spec.ts @@ -1,246 +1,283 @@ /// '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; + +type WPromise = webdriver.promise.Promise; + +class Hero { + id: number; + name: string; - function getPageStruct() { - let hrefEles = element.all(by.css('my-app a')); + // Factory methods + // Hero from string formatted as ' '. + static fromString(s: string): Hero { 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')), + id: +s.substr(0, s.indexOf(' ')), + name: s.substr(s.indexOf(' ') + 1), + }; + } - myHeroesHref: hrefEles.get(1), - myHeroesParent: element(by.css('my-app my-heroes')), - allHeroes: element.all(by.css('my-app my-heroes li .hero-element')), + // Hero from hero list
  • element. + static async fromLi(li: protractor.ElementFinder): Promise { + let strings = await li.all(by.xpath('span')).getText(); + return { id: +strings[0], name: strings[1] }; + } - firstDeleteButton: element.all(by.buttonText('Delete')).get(0), + // Hero id and name from the given detail element. + static async fromDetail(detail: protractor.ElementFinder): Promise { + // Get hero id from the first
    + 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(' ')) + }; + } +} - addButton: element.all(by.buttonText('Add New Hero')).get(0), +describe('Tutorial part 6', () => { - heroDetail: element(by.css('my-app my-hero-detail')), + beforeAll(() => browser.get('')); + + function getPageElts() { + let hrefElts = element.all(by.css('my-app a')); + + return { + hrefs: hrefElts, + + myDashboardHref: hrefElts.get(0), + myDashboard: element(by.css('my-app my-dashboard')), + topHeroes: element.all(by.css('my-app my-dashboard > div h4')), + + 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')), + + 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 () { - 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'); + 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(); + }); + }); - it('should be able to add a hero from the "Heroes" view', function(){ - let page = getPageStruct(); - let heroCount: webdriver.promise.Promise; + describe('Dashboard tests', () => { - 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(); + beforeAll(() => browser.get('')); + + it('has top heroes', () => { + let page = getPageElts(); + expect(page.topHeroes.count()).toEqual(4); + }); - heroCount = page.allHeroes.count(); - expect(heroCount).toBe(11, 'should show 11'); + it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); - let newHero = element(by.xpath('//span[@class="hero-element" and contains(text(),"The New Hero")]')); - expect(newHero).toBeDefined(); + 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('should be able to delete hero from "Heroes" view', function(){ - let page = getPageStruct(); - let heroCount: webdriver.promise.Promise; - - 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(`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); }); + }); - 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'); + 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('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'); + it(`selects and shows ${targetHero.name} as selected in list`, () => { + getHeroLiEltById(targetHero.id).click(); + expect(Hero.fromLi(getPageElts().selectedHero)).toEqual(targetHero); }); - }); - 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(); + 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('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'); - }); + it('can route to hero details', () => { + element(by.buttonText('View Details')).click(); - it('should be able to see dashboard choices', function () { - let page = getPageStruct(); - expect(page.topHeroes.count()).toBe(4, 'should be 4 dashboard hero choices'); - }); + 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 toggle the views', function () { - let page = getPageStruct(); + it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); - 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(`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(); - 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'); + 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('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'); + 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}); }); }); - 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(); + 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); + 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); + }); + }); + + 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 + + let page = getPageElts(); + expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); + let hero = Hero.fromDetail(page.heroDetail); + expect(hero).toEqual(targetHero); + } + + async function updateHeroNameInDetailView() { + // Assumes that the current view is the hero details view. + addToHeroName(nameSuffix); + + let hero = await Hero.fromDetail(getPageElts().heroDetail); + expect(hero).toEqual({id: targetHero.id, name: newHeroName}); } }); + +function addToHeroName(text: string): WPromise { + 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 { + let promisedHeroes: Array> = await allHeroes.map(Hero.fromLi); + // The cast is necessary to get around issuing with the signature of Promise.all() + return > Promise.all(promisedHeroes); +} diff --git a/public/docs/_examples/toh-6/ts/app/app.component.ts b/public/docs/_examples/toh-6/ts/app/app.component.ts index 16d6396184..e55e09f661 100644 --- a/public/docs/_examples/toh-6/ts/app/app.component.ts +++ b/public/docs/_examples/toh-6/ts/app/app.component.ts @@ -2,10 +2,6 @@ // #docregion import { Component } from '@angular/core'; -// #docregion rxjs-extensions -import './rxjs-extensions'; -// #enddocregion rxjs-extensions - @Component({ selector: 'my-app', diff --git a/public/docs/_examples/toh-6/ts/app/app.module.ts b/public/docs/_examples/toh-6/ts/app/app.module.ts index 1712c79371..7c497b8f14 100644 --- a/public/docs/_examples/toh-6/ts/app/app.module.ts +++ b/public/docs/_examples/toh-6/ts/app/app.module.ts @@ -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'; diff --git a/public/docs/_examples/toh-6/ts/app/hero-detail.component.html b/public/docs/_examples/toh-6/ts/app/hero-detail.component.html index 38af5f707e..32fe6d4391 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-detail.component.html +++ b/public/docs/_examples/toh-6/ts/app/hero-detail.component.html @@ -1,4 +1,3 @@ -

    {{hero.name}} details!

    diff --git a/public/docs/_examples/toh-6/ts/app/hero-detail.component.ts b/public/docs/_examples/toh-6/ts/app/hero-detail.component.ts index 4329f63a2c..87333a9fe3 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-detail.component.ts +++ b/public/docs/_examples/toh-6/ts/app/hero-detail.component.ts @@ -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 } diff --git a/public/docs/_examples/toh-6/ts/app/hero.service.ts b/public/docs/_examples/toh-6/ts/app/hero.service.ts index 21d036ad9e..e220982f6f 100644 --- a/public/docs/_examples/toh-6/ts/app/hero.service.ts +++ b/public/docs/_examples/toh-6/ts/app/hero.service.ts @@ -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 { - if (hero.id) { - return this.put(hero); - } - return this.post(hero); - } - // #enddocregion save - // #docregion delete - delete(hero: Hero): Promise { - 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 { + 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 { - let headers = new Headers({ - 'Content-Type': 'application/json'}); - + // #docregion create + create(name: string): Promise { 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 { - let headers = new Headers(); - headers.append('Content-Type', 'application/json'); - - let url = `${this.heroesUrl}/${hero.id}`; + // #enddocregion create + // #docregion update + update(hero: Hero): Promise { + 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 { - console.error('An error occurred', error); + console.error('An error occurred', error); // for demo purposes only return Promise.reject(error.message || error); } // #enddocregion handleError diff --git a/public/docs/_examples/toh-6/ts/app/heroes.component.css b/public/docs/_examples/toh-6/ts/app/heroes.component.css index 75969e0b3a..d2c958a911 100644 --- a/public/docs/_examples/toh-6/ts/app/heroes.component.css +++ b/public/docs/_examples/toh-6/ts/app/heroes.component.css @@ -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; } diff --git a/public/docs/_examples/toh-6/ts/app/heroes.component.html b/public/docs/_examples/toh-6/ts/app/heroes.component.html index 05afc9ea2f..392d241d52 100644 --- a/public/docs/_examples/toh-6/ts/app/heroes.component.html +++ b/public/docs/_examples/toh-6/ts/app/heroes.component.html @@ -1,24 +1,26 @@

    My Heroes

    + +
    + + +
    +
      -
    • - - {{hero.id}} {{hero.name}} - - - - + +
    • + {{hero.id}} + {{hero.name}} + + +
    • +
    - - -
    {{error}}
    - -
    - -
    - -

    {{selectedHero.name | uppercase}} is my hero diff --git a/public/docs/_examples/toh-6/ts/app/heroes.component.ts b/public/docs/_examples/toh-6/ts/app/heroes.component.ts index b3d17549e8..6c0e8f2306 100644 --- a/public/docs/_examples/toh-6/ts/app/heroes.component.ts +++ b/public/docs/_examples/toh-6/ts/app/heroes.component.ts @@ -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 { diff --git a/public/docs/dart/latest/tutorial/_data.json b/public/docs/dart/latest/tutorial/_data.json index b292070557..c8e944bc80 100644 --- a/public/docs/dart/latest/tutorial/_data.json +++ b/public/docs/dart/latest/tutorial/_data.json @@ -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 } } diff --git a/public/docs/dart/latest/tutorial/toh-pt6.jade b/public/docs/dart/latest/tutorial/toh-pt6.jade index 0ca2c931ed..fadf4dc5cc 100644 --- a/public/docs/dart/latest/tutorial/toh-pt6.jade +++ b/public/docs/dart/latest/tutorial/toh-pt6.jade @@ -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, diff --git a/public/docs/js/latest/tutorial/_data.json b/public/docs/js/latest/tutorial/_data.json index 0e5cc1661d..54b30c6db2 100644 --- a/public/docs/js/latest/tutorial/_data.json +++ b/public/docs/js/latest/tutorial/_data.json @@ -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 } } diff --git a/public/docs/ts/_cache/tutorial/toh-pt6.jade b/public/docs/ts/_cache/tutorial/toh-pt6.jade index cb7f750aec..4a484b20e5 100644 --- a/public/docs/ts/_cache/tutorial/toh-pt6.jade +++ b/public/docs/ts/_cache/tutorial/toh-pt6.jade @@ -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_. + + +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') ++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. + +

    HTTP !{_Promise}

    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. + But first, let's see what happens now when we try to update a hero's details. +.l-main-section :marked - ### Post + ## Update hero details - We will be using `post` to add new heroes. Post requests require a little bit more setup than Get requests: + 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! -+makeExcerpt('app/hero.service.ts', 'post') +.l-sub-section + :marked + 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 - 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. + ### Save hero details - ### Put + 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`: - 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') ++makeExcerpt('app/hero-detail.component.html', 'save') :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') + The `save` method persists hero name changes using the hero service + `update` method and then navigates back to the previous view: -:marked - We add a `catch` to handle errors for all three methods. ++makeExcerpt('app/hero-detail.component.ts', 'save') :marked - ### Save + ### Hero service `update` method - 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. + 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: -+makeExcerpt('app/hero.service.ts', 'save') ++makeExcerpt('app/hero.service.ts', 'update') :marked - After these additions our `HeroService` looks like this: + 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. -+makeExample('app/hero.service.ts') + Refresh the browser and give it a try. Changes to hero names should now persist. .l-main-section :marked - ## Updating Components - - 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. + ## Add a hero -block hero-detail-comp-extra-imports-and-vars - :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* + 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. - 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. + Insert the following into the heroes component HTML, first thing after + the heading: - 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/heroes.component.html', 'add') - +makeExcerpt('app/hero-detail.component.ts', 'ngOnInit') +: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. - :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. ++makeExcerpt('app/heroes.component.ts', 'add') :marked - Add a save method to `HeroDetailComponent` and call the corresponding save method in `HeroesService`. + 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}. -+makeExcerpt('app/hero-detail.component.ts', 'save') + Go ahead, refresh the browser and create some new heroes! -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. +.l-main-section +:marked + ## Delete a hero - After we save a hero, we redirect the browser back to the previous page using the `goBack()` method. + Too many heroes? + Let's add a delete button to each hero in the heroes view. - +makeExcerpt('app/hero-detail.component.ts', 'goBack') + Add this button element to the heroes component HTML, right after the hero + name in the repeated `
  • ` tag: - :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 Component Interaction Cookbook ++makeExcerpt('app/heroes.component.html', 'delete', '') :marked - Here is `HeroDetailComponent` with its new save button and the corresponding HTML. + The `
  • ` element should now look like this: -figure.image-display - img(src='/resources/images/devguide/toh/hero-details-save-button.png' alt="Hero Details With Save Button") - -+makeExcerpt('app/hero-detail.component.html', 'save') ++makeExcerpt('app/heroes.component.html', 'li-element') :marked - ### Add/Delete in the *HeroesComponent* + 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 `
  • ` click handler to be triggered because that would + select the hero that we are going to delete! - We'll be reporting propagated HTTP errors, let's start by adding the following - field to the `HeroesComponent` class: + The logic of the `delete` handler is a bit trickier: -+makeExcerpt('app/heroes.component.ts', 'error', '') ++makeExcerpt('app/heroes.component.ts', 'delete') :marked - The user can *add* a new hero by clicking a button and entering a name. - -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. + 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. -- var _below = _docsFor == 'dart' ? 'before' : 'below'; :marked - Add the following to the heroes component HTML, just !{_below} the hero list (`
      ...
    `). -+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. + We want our delete button to be placed at the far right of the hero entry. + This extra CSS accomplishes that: - 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 `
  • ` tag: -+makeExcerpt('app/heroes.component.html', 'delete') -:marked - Add the following to the bottom of the `HeroesComponent` CSS file: +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') +:marked + ### Hero service `delete` method -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. + The hero service's `delete` method uses the _delete_ HTTP method to remove the hero from the server: -:marked - The *delete* logic is a bit trickier. -+makeExcerpt('app/heroes.component.ts', 'deleteHero') ++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 + hero-search.component.ts + and add it to the `!{_declarations}` !{_array}: +makeExcerpt(declFile, 'search') diff --git a/public/docs/ts/latest/tutorial/_data.json b/public/docs/ts/latest/tutorial/_data.json index 22b7c4be4e..54b30c6db2 100644 --- a/public/docs/ts/latest/tutorial/_data.json +++ b/public/docs/ts/latest/tutorial/_data.json @@ -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 } -} \ No newline at end of file +} diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index bf98e28054..8f17d79380 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -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 (updated getHeroes and new class members)', 'getHeroes') + +:marked + Our updated import statements are now: -+makeExcerpt('app/hero.service.ts (new constructor and revised getHeroes)', 'getHeroes') ++makeExcerpt('app/hero.service.ts (updated imports)', 'imports') +- var _h3id = `http-${_promise}` :marked - ### HTTP !{_Promise} + Refresh the browser, and the hero data should be successfully loaded from the + mock server. + +

    HTTP !{_Promise}

    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 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. + Our stakeholders are thrilled with the added flexibility from the API integration. + Now they want the ability to create and delete heroes. - We'll complete `HeroService` by creating `post`, `put` and `delete` methods to meet our new requirements. + Let's see first what happens when we try to update a hero's details. +.l-main-section :marked - ### Post + ## Update hero details - We will be using `post` to add new heroes. Post requests require a little bit more setup than Get requests: + 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! -+makeExcerpt('app/hero.service.ts', 'post') +.l-sub-section + :marked + 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 - 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 + ### Save hero details - 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. + 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.service.ts', 'put') ++makeExcerpt('app/hero-detail.component.html', 'save') :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') + The `save` method persists hero name changes using the hero service + `update` method and then navigates back to the previous view: -:marked - We add a `catch` to handle errors for all three methods. ++makeExcerpt('app/hero-detail.component.ts', 'save') :marked - ### Save + ### Hero service `update` method - 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. + 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: -+makeExcerpt('app/hero.service.ts', 'save') ++makeExcerpt('app/hero.service.ts', 'update') :marked - After these additions our `HeroService` looks like this: + 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. -+makeExample('app/hero.service.ts') + Refresh the browser and give it a try. Changes to hero names should now persist. .l-main-section :marked - ## Updating Components + ## Add a hero - 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. + 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. -block hero-detail-comp-extra-imports-and-vars - :marked - Before we can add those methods, we need to initialize some variables with their respective imports. + Insert the following into the heroes component HTML, first thing after + the heading: - +makeExcerpt('app/hero-detail.component.ts ()', 'variables-imports') ++makeExcerpt('app/heroes.component.html', 'add') -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. +: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/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. ++makeExcerpt('app/heroes.component.ts', 'add') :marked - Add a save method to `HeroDetailComponent` and call the corresponding save method in `HeroesService`. - -+makeExcerpt('app/hero-detail.component.ts', 'save') + 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}. -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. + Go ahead, refresh the browser and create some new heroes! - After we save a hero, we redirect the browser back to the previous page using the `goBack()` method. +.l-main-section +:marked + ## Delete a hero - +makeExcerpt('app/hero-detail.component.ts', 'goBack') + Too many heroes? + Let's add a delete button to each hero in the heroes view. - :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. + Add this button element to the heroes component HTML, right after the hero + name in the repeated `
  • ` tag: - .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 Component Interaction Cookbook ++makeExcerpt('app/heroes.component.html', 'delete', '') :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") + The `
  • ` element should now look like this: -+makeExcerpt('app/hero-detail.component.html', 'save') ++makeExcerpt('app/heroes.component.html', 'li-element') :marked - ### Add/Delete in the *HeroesComponent* + 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 `
  • ` click handler to be triggered because that would + select the hero that we are going to delete! - We'll be reporting propagated HTTP errors, let's start by adding the following - field to the `HeroesComponent` class: + The logic of the `delete` handler is a bit trickier: -+makeExcerpt('app/heroes.component.ts', 'error', '') ++makeExcerpt('app/heroes.component.ts', 'delete') :marked - The user can *add* a new hero by clicking a button and entering a name. - -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. + 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. -- var _below = _docsFor == 'dart' ? 'before' : 'below'; :marked - Add the following to the heroes component HTML, just !{_below} the hero list (`
      ...
    `). -+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. + We want our delete button to be placed at the far right of the hero entry. + This extra CSS accomplishes that: - 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 `
  • ` tag: -+makeExcerpt('app/heroes.component.html', 'delete') -:marked - Add the following to the bottom of the `HeroesComponent` CSS file: +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') +:marked + ### Hero service `delete` method -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. + The hero service's `delete` method uses the _delete_ HTTP method to remove the hero from the server: -:marked - The *delete* logic is a bit trickier. -+makeExcerpt('app/heroes.component.ts', 'deleteHero') ++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 + hero-search.component.ts and add it to the `!{_declarations}` !{_array}: +makeExcerpt(declFile, 'search')