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:
parent
a49ecc7fd8
commit
8a6c5b5725
|
@ -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<Hero> heroes;
|
||||
|
||||
// #docregion ctor
|
||||
final Router _router;
|
||||
final HeroService _heroService;
|
||||
|
||||
DashboardComponent(this._heroService, this._router);
|
||||
|
||||
// #enddocregion ctor
|
||||
|
||||
Future<Null> 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
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
<!-- #docregion -->
|
||||
<h3>Top Heroes</h3>
|
||||
<div class="grid grid-pad">
|
||||
<!-- #docregion click -->
|
||||
<div *ngFor="let hero of heroes" (click)="gotoDetail(hero)" class="col-1-4">
|
||||
<!-- #enddocregion click -->
|
||||
<div class="module hero">
|
||||
<h4>{{hero.name}}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hero-search></hero-search>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<Hero> _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<Response> _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'];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="sample.css">
|
||||
|
||||
<script defer src="main.dart" type="application/dart"></script>
|
||||
<script defer src="packages/browser/dart.js"></script>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
/* #docregion */
|
||||
.error {color:red;}
|
||||
button.delete-button {
|
||||
float:right;
|
||||
background-color: gray !important;
|
||||
color:white;
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -18,7 +18,7 @@ export class HeroSearchComponent implements OnInit {
|
|||
heroes: Observable<Hero[]>;
|
||||
// #enddocregion search
|
||||
// #docregion searchTerms
|
||||
searchTerms = new Subject<string>();
|
||||
private searchTerms = new Subject<string>();
|
||||
// #enddocregion searchTerms
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -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<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
|
||||
.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`)
|
||||
|
|
|
@ -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 `<li>` 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
|
||||
## Observables
|
||||
## !{_Observable}s
|
||||
|
||||
block observables-section-intro
|
||||
:marked
|
||||
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.
|
||||
|
||||
: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 *promises*.
|
||||
It's easy with *observables* as we'll see.
|
||||
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.
|
||||
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.
|
||||
|
||||
+makeExample('toh-6/ts/app/hero-search.service.ts', null, 'app/hero-search.service.ts')(format=".")
|
||||
+makeExample('app/hero-search.service.ts')
|
||||
|
||||
: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.
|
||||
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 - just a textbox and a list of matching search results.
|
||||
+makeExample('toh-6/ts/app/hero-search.component.html', null,'hero-search.component.html')
|
||||
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` with the new search box value.
|
||||
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 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`.
|
||||
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('toh-6/ts/app/hero-search.component.ts', null,'hero-search.component.ts')
|
||||
|
||||
+makeExample('app/hero-search.component.ts')
|
||||
|
||||
:marked
|
||||
Focus on the `searchTerms`.
|
||||
+makeExample('toh-6/ts/app/hero-search.component.ts', 'searchTerms')(format=".")
|
||||
#### Search terms
|
||||
|
||||
Let's focus on the `!{_priv}searchTerms`:
|
||||
|
||||
+makeExcerpt('app/hero-search.component.ts', 'searchTerms', '')
|
||||
|
||||
block search-criteria-intro
|
||||
: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.
|
||||
|
||||
+makeExample('toh-6/ts/app/hero-search.component.ts', 'search')(format=".")
|
||||
:marked
|
||||
If we passed every user keystroke directly to the `HeroSearchService`, we'd unleash a storm of http requests.
|
||||
<a id="ngoninit"></a>
|
||||
#### Initialize the _**heroes**_ property (_**ngOnInit**_)
|
||||
|
||||
<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.
|
||||
|
||||
Fortunately we can chain `Observable` operators to the string `Observable` that reduce the request flow.
|
||||
block observable-transformers
|
||||
:marked
|
||||
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,11 +509,12 @@ 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
|
||||
|
@ -500,12 +524,12 @@ block observables-section
|
|||
+makeExample('app/dashboard.component.html')
|
||||
|
||||
: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
|
||||
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.
|
||||
|
||||
figure.image-display
|
||||
|
@ -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.
|
||||
<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
|
||||
+makeTabs(
|
||||
|
|
Loading…
Reference in New Issue