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
// #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
}

View File

@ -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 *ngFor="let hero of heroes" (click)="gotoDetail(hero)" class="col-1-4">
<div class="module hero">
<h4>{{hero.name}}</h4>
</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 {
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;
}

View File

@ -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'];

View File

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

View File

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

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
// #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(

View File

@ -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(

View File

@ -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`)

View File

@ -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
## !{_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.
<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
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
<a id="ngoninit"></a>
#### 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
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.
<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(
@ -599,4 +623,4 @@ block file-summary
hero-search.component.ts,
hero-search.service.html,
rxjs-operators.ts`
)
)