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
This commit is contained in:
Patrice Chalin 2016-08-02 09:59:35 -07:00 committed by Kathy Walrath
parent a49ecc7fd8
commit 8a6c5b5725
15 changed files with 326 additions and 139 deletions

View File

@ -1,41 +1,31 @@
// #docplaster // #docregion , search
// #docregion
import 'dart:async'; import 'dart:async';
import 'package:angular2/core.dart'; import 'package:angular2/core.dart';
// #docregion import-router
import 'package:angular2/router.dart'; import 'package:angular2/router.dart';
// #enddocregion import-router
import 'hero.dart'; import 'hero.dart';
import 'hero_service.dart'; import 'hero_service.dart';
import 'hero_search_component.dart';
@Component( @Component(
selector: 'my-dashboard', selector: 'my-dashboard',
// #docregion template-url
templateUrl: 'dashboard_component.html', templateUrl: 'dashboard_component.html',
// #enddocregion template-url styleUrls: const ['dashboard_component.css'],
// #docregion css directives: const [HeroSearchComponent])
styleUrls: const ['dashboard_component.css'] // #enddocregion search
// #enddocregion css
)
// #docregion component
class DashboardComponent implements OnInit { class DashboardComponent implements OnInit {
List<Hero> heroes; List<Hero> heroes;
// #docregion ctor
final Router _router; final Router _router;
final HeroService _heroService; final HeroService _heroService;
DashboardComponent(this._heroService, this._router); DashboardComponent(this._heroService, this._router);
// #enddocregion ctor
Future<Null> ngOnInit() async { Future<Null> ngOnInit() async {
heroes = (await _heroService.getHeroes()).skip(1).take(4).toList(); heroes = (await _heroService.getHeroes()).skip(1).take(4).toList();
} }
// #docregion goto-detail
void gotoDetail(Hero hero) { void gotoDetail(Hero hero) {
var link = [ var link = [
'HeroDetail', 'HeroDetail',
@ -43,5 +33,4 @@ class DashboardComponent implements OnInit {
]; ];
_router.navigate(link); _router.navigate(link);
} }
// #enddocregion goto-detail
} }

View File

@ -1,11 +1,10 @@
<!-- #docregion --> <!-- #docregion -->
<h3>Top Heroes</h3> <h3>Top Heroes</h3>
<div class="grid grid-pad"> <div class="grid grid-pad">
<!-- #docregion click --> <div *ngFor="let hero of heroes" (click)="gotoDetail(hero)" class="col-1-4">
<div *ngFor="let hero of heroes" (click)="gotoDetail(hero)" class="col-1-4" >
<!-- #enddocregion click -->
<div class="module hero"> <div class="module hero">
<h4>{{hero.name}}</h4> <h4>{{hero.name}}</h4>
</div> </div>
</div> </div>
</div> </div>
<hero-search></hero-search>

View File

@ -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;
}

View File

@ -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<List<Hero>> heroes;
// #enddocregion search
// #docregion searchTerms
StreamController<String> _searchTerms =
new StreamController<String>.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<Null> ngOnInit() async {
heroes = _searchTerms.stream
.transform(new Debounce(new Duration(milliseconds: 300)))
.distinct()
.transform(new FlatMapLatest((term) => term.isEmpty
? new Stream<List<Hero>>.fromIterable([<Hero>[]])
: _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);
}
}

View File

@ -0,0 +1,11 @@
<!-- #docregion -->
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<div>
<div *ngFor="let hero of heroes | async"
(click)="gotoDetail(hero)" class="search-result" >
{{hero.name}}
</div>
</div>
</div>

View File

@ -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<List<Hero>> 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');
}
}

View File

@ -1,3 +1,4 @@
/* #docregion */
.selected { .selected {
background-color: #CFD8DC !important; background-color: #CFD8DC !important;
color: white; color: white;
@ -57,3 +58,10 @@ button {
button:hover { button:hover {
background-color: #cfd8dc; background-color: #cfd8dc;
} }
/* #docregion additions */
.error {color:red;}
button.delete-button {
float:right;
background-color: gray !important;
color:white;
}

View File

@ -1,6 +1,7 @@
// #docregion // #docregion
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
// #docregion init // #docregion init
import 'package:angular2/core.dart'; import 'package:angular2/core.dart';
@ -23,17 +24,18 @@ class InMemoryDataService extends MockClient {
{'id': 19, 'name': 'Magma'}, {'id': 19, 'name': 'Magma'},
{'id': 20, 'name': 'Tornado'} {'id': 20, 'name': 'Tornado'}
]; ];
// #enddocregion init
static final List<Hero> _heroesDb = static final List<Hero> _heroesDb =
_initialHeroes.map((json) => new Hero.fromJson(json)).toList(); _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<Response> _handler(Request request) async { static Future<Response> _handler(Request request) async {
var data; var data;
switch (request.method) { switch (request.method) {
case 'GET': 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; break;
case 'POST': case 'POST':
var name = JSON.decode(request.body)['name']; var name = JSON.decode(request.body)['name'];

View File

@ -13,6 +13,7 @@ dependencies:
dart_to_js_script_rewriter: ^1.0.1 dart_to_js_script_rewriter: ^1.0.1
# #docregion additions # #docregion additions
http: ^0.11.0 http: ^0.11.0
stream_transformers: ^0.3.0
transformers: transformers:
- angular2: - angular2:
# #enddocregion additions # #enddocregion additions

View File

@ -7,7 +7,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="sample.css">
<script defer src="main.dart" type="application/dart"></script> <script defer src="main.dart" type="application/dart"></script>
<script defer src="packages/browser/dart.js"></script> <script defer src="packages/browser/dart.js"></script>

View File

@ -1,7 +0,0 @@
/* #docregion */
.error {color:red;}
button.delete-button {
float:right;
background-color: gray !important;
color:white;
}

View File

@ -1,6 +1,4 @@
// #docplaster // #docregion , search
// #docregion
// #docregion hero-search-component
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -14,9 +12,8 @@ import { HeroSearchComponent } from './hero-search.component';
styleUrls: ['app/dashboard.component.css'], styleUrls: ['app/dashboard.component.css'],
directives: [HeroSearchComponent] directives: [HeroSearchComponent]
}) })
// #enddocregion hero-search-component // #enddocregion search
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
heroes: Hero[] = []; heroes: Hero[] = [];
constructor( constructor(

View File

@ -18,7 +18,7 @@ export class HeroSearchComponent implements OnInit {
heroes: Observable<Hero[]>; heroes: Observable<Hero[]>;
// #enddocregion search // #enddocregion search
// #docregion searchTerms // #docregion searchTerms
searchTerms = new Subject<string>(); private searchTerms = new Subject<string>();
// #enddocregion searchTerms // #enddocregion searchTerms
constructor( constructor(

View File

@ -25,7 +25,8 @@ block http-library
### Pubspec updates ### 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 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 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 [guide-http]: ../guide/server-communication.html#!#http-providers
[ng2x]: https://github.com/angular/angular/wiki/Angular-2-Dart-Transformer [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) +makeExcerpt('pubspec.yaml', 'additions', null, stylePattern)
block http-providers block http-providers
@ -55,7 +56,13 @@ block backend
implementations. implementations.
block dont-be-distracted-by-backend-subst 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 block get-heroes-details
:marked :marked
@ -89,8 +96,42 @@ block heroes-comp-add
block review block review
//- Not showing animated gif due to differences between TS and Dart implementations. //- Not showing animated gif due to differences between TS and Dart implementations.
block observables-section block observables-section-intro
//- TBC :marked
Recall that `HeroService.getHeroes()` awaits for an `http.get()`
response and yields a _Future_ `List<Hero>`, 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 block filetree
.filetree .filetree
@ -98,47 +139,65 @@ block filetree
.children .children
.file lib .file lib
.children .children
.file app_component.dart
.file app_component.css .file app_component.css
.file app_component.dart
.file dashboard_component.css .file dashboard_component.css
.file dashboard_component.html
.file dashboard_component.dart .file dashboard_component.dart
.file dashboard_component.html
.file hero.dart .file hero.dart
.file hero_detail_component.css .file hero_detail_component.css
.file hero_detail_component.html
.file hero_detail_component.dart .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 hero_service.dart
.file heroes_component.css .file heroes_component.css
.file heroes_component.html
.file heroes_component.dart .file heroes_component.dart
.file main.dart .file heroes_component.html
.file in_memory_data_service.dart (new) .file in_memory_data_service.dart (new)
.file web .file web
.children .children
.file main.dart .file main.dart
.file index.html .file index.html
.file sample.css (new)
.file styles.css .file styles.css
.file pubspec.yaml .file pubspec.yaml
block file-summary block file-summary
+makeTabs( +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.dart,
toh-6/dart/lib/hero_detail_component.html, toh-6/dart/lib/hero_detail_component.html,
toh-6/dart/lib/hero_service.dart, toh-6/dart/lib/hero_service.dart,
toh-6/dart/lib/heroes_component.dart, toh-6/dart/lib/heroes_component.css,
toh-6/dart/web/index.html, toh-6/dart/lib/heroes_component.dart`,
toh-6/dart/web/main.dart, null,
toh-6/dart/web/sample.css`, `lib/dashboard_component.dart,
`,,,,,,final,`, lib/dashboard_component.html,
`lib/hero.dart, lib/hero.dart,
lib/hero_detail_component.dart, lib/hero_detail_component.dart,
lib/hero_detail_component.html, lib/hero_detail_component.html,
lib/hero_service.dart, lib/hero_service.dart,
lib/heroes_component.dart, lib/heroes_component.css,
web/index.html, lib/heroes_component.dart`)
web/main.dart,
web/sample.css`)
+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`)

View File

@ -111,7 +111,7 @@ block dont-be-distracted-by-backend-subst
Look at our current `HeroService` implementation 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 :marked
We returned a !{_Promise} resolved with mock heroes. 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 For *now* we get back on familiar ground by immediately by
converting that `Observable` to a `Promise` using the `toPromise` operator. 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 :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. 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. 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. 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: 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 :marked
### Extracting the data in the *then* callback ### 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. data within the response.
+makeExample('toh-6/ts/app/hero.service.ts', 'to-data')(format=".") +makeExcerpt('app/hero.service.ts', 'to-data', '')
:marked :marked
That response JSON has a single `data` property. That response JSON has a single `data` property.
@ -172,12 +172,15 @@ block get-heroes-details
### Error Handling ### Error Handling
At the end of `getHeroes()` we `catch` server failures and pass them to an error handler: 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 :marked
This is a critical step! This is a critical step!
We must anticipate HTTP failures as they happen frequently for reasons beyond our control. 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'; - var rejected_promise = _docsFor == 'dart' ? 'propagated exception' : 'rejected promise';
:marked :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 should do better in real life.
@ -209,7 +212,7 @@ block get-heroes-details
### Put ### 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') +makeExcerpt('app/hero.service.ts', 'put')
@ -259,7 +262,7 @@ block hero-detail-comp-updates
+makeExcerpt('app/hero-detail.component.ts', 'ngOnInit') +makeExcerpt('app/hero-detail.component.ts', 'ngOnInit')
:marked :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 :marked
Add a save method to `HeroDetailComponent` and call the corresponding save method in `HeroesService`. 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. The user can *delete* an existing hero by clicking a delete button next to the hero's name.
Add the following to the heroes component HTML right after the hero name in the repeated `<li>` tag: Add the following to the heroes component HTML right after the hero name in the repeated `<li>` tag:
+makeExample('app/heroes.component.html', 'delete') +makeExcerpt('app/heroes.component.html', 'delete')
:marked :marked
Now let's fix-up the `HeroesComponent` to support the *add* and *delete* actions used in the template. 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 ### Let's see it
Here are the fruits of labor in action: Here are the fruits of labor in action:
figure.image-display 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 :marked
## Observables
Each `Http` method returns an `Observable` of HTTP `Response` objects. 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. 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 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. and it helps that promises are widely understood by JavaScript programmers.
But requests aren't always "one and done". We may start one request, :marked
then cancel it, and make a different request ... before the server has responded to the first request. But requests aren't always "one and done". We may start one request,
Such a _request-cancel-new-request_ sequence is difficult to implement with *promises*. then cancel it, and make a different request before the server has responded to the first request.
It's easy with *observables* as we'll see. 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 ### Search-by-name
We're going to add a *hero search* feature to the Tour of Heroes. 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. 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.
<span if-docs="ts">Another notable difference: we no longer call `toPromise`,
we simply return the *observable* instead.</span>
### HeroSearchComponent
Let's create a new `HeroSearchComponent` that calls this new `HeroSearchService`.
The component template is simple &mdash; 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 :marked
The `http.get` call in `HeroSearchService` is similar to the `http.get` call in the `HeroService`. A `Subject` is a producer of an _observable_ event stream;
The notable difference: we no longer call `toPromise`. `searchTerms` produces an `Observable` of strings, the filter criteria for the name search.
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.
Each call to `search` puts a new string into this subject's _observable_ stream by calling `next`. Each call to `search` puts a new string into this subject's _observable_ stream by calling `next`.
A `Subject` is also an `Observable`. :marked
We're going to access that `Observable` and turn the stream <a id="ngoninit"></a>
of strings into a stream of `Hero[]` arrays, the `heroes` property. #### Initialize the _**heroes**_ property (_**ngOnInit**_)
+makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".") <span if-docs="ts">A `Subject` is also an `Observable`.</span>
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 :marked
If we passed every user keystroke directly to the `HeroSearchService`, we'd unleash a storm of http requests. Fortunately, we can chain `Observable` operators to the string `Observable` that reduce the request flow.
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.
We'll make fewer calls to the `HeroSearchService` and still get timely results. Here's how: 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 * `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 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. 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 :marked
We load them all at once by importing `rxjs-extensions` in `AppComponent`. 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 :marked
### Add the search component to the dashboard ### 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 :marked
And finally, we import the `HeroSearchComponent` and add it to the `directives` array. 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 :marked
Run the app again, go to the *Dashboard*, and enter some text in the search box below the hero tiles. Run the app again, go to the *Dashboard*, and enter some text in the search box.
At some point it might look like this. At some point it might look like this.
figure.image-display figure.image-display
img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component") img(src='/resources/images/devguide/toh/toh-hero-search.png' alt="Hero Search Component")
.l-main-section .l-main-section
:marked :marked
@ -534,9 +558,9 @@ block filetree
.file hero-detail.component.css .file hero-detail.component.css
.file hero-detail.component.html .file hero-detail.component.html
.file hero-detail.component.ts .file hero-detail.component.ts
.file hero-search.component.html .file hero-search.component.html (new)
.file hero-search.component.ts .file hero-search.component.ts (new)
.file hero-search.service.ts .file hero-search.service.ts (new)
.file rxjs-operators.ts .file rxjs-operators.ts
.file hero.service.ts .file hero.service.ts
.file heroes.component.css .file heroes.component.css
@ -559,14 +583,14 @@ block filetree
## Home Stretch ## Home Stretch
We are at the end of our journey for now, but we have accomplished a lot. 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 added the necessary dependencies to use HTTP in our application.
- We refactored HeroService to load heroes from an API. - We refactored `HeroService` to load heroes from a web API.
- We extended HeroService to support post, put and delete calls. - We extended `HeroService` to support post, put and delete methods.
- We updated our components to allow adding, editing and deleting of heroes. - We updated our components to allow adding, editing and deleting of heroes.
- We configured an in-memory web API. - We configured an in-memory web API.
<li if-docs="ts"> We learned how to use Observables.</li> - 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 block file-summary
+makeTabs( +makeTabs(
@ -599,4 +623,4 @@ block file-summary
hero-search.component.ts, hero-search.component.ts,
hero-search.service.html, hero-search.service.html,
rxjs-operators.ts` rxjs-operators.ts`
) )