From 8a6c5b5725e4a39d93d29391f11023cb1c05218b Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Tue, 2 Aug 2016 09:59:35 -0700 Subject: [PATCH] docs(toh-6): add hero search to Dart; minor edits to TS (#2018) * docs(toh-6/dart): add hero search Fixes #1924. * docs(toh-6/ts): minor updates * post-review updates * post-review updates --- .../toh-6/dart/lib/dashboard_component.dart | 21 +- .../toh-6/dart/lib/dashboard_component.html | 5 +- .../toh-6/dart/lib/hero_search_component.css | 15 ++ .../toh-6/dart/lib/hero_search_component.dart | 57 ++++++ .../toh-6/dart/lib/hero_search_component.html | 11 ++ .../toh-6/dart/lib/hero_search_service.dart | 33 ++++ .../toh-6/dart/lib/heroes_component.css | 8 + .../dart/lib/in_memory_data_service.dart | 10 +- public/docs/_examples/toh-6/dart/pubspec.yaml | 1 + .../docs/_examples/toh-6/dart/web/index.html | 1 - .../docs/_examples/toh-6/dart/web/sample.css | 7 - .../toh-6/ts/app/dashboard.component.ts | 7 +- .../toh-6/ts/app/hero-search.component.ts | 2 +- public/docs/dart/latest/tutorial/toh-pt6.jade | 105 +++++++--- public/docs/ts/latest/tutorial/toh-pt6.jade | 182 ++++++++++-------- 15 files changed, 326 insertions(+), 139 deletions(-) create mode 100644 public/docs/_examples/toh-6/dart/lib/hero_search_component.css create mode 100644 public/docs/_examples/toh-6/dart/lib/hero_search_component.dart create mode 100644 public/docs/_examples/toh-6/dart/lib/hero_search_component.html create mode 100644 public/docs/_examples/toh-6/dart/lib/hero_search_service.dart delete mode 100644 public/docs/_examples/toh-6/dart/web/sample.css diff --git a/public/docs/_examples/toh-6/dart/lib/dashboard_component.dart b/public/docs/_examples/toh-6/dart/lib/dashboard_component.dart index ff0fb8c0d1..da965c301c 100644 --- a/public/docs/_examples/toh-6/dart/lib/dashboard_component.dart +++ b/public/docs/_examples/toh-6/dart/lib/dashboard_component.dart @@ -1,41 +1,31 @@ -// #docplaster -// #docregion +// #docregion , search import 'dart:async'; import 'package:angular2/core.dart'; -// #docregion import-router import 'package:angular2/router.dart'; -// #enddocregion import-router import 'hero.dart'; import 'hero_service.dart'; +import 'hero_search_component.dart'; @Component( selector: 'my-dashboard', - // #docregion template-url templateUrl: 'dashboard_component.html', - // #enddocregion template-url - // #docregion css - styleUrls: const ['dashboard_component.css'] - // #enddocregion css - ) -// #docregion component + styleUrls: const ['dashboard_component.css'], + directives: const [HeroSearchComponent]) +// #enddocregion search class DashboardComponent implements OnInit { List heroes; - // #docregion ctor final Router _router; final HeroService _heroService; DashboardComponent(this._heroService, this._router); - // #enddocregion ctor - Future ngOnInit() async { heroes = (await _heroService.getHeroes()).skip(1).take(4).toList(); } - // #docregion goto-detail void gotoDetail(Hero hero) { var link = [ 'HeroDetail', @@ -43,5 +33,4 @@ class DashboardComponent implements OnInit { ]; _router.navigate(link); } -// #enddocregion goto-detail } diff --git a/public/docs/_examples/toh-6/dart/lib/dashboard_component.html b/public/docs/_examples/toh-6/dart/lib/dashboard_component.html index 7133c10ada..fe1ee585f6 100644 --- a/public/docs/_examples/toh-6/dart/lib/dashboard_component.html +++ b/public/docs/_examples/toh-6/dart/lib/dashboard_component.html @@ -1,11 +1,10 @@

Top Heroes

- -
- +

{{hero.name}}

+ diff --git a/public/docs/_examples/toh-6/dart/lib/hero_search_component.css b/public/docs/_examples/toh-6/dart/lib/hero_search_component.css new file mode 100644 index 0000000000..b41b4ec33e --- /dev/null +++ b/public/docs/_examples/toh-6/dart/lib/hero_search_component.css @@ -0,0 +1,15 @@ +/* #docregion */ +.search-result { + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + width:195px; + height: 20px; + padding: 5px; + background-color: white; + cursor: pointer; +} +#search-box { + width: 200px; + height: 20px; +} diff --git a/public/docs/_examples/toh-6/dart/lib/hero_search_component.dart b/public/docs/_examples/toh-6/dart/lib/hero_search_component.dart new file mode 100644 index 0000000000..fc52ace1ba --- /dev/null +++ b/public/docs/_examples/toh-6/dart/lib/hero_search_component.dart @@ -0,0 +1,57 @@ +// #docplaster +// #docregion +import 'dart:async'; + +import 'package:angular2/core.dart'; +import 'package:angular2/router.dart'; +import 'package:stream_transformers/stream_transformers.dart'; + +import 'hero_search_service.dart'; +import 'hero.dart'; + +@Component( + selector: 'hero-search', + templateUrl: 'hero_search_component.html', + styleUrls: const ['hero_search_component.css'], + providers: const [HeroSearchService]) +class HeroSearchComponent implements OnInit { + HeroSearchService _heroSearchService; + Router _router; + + // #docregion search + Stream> heroes; + // #enddocregion search + // #docregion searchTerms + StreamController _searchTerms = + new StreamController.broadcast(); + // #enddocregion searchTerms + + HeroSearchComponent(this._heroSearchService, this._router) {} + // #docregion searchTerms + + // Push a search term into the stream. + void search(String term) => _searchTerms.add(term); + // #enddocregion searchTerms + // #docregion search + + Future ngOnInit() async { + heroes = _searchTerms.stream + .transform(new Debounce(new Duration(milliseconds: 300))) + .distinct() + .transform(new FlatMapLatest((term) => term.isEmpty + ? new Stream>.fromIterable([[]]) + : _heroSearchService.search(term).asStream())) + .handleError((e) { + print(e); // for demo purposes only + }); + } + // #enddocregion search + + void gotoDetail(Hero hero) { + var link = [ + 'HeroDetail', + {'id': hero.id.toString()} + ]; + _router.navigate(link); + } +} diff --git a/public/docs/_examples/toh-6/dart/lib/hero_search_component.html b/public/docs/_examples/toh-6/dart/lib/hero_search_component.html new file mode 100644 index 0000000000..08c0560c5b --- /dev/null +++ b/public/docs/_examples/toh-6/dart/lib/hero_search_component.html @@ -0,0 +1,11 @@ + +
+

Hero Search

+ +
+
+ {{hero.name}} +
+
+
diff --git a/public/docs/_examples/toh-6/dart/lib/hero_search_service.dart b/public/docs/_examples/toh-6/dart/lib/hero_search_service.dart new file mode 100644 index 0000000000..3fecf42e1b --- /dev/null +++ b/public/docs/_examples/toh-6/dart/lib/hero_search_service.dart @@ -0,0 +1,33 @@ +// #docregion +import 'dart:async'; +import 'dart:convert'; + +import 'package:angular2/core.dart'; +import 'package:http/http.dart'; + +import 'hero.dart'; + +@Injectable() +class HeroSearchService { + final Client _http; + + HeroSearchService(this._http); + + Future> search(String term) async { + try { + final response = await _http.get('app/heroes/?name=$term'); + return _extractData(response) + .map((json) => new Hero.fromJson(json)) + .toList(); + } catch (e) { + throw _handleError(e); + } + } + + dynamic _extractData(Response resp) => JSON.decode(resp.body)['data']; + + Exception _handleError(dynamic e) { + print(e); // for demo purposes only + return new Exception('Server error; cause: $e'); + } +} 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 35e45af98d..15efef53e4 100644 --- a/public/docs/_examples/toh-6/dart/lib/heroes_component.css +++ b/public/docs/_examples/toh-6/dart/lib/heroes_component.css @@ -1,3 +1,4 @@ +/* #docregion */ .selected { background-color: #CFD8DC !important; color: white; @@ -57,3 +58,10 @@ button { button:hover { background-color: #cfd8dc; } +/* #docregion additions */ +.error {color:red;} +button.delete-button { + float:right; + background-color: gray !important; + color:white; +} 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 86aad3de53..927b93bc70 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,6 +1,7 @@ // #docregion import 'dart:async'; import 'dart:convert'; +import 'dart:math'; // #docregion init import 'package:angular2/core.dart'; @@ -23,17 +24,18 @@ class InMemoryDataService extends MockClient { {'id': 19, 'name': 'Magma'}, {'id': 20, 'name': 'Tornado'} ]; - // #enddocregion init - static final List _heroesDb = _initialHeroes.map((json) => new Hero.fromJson(json)).toList(); - static int _nextId = 21; + // #enddocregion init + static int _nextId = _heroesDb.map((hero) => hero.id).reduce(max) + 1; static Future _handler(Request request) async { var data; switch (request.method) { case 'GET': - data = _heroesDb; + String prefix = request.url.queryParameters['name'] ?? ''; + final regExp = new RegExp(prefix, caseSensitive: false); + data = _heroesDb.where((hero) => hero.name.contains(regExp)).toList(); break; case 'POST': var name = JSON.decode(request.body)['name']; diff --git a/public/docs/_examples/toh-6/dart/pubspec.yaml b/public/docs/_examples/toh-6/dart/pubspec.yaml index b59118580c..cdb0ce29f1 100644 --- a/public/docs/_examples/toh-6/dart/pubspec.yaml +++ b/public/docs/_examples/toh-6/dart/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: dart_to_js_script_rewriter: ^1.0.1 # #docregion additions http: ^0.11.0 + stream_transformers: ^0.3.0 transformers: - angular2: # #enddocregion additions diff --git a/public/docs/_examples/toh-6/dart/web/index.html b/public/docs/_examples/toh-6/dart/web/index.html index be8fb7b42e..acb7482f70 100644 --- a/public/docs/_examples/toh-6/dart/web/index.html +++ b/public/docs/_examples/toh-6/dart/web/index.html @@ -7,7 +7,6 @@ - diff --git a/public/docs/_examples/toh-6/dart/web/sample.css b/public/docs/_examples/toh-6/dart/web/sample.css deleted file mode 100644 index 6bbf5de8b7..0000000000 --- a/public/docs/_examples/toh-6/dart/web/sample.css +++ /dev/null @@ -1,7 +0,0 @@ -/* #docregion */ -.error {color:red;} -button.delete-button { - float:right; - background-color: gray !important; - color:white; -} diff --git a/public/docs/_examples/toh-6/ts/app/dashboard.component.ts b/public/docs/_examples/toh-6/ts/app/dashboard.component.ts index 82a817fa5a..41b4de58df 100644 --- a/public/docs/_examples/toh-6/ts/app/dashboard.component.ts +++ b/public/docs/_examples/toh-6/ts/app/dashboard.component.ts @@ -1,6 +1,4 @@ -// #docplaster -// #docregion -// #docregion hero-search-component +// #docregion , search import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @@ -14,9 +12,8 @@ import { HeroSearchComponent } from './hero-search.component'; styleUrls: ['app/dashboard.component.css'], directives: [HeroSearchComponent] }) -// #enddocregion hero-search-component +// #enddocregion search export class DashboardComponent implements OnInit { - heroes: Hero[] = []; constructor( diff --git a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts index 81bb8eb773..d9a04fce3f 100644 --- a/public/docs/_examples/toh-6/ts/app/hero-search.component.ts +++ b/public/docs/_examples/toh-6/ts/app/hero-search.component.ts @@ -18,7 +18,7 @@ export class HeroSearchComponent implements OnInit { heroes: Observable; // #enddocregion search // #docregion searchTerms - searchTerms = new Subject(); + private searchTerms = new Subject(); // #enddocregion searchTerms constructor( diff --git a/public/docs/dart/latest/tutorial/toh-pt6.jade b/public/docs/dart/latest/tutorial/toh-pt6.jade index 6536c1a7d3..bee08c95de 100644 --- a/public/docs/dart/latest/tutorial/toh-pt6.jade +++ b/public/docs/dart/latest/tutorial/toh-pt6.jade @@ -25,7 +25,8 @@ block http-library ### Pubspec updates - We need to add a package dependency for the !{_Angular_http_library}. + We need to add package dependencies for the + `stream_transformers` and !{_Angular_http_library}s. We also need to add a `resolved_identifiers` entry, to inform the [angular2 transformer][ng2x] that we'll be using `BrowserClient`. (For an explanation of why @@ -37,7 +38,7 @@ block http-library [guide-http]: ../guide/server-communication.html#!#http-providers [ng2x]: https://github.com/angular/angular/wiki/Angular-2-Dart-Transformer - - var stylePattern = { pnk: /(http.*|resolved_identifiers:|Browser.*|Client.*)/gm }; + - var stylePattern = { pnk: /(http.*|stream.*|resolved_identifiers:|Browser.*|Client.*)/gm }; +makeExcerpt('pubspec.yaml', 'additions', null, stylePattern) block http-providers @@ -55,7 +56,13 @@ block backend implementations. block dont-be-distracted-by-backend-subst - //- N/A + //- No backend substitution but we do need to comment on the changes to Hero. + :marked + As is common for web API services, our mock in-memory service will be + encoding and decoding heroes in JSON format, so we enhance the `Hero` + class with these capabilities: + + +makeExample('lib/hero.dart') block get-heroes-details :marked @@ -89,8 +96,42 @@ block heroes-comp-add block review //- Not showing animated gif due to differences between TS and Dart implementations. -block observables-section - //- TBC +block observables-section-intro + :marked + Recall that `HeroService.getHeroes()` awaits for an `http.get()` + response and yields a _Future_ `List`, which is fine when we are only + interested in a single result. + +block search-criteria-intro + :marked + A [StreamController][], as its name implies, is a controller for a [Stream][] that allows us to + manipulate the underlying stream by adding data to it, for example. + + In our sample, the underlying stream of strings (`_searchTerms.stream`) represents the hero + name search patterns entered by the user. Each call to `search` puts a new string into + the stream by calling `add` over the controller. + + [Stream]: https://api.dartlang.org/stable/dart-async/Stream-class.html + [StreamController]: https://api.dartlang.org/stable/dart-async/StreamController-class.html + +block observable-transformers + :marked + Fortunately, there are stream transformers that will help us reduce the request flow. + We'll make fewer calls to the `HeroSearchService` and still get timely results. Here's how: + + * `transform(new Debounce(... 300)))` waits until the flow of search terms pauses for 300 + milliseconds before passing along the latest string. We'll never make requests more frequently + than 300ms. + + * `distinct()` ensures that we only send a request if a search term has changed. + There's no point in repeating a request for the same search term. + + * `transform(new FlatMapLatest(...))` applies a map-like transformer that (1) calls our search + service for each search term that makes it through the debounce and distinct gauntlet and (2) + returns only the most recent search service result, discarding any previous results. + + * `handleError()` handles errors. Our simple example prints the error to the console; a real + life application should do better. block filetree .filetree @@ -98,47 +139,65 @@ block filetree .children .file lib .children - .file app_component.dart .file app_component.css + .file app_component.dart .file dashboard_component.css - .file dashboard_component.html .file dashboard_component.dart + .file dashboard_component.html .file hero.dart .file hero_detail_component.css - .file hero_detail_component.html .file hero_detail_component.dart + .file hero_detail_component.html + .file hero_search_component.css (new) + .file hero_search_component.dart (new) + .file hero_search_component.html (new) + .file hero_search_service.dart (new) .file hero_service.dart .file heroes_component.css - .file heroes_component.html .file heroes_component.dart - .file main.dart + .file heroes_component.html .file in_memory_data_service.dart (new) .file web .children .file main.dart .file index.html - .file sample.css (new) .file styles.css .file pubspec.yaml block file-summary +makeTabs( - `toh-6/dart/lib/hero.dart, + `toh-6/dart/lib/dashboard_component.dart, + toh-6/dart/lib/dashboard_component.html, + toh-6/dart/lib/hero.dart, toh-6/dart/lib/hero_detail_component.dart, toh-6/dart/lib/hero_detail_component.html, toh-6/dart/lib/hero_service.dart, - toh-6/dart/lib/heroes_component.dart, - toh-6/dart/web/index.html, - toh-6/dart/web/main.dart, - toh-6/dart/web/sample.css`, - `,,,,,,final,`, - `lib/hero.dart, + toh-6/dart/lib/heroes_component.css, + toh-6/dart/lib/heroes_component.dart`, + null, + `lib/dashboard_component.dart, + lib/dashboard_component.html, + lib/hero.dart, lib/hero_detail_component.dart, lib/hero_detail_component.html, lib/hero_service.dart, - lib/heroes_component.dart, - web/index.html, - web/main.dart, - web/sample.css`) + lib/heroes_component.css, + lib/heroes_component.dart`) - +makeExample('pubspec.yaml') + +makeTabs( + `toh-6/dart/lib/hero_search_component.css, + toh-6/dart/lib/hero_search_component.dart, + toh-6/dart/lib/hero_search_component.html, + toh-6/dart/lib/hero_search_service.dart`, + null, + `lib/hero_search_component.css, + lib/hero_search_component.dart, + lib/hero_search_component.html, + lib/hero_search_service.dart`) + + +makeTabs( + `toh-6/dart/pubspec.yaml, + toh-6/dart/web/main.dart`, + `,final`, + `pubspec.yaml, + web/main.dart`) diff --git a/public/docs/ts/latest/tutorial/toh-pt6.jade b/public/docs/ts/latest/tutorial/toh-pt6.jade index c7edf449fc..4730b7e61f 100644 --- a/public/docs/ts/latest/tutorial/toh-pt6.jade +++ b/public/docs/ts/latest/tutorial/toh-pt6.jade @@ -111,7 +111,7 @@ block dont-be-distracted-by-backend-subst Look at our current `HeroService` implementation -+makeExample('toh-4/ts/app/hero.service.ts', 'get-heroes', 'app/hero.service.ts (old getHeroes)')(format=".") ++makeExcerpt('toh-4/ts/app/hero.service.ts (old getHeroes)', 'get-heroes') :marked We returned a !{_Promise} resolved with mock heroes. @@ -135,7 +135,7 @@ 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. - +makeExample('toh-6/ts/app/hero.service.ts', 'to-promise')(format=".") + +makeExcerpt('app/hero.service.ts', 'to-promise', '') :marked Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ... not out of the box. The Angular `Observable` is a bare-bones implementation. @@ -143,13 +143,13 @@ block get-heroes-details 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: - +makeExample('toh-6/ts/app/hero.service.ts', 'rxjs')(format=".") + +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. - +makeExample('toh-6/ts/app/hero.service.ts', 'to-data')(format=".") + +makeExcerpt('app/hero.service.ts', 'to-data', '') :marked That response JSON has a single `data` property. @@ -172,12 +172,15 @@ block get-heroes-details ### Error Handling At the end of `getHeroes()` we `catch` server failures and pass them to an error handler: -+makeExcerpt('app/hero.service.ts', 'catch') + ++makeExcerpt('app/hero.service.ts', 'catch', '') + :marked This is a critical step! We must anticipate HTTP failures as they happen frequently for reasons beyond our control. -+makeExcerpt('app/hero.service.ts', 'handleError') ++makeExcerpt('app/hero.service.ts', 'handleError', '') + - 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. @@ -209,7 +212,7 @@ block get-heroes-details ### 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. + 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') @@ -259,7 +262,7 @@ block hero-detail-comp-updates +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. + 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. :marked Add a save method to `HeroDetailComponent` and call the corresponding save method in `HeroesService`. @@ -315,7 +318,7 @@ block add-new-hero-via-detail-comp 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: -+makeExample('app/heroes.component.html', 'delete') ++makeExcerpt('app/heroes.component.html', 'delete') :marked Now let's fix-up the `HeroesComponent` to support the *add* and *delete* actions used in the template. @@ -357,12 +360,13 @@ block review ### 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 Editting w/ HTTP") + img(src='/resources/images/devguide/toh/toh-http.anim.gif' alt="Heroes List Editing w/ HTTP") -block observables-section +:marked + ## !{_Observable}s + +block observables-section-intro :marked - ## Observables - Each `Http` method returns an `Observable` of HTTP `Response` objects. Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller. @@ -384,59 +388,78 @@ block observables-section 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. - But requests aren't always "one and done". We may start one request, - then cancel it, and make a different request ... before the server has responded to the first request. - Such a _request-cancel-new-request_ sequence is difficult to implement with *promises*. - It's easy with *observables* as we'll see. +:marked + But requests aren't always "one and done". We may start one request, + then cancel it, and make a different request before the server has responded to the first request. + Such a _request-cancel-new-request_ sequence is difficult to implement with *!{_Promise}s*. + It's easy with *!{_Observable}s* as we'll see. - ### Search-by-name - We're going to add a *hero search* feature to the Tour of Heroes. - As the user types a name into a search box, we'll make repeated http requests for heroes filtered by that name. + ### Search-by-name + We're going to add a *hero search* feature to the Tour of Heroes. + As the user types a name into a search box, we'll make repeated HTTP requests for heroes filtered by that name. - We start by creating `HeroSearchService` that sends search queries to our server's web api. + We start by creating `HeroSearchService` that sends search queries to our server's web api. - +makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".") ++makeExample('app/hero-search.service.ts') +:marked + The `!{_priv}http.get()` call in `HeroSearchService` is similar to the one + in the `HeroService`, although the URL now has a query string. + Another notable difference: we no longer call `toPromise`, + we simply return the *observable* instead. + + ### HeroSearchComponent + + Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`. + + The component template is simple — just a text box and a list of matching search results. + ++makeExample('app/hero-search.component.html') + +:marked + As the user types in the search box, a *keyup* event binding calls the component's `search` method with the new search box value. + + The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there. + + But, as we'll soon see, the `heroes` property is now !{_an} *!{_Observable}* of hero !{_array}s, rather than just a hero !{_array}. + The `*ngFor` can't do anything with !{_an} `!{_Observable}` until we flow it through the `async` pipe (`AsyncPipe`). + The `async` pipe subscribes to the `!{_Observable}` and produces the !{_array} of heroes to `*ngFor`. + + Time to create the `HeroSearchComponent` class and metadata. + ++makeExample('app/hero-search.component.ts') + +:marked + #### Search terms + + Let's focus on the `!{_priv}searchTerms`: + ++makeExcerpt('app/hero-search.component.ts', 'searchTerms', '') + +block search-criteria-intro :marked - The `http.get` call in `HeroSearchService` is similar to the `http.get` call in the `HeroService`. - The notable difference: we no longer call `toPromise`. - We simply return the *observable* instead. - - ### HeroSearchComponent - Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`. - - The component template is simple - just a textbox and a list of matching search results. - +makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html') - :marked - As the user types in the search box, a *keyup* event binding calls the component's `search` with the new search box value. - - The `*ngFor` repeats *hero* objects from the component's `heroes` property. No surprise there. - - But, as we'll soon see, the `heroes` property returns an `Observable` of heroes, not an array of heroes. - The `*ngFor` can't do anything with an observable until we flow it through the `AsyncPipe` (`heroes | async`). - The `AsyncPipe` subscribes to the observable and produces the array of heroes to `*ngFor`. - - Time to create the `HeroSearchComponent` class and metadata. - +makeExample('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts') - :marked - Focus on the `searchTerms`. - +makeExample('toh-6/ts/app/hero-search.component.ts', 'searchTerms')(format=".") - :marked - A `Subject` is a producer of an _observable_ event stream. - This `searchTerms` produces an `Observable` of strings, the filter criteria for the name search. + A `Subject` is a producer of an _observable_ event stream; + `searchTerms` produces an `Observable` of strings, the filter criteria for the name search. Each call to `search` puts a new string into this subject's _observable_ stream by calling `next`. - A `Subject` is also an `Observable`. - We're going to access that `Observable` and turn the stream - of strings into a stream of `Hero[]` arrays, the `heroes` property. +:marked + + #### Initialize the _**heroes**_ property (_**ngOnInit**_) - +makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".") + A `Subject` is also an `Observable`. + We're going to turn the stream + of search terms into a stream of `Hero` !{_array}s and assign the result to the `heroes` property. + ++makeExcerpt('app/hero-search.component.ts', 'search', '') + +:marked + If we passed every user keystroke directly to the `HeroSearchService`, we'd unleash a storm of HTTP requests. + Bad idea. We don't want to tax our server resources and burn through our cellular network data plan. + +block observable-transformers :marked - If we passed every user keystroke directly to the `HeroSearchService`, we'd unleash a storm of http requests. - Bad idea. We don't want to tax our server resources and burn through our cellular network data plan. - - Fortunately we can chain `Observable` operators to the string `Observable` that reduce the request flow. + Fortunately, we can chain `Observable` operators to the string `Observable` that reduce the request flow. We'll make fewer calls to the `HeroSearchService` and still get timely results. Here's how: * `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds @@ -486,30 +509,31 @@ block observables-section 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('toh-6/ts/app/rxjs-extensions.ts', null, 'app/rxjs-extensions.ts')(format=".") + +makeExample('app/rxjs-extensions.ts') + :marked We load them all at once by importing `rxjs-extensions` in `AppComponent`. - +makeExample('toh-6/ts/app/app.component.ts', 'rxjs-extensions', 'app/app.component.ts')(format=".") + +makeExcerpt('app/app.component.ts', 'rxjs-extensions') - :marked - ### Add the search component to the dashboard +:marked + ### Add the search component to the dashboard - We add the hero search HTML element to the bottom of the `DashboardComponent` template. + We add the hero search HTML element to the bottom of the `DashboardComponent` template. - +makeExample('app/dashboard.component.html') ++makeExample('app/dashboard.component.html') - :marked - And finally, we import the `HeroSearchComponent` and add it to the `directives` array. +:marked + And finally, we import the `HeroSearchComponent` and add it to the `directives` !{_array}. - +makeExcerpt('app/dashboard.component.ts', 'hero-search-component') ++makeExcerpt('app/dashboard.component.ts', 'search') - :marked - Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles. - At some point it might look like this. +:marked + Run the app again, go to the *Dashboard*, and enter some text in the search box. + At some point it might look like this. - figure.image-display - img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") +figure.image-display + img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") .l-main-section :marked @@ -534,9 +558,9 @@ block filetree .file hero-detail.component.css .file hero-detail.component.html .file hero-detail.component.ts - .file hero-search.component.html - .file hero-search.component.ts - .file hero-search.service.ts + .file hero-search.component.html (new) + .file hero-search.component.ts (new) + .file hero-search.service.ts (new) .file rxjs-operators.ts .file hero.service.ts .file heroes.component.css @@ -559,14 +583,14 @@ block filetree ## Home Stretch We are at the end of our journey for now, but we have accomplished a lot. - - We added the necessary dependencies to use Http in our application. - - We refactored HeroService to load heroes from an API. - - We extended HeroService to support post, put and delete calls. + - We added the necessary dependencies to use HTTP in our application. + - We refactored `HeroService` to load heroes from a web API. + - We extended `HeroService` to support post, put and delete methods. - We updated our components to allow adding, editing and deleting of heroes. - We configured an in-memory web API. -
  • We learned how to use Observables.
  • + - We learned how to use !{_Observable}s. - Below is a summary of the files we changed and added. + Here are the files we added or changed in this chapter. block file-summary +makeTabs( @@ -599,4 +623,4 @@ block file-summary hero-search.component.ts, hero-search.service.html, rxjs-operators.ts` -) + )