620 lines
23 KiB
Plaintext
620 lines
23 KiB
Plaintext
- var _example = 'toh-6';
|
|
|
|
block includes
|
|
include ../_util-fns
|
|
- var _Http = 'Http'; // Angular `Http` library name.
|
|
- var _Angular_Http = 'Angular <code>Http</code>'
|
|
- var _Angular_http_library = 'Angular HTTP library'
|
|
- var _HttpModule = 'HttpModule'
|
|
- var _JSON_stringify = 'JSON.stringify'
|
|
|
|
//- Shared var definitions
|
|
- var _promise = _Promise.toLowerCase()
|
|
|
|
:marked
|
|
# Getting and Saving Data
|
|
|
|
Our stakeholders appreciate our progress.
|
|
Now they want to get the hero data from a server, let users add, edit, and delete heroes,
|
|
and save these changes back to the server.
|
|
|
|
In this chapter we teach our application to make the corresponding HTTP calls to a remote server's web API.
|
|
|
|
Run the <live-example></live-example> for this part.
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Where We Left Off
|
|
|
|
In the [previous chapter](toh-pt5.html), we learned to navigate between the dashboard and the fixed heroes list, editing a selected hero along the way.
|
|
That's our starting point for this chapter.
|
|
|
|
block start-server-and-watch
|
|
:marked
|
|
### Keep the app transpiling and running
|
|
|
|
Open a terminal/console window and enter the following command to
|
|
start the TypeScript compiler, start the server, and watch for changes:
|
|
|
|
code-example(language="bash").
|
|
npm start
|
|
|
|
:marked
|
|
The application runs and updates automatically as we continue to build the Tour of Heroes.
|
|
|
|
.l-main-section#http-providers
|
|
h1 Providing HTTP Services
|
|
block http-library
|
|
:marked
|
|
The `HttpModule` is ***not*** a core Angular module.
|
|
It's Angular's optional approach to web access and it exists as a separate add-on module called `@angular/http`,
|
|
shipped in a separate script file as part of the Angular npm package.
|
|
|
|
Fortunately we're ready to import from `@angular/http` because `systemjs.config` configured *SystemJS* to load that library when we need it.
|
|
|
|
:marked
|
|
### Register for HTTP services
|
|
|
|
block http-providers
|
|
:marked
|
|
Our app will depend upon the Angular `http` service which itself depends upon other supporting services.
|
|
The `HttpModule` from `@angular/http` library holds providers for a complete set of HTTP services.
|
|
|
|
We should be able to access these services from anywhere in the application.
|
|
So we register them all by adding `HttpModule` to the `imports` list of the `AppModule` where we
|
|
bootstrap the application and its root `AppComponent`.
|
|
|
|
+makeExample('app/app.module.ts', 'v1','app/app.module.ts (v1)')
|
|
|
|
:marked
|
|
Notice that we supply `!{_HttpModule}` as part of the *imports* !{_array} in root NgModule `AppModule`.
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Simulating the web API
|
|
|
|
We recommend registering application-wide services in the root
|
|
`!{_AppModuleVsAppComp}` *providers*. <span if-docs="dart">Here we're
|
|
registering in `main` for a special reason.</span>
|
|
|
|
Our application is in the early stages of development and far from ready for production.
|
|
We don't even have a web server that can handle requests for heroes.
|
|
Until we do, *we'll have to fake it*.
|
|
|
|
We're going to *trick* the HTTP client into fetching and saving data from
|
|
a mock service, the *in-memory web API*.
|
|
<span if-docs="dart"> The application itself doesn't need to know and
|
|
shouldn't know about this. So we'll slip the in-memory web API into the
|
|
configuration *above* the `AppComponent`.</span>
|
|
|
|
Here is a version of <span ngio-ex>!{_appModuleTsVsMainTs}</span> that performs this trick:
|
|
|
|
+makeExcerpt(_appModuleTsVsMainTs, 'v2')
|
|
|
|
block backend
|
|
:marked
|
|
We're importing the `InMemoryWebApiModule` and adding it to the module `imports`.
|
|
The `InMemoryWebApiModule` replaces the default `Http` client backend —
|
|
the supporting service that talks to the remote server —
|
|
with an _in-memory web API alternative service_.
|
|
|
|
+makeExcerpt(_appModuleTsVsMainTs, 'in-mem-web-api', '')
|
|
|
|
:marked
|
|
The `forRoot` configuration method takes an `InMemoryDataService` class
|
|
that primes the in-memory database as follows:
|
|
|
|
+makeExample('app/in-memory-data.service.ts', 'init')(format='.')
|
|
|
|
p This file replaces the #[code #[+adjExPath('mock-heroes.ts')]] which is now safe to delete.
|
|
|
|
block dont-be-distracted-by-backend-subst
|
|
.alert.is-helpful
|
|
:marked
|
|
This chapter is an introduction to the !{_Angular_http_library}.
|
|
Please don't be distracted by the details of this backend substitution. Just follow along with the example.
|
|
|
|
Learn more later about the in-memory web API in the [HTTP client chapter](../guide/server-communication.html#!#in-mem-web-api).
|
|
Remember, the in-memory web API is only useful in the early stages of development and for demonstrations such as this Tour of Heroes.
|
|
Skip it when you have a real web API server.
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Heroes and HTTP
|
|
|
|
Look at our current `HeroService` implementation
|
|
|
|
+makeExcerpt('toh-4/ts/app/hero.service.ts (old getHeroes)', 'get-heroes')
|
|
|
|
:marked
|
|
We returned a !{_Promise} resolved with mock heroes.
|
|
It may have seemed like overkill at the time, but we were anticipating the
|
|
day when we fetched heroes with an HTTP client and we knew that would have to be an asynchronous operation.
|
|
|
|
That day has arrived! Let's convert `getHeroes()` to use HTTP.
|
|
|
|
+makeExcerpt('app/hero.service.ts (updated getHeroes and new class members)', 'getHeroes')
|
|
|
|
:marked
|
|
Our updated import statements are now:
|
|
|
|
+makeExcerpt('app/hero.service.ts (updated imports)', 'imports')
|
|
|
|
- var _h3id = `http-${_promise}`
|
|
:marked
|
|
Refresh the browser, and the hero data should be successfully loaded from the
|
|
mock server.
|
|
|
|
<h3 id="!{_h3id}">HTTP !{_Promise}</h3>
|
|
|
|
We're still returning a !{_Promise} but we're creating it differently.
|
|
|
|
block get-heroes-details
|
|
:marked
|
|
The Angular `http.get` returns an RxJS `Observable`.
|
|
*Observables* are a powerful way to manage asynchronous data flows.
|
|
We'll learn about [Observables](#observables) later in this chapter.
|
|
|
|
For *now* we get back on familiar ground by immediately by
|
|
converting that `Observable` to a `Promise` using the `toPromise` operator.
|
|
|
|
+makeExcerpt('app/hero.service.ts', 'to-promise', '')
|
|
|
|
:marked
|
|
Unfortunately, the Angular `Observable` doesn't have a `toPromise` operator ...
|
|
not out of the box.
|
|
The Angular `Observable` is a bare-bones implementation.
|
|
|
|
There are scores of operators like `toPromise` that extend `Observable` with useful capabilities.
|
|
If we want those capabilities, we have to add the operators ourselves.
|
|
That's as easy as importing them from the RxJS library like this:
|
|
|
|
+makeExcerpt('app/hero.service.ts', 'rxjs', '')
|
|
|
|
:marked
|
|
### Extracting the data in the *then* callback
|
|
|
|
In the *promise*'s `then` callback we call the `json` method of the HTTP `Response` to extract the
|
|
data within the response.
|
|
+makeExcerpt('app/hero.service.ts', 'to-data', '')
|
|
|
|
:marked
|
|
That response JSON has a single `data` property.
|
|
The `data` property holds the !{_array} of *heroes* that the caller really wants.
|
|
So we grab that !{_array} and return it as the resolved !{_Promise} value.
|
|
|
|
.alert.is-important
|
|
:marked
|
|
Pay close attention to the shape of the data returned by the server.
|
|
This particular *in-memory web API* example happens to return an object with a `data` property.
|
|
Your API might return something else. Adjust the code to match *your web API*.
|
|
|
|
:marked
|
|
The caller is unaware of these machinations. It receives a !{_Promise} of *heroes* just as it did before.
|
|
It has no idea that we fetched the heroes from the (mock) server.
|
|
It knows nothing of the twists and turns required to convert the HTTP response into heroes.
|
|
Such is the beauty and purpose of delegating data access to a service like this `HeroService`.
|
|
|
|
### Error Handling
|
|
|
|
At the end of `getHeroes()` we `catch` server failures and pass them to an error handler:
|
|
|
|
+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', '')
|
|
|
|
- var rejected_promise = _docsFor == 'dart' ? 'propagated exception' : 'rejected promise';
|
|
:marked
|
|
In this demo service we log the error to the console; we would do better in real life.
|
|
|
|
We've also decided to return a user friendly form of the error to
|
|
the caller in a !{rejected_promise} so that the caller can display a proper error message to the user.
|
|
|
|
### Unchanged `getHeroes` API
|
|
|
|
Although we made significant *internal* changes to `getHeroes()`, the public signature did not change.
|
|
We still return a !{_Promise}. We won't have to update any of the components that call `getHeroes()`.
|
|
|
|
Our stakeholders are thrilled with the added flexibility from the API integration.
|
|
Now they want the ability to create and delete heroes.
|
|
|
|
Let's see first what happens when we try to update a hero's details.
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Update hero details
|
|
|
|
We can edit a hero's name already in the hero detail view. Go ahead and try
|
|
it. As we type, the hero name is updated in the view heading.
|
|
But when we hit the `Back` button, the changes are lost!
|
|
|
|
.l-sub-section
|
|
:marked
|
|
Updates weren't lost before, what's happening?
|
|
When the app used a list of mock heroes, changes were made directly to the
|
|
hero objects in the single, app-wide shared list. Now that we are fetching data
|
|
from a server, if we want changes to persist, we'll need to write them back to
|
|
the server.
|
|
|
|
:marked
|
|
### Save hero details
|
|
|
|
Let's ensure that edits to a hero's name aren't lost. Start by adding,
|
|
to the end of the hero detail template, a save button with a `click` event
|
|
binding that invokes a new component method named `save`:
|
|
|
|
+makeExcerpt('app/hero-detail.component.html', 'save')
|
|
|
|
:marked
|
|
The `save` method persists hero name changes using the hero service
|
|
`update` method and then navigates back to the previous view:
|
|
|
|
+makeExcerpt('app/hero-detail.component.ts', 'save')
|
|
|
|
:marked
|
|
### Hero service `update` method
|
|
|
|
The overall structure of the `update` method is similar to that of
|
|
`getHeroes`, although we'll use an HTTP _put_ to persist changes
|
|
server-side:
|
|
|
|
+makeExcerpt('app/hero.service.ts', 'update')
|
|
|
|
:marked
|
|
We identify _which_ hero the server should update by encoding the hero id in
|
|
the URL. The put body is the JSON string encoding of the hero, obtained by
|
|
calling `!{_JSON_stringify}`. We identify the body content type
|
|
(`application/json`) in the request header.
|
|
|
|
Refresh the browser and give it a try. Changes to hero names should now persist.
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Add a hero
|
|
|
|
To add a new hero we need to know the hero's name. Let's use an input
|
|
element for that, paired with an add button.
|
|
|
|
Insert the following into the heroes component HTML, first thing after
|
|
the heading:
|
|
|
|
+makeExcerpt('app/heroes.component.html', 'add')
|
|
|
|
:marked
|
|
In response to a click event, we call the component's click handler and then
|
|
clear the input field so that it will be ready to use for another name.
|
|
|
|
+makeExcerpt('app/heroes.component.ts', 'add')
|
|
|
|
:marked
|
|
When the given name is non-blank, the handler delegates creation of the
|
|
named hero to the hero service, and then adds the new hero to our !{_array}.
|
|
|
|
Go ahead, refresh the browser and create some new heroes!
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Delete a hero
|
|
|
|
Too many heroes?
|
|
Let's add a delete button to each hero in the heroes view.
|
|
|
|
Add this button element to the heroes component HTML, right after the hero
|
|
name in the repeated `<li>` tag:
|
|
|
|
+makeExcerpt('app/heroes.component.html', 'delete', '')
|
|
|
|
:marked
|
|
The `<li>` element should now look like this:
|
|
|
|
+makeExcerpt('app/heroes.component.html', 'li-element')
|
|
|
|
:marked
|
|
In addition to calling the component's `delete` method, the delete button
|
|
click handling code stops the propagation of the click event — we
|
|
don't want the `<li>` click handler to be triggered because that would
|
|
select the hero that we are going to delete!
|
|
|
|
The logic of the `delete` handler is a bit trickier:
|
|
|
|
+makeExcerpt('app/heroes.component.ts', 'delete')
|
|
|
|
:marked
|
|
Of course, we delegate hero deletion to the hero service, but the component
|
|
is still responsible for updating the display: it removes the deleted hero
|
|
from the !{_array} and resets the selected hero if necessary.
|
|
|
|
:marked
|
|
We want our delete button to be placed at the far right of the hero entry.
|
|
This extra CSS accomplishes that:
|
|
|
|
+makeExcerpt('app/heroes.component.css', 'additions')
|
|
|
|
:marked
|
|
### Hero service `delete` method
|
|
|
|
The hero service's `delete` method uses the _delete_ HTTP method to remove the hero from the server:
|
|
|
|
+makeExcerpt('app/hero.service.ts', 'delete')
|
|
|
|
:marked
|
|
Refresh the browser and try the new delete functionality.
|
|
|
|
:marked
|
|
## !{_Observable}s
|
|
|
|
block observables-section-intro
|
|
:marked
|
|
Each `Http` service method returns an `Observable` of HTTP `Response` objects.
|
|
|
|
Our `HeroService` converts that `Observable` into a `Promise` and returns the promise to the caller.
|
|
In this section we learn to return the `Observable` directly and discuss when and why that might be
|
|
a good thing to do.
|
|
|
|
### Background
|
|
An *observable* is a stream of events that we can process with array-like operators.
|
|
|
|
Angular core has basic support for observables. We developers augment that support with
|
|
operators and extensions from the [RxJS Observables](http://reactivex.io/rxjs/) library.
|
|
We'll see how shortly.
|
|
|
|
Recall that our `HeroService` quickly chained the `toPromise` operator to the `Observable` result of `http.get`.
|
|
That operator converted the `Observable` into a `Promise` and we passed that promise back to the caller.
|
|
|
|
Converting to a promise is often a good choice. We typically ask `http.get` to fetch a single chunk of data.
|
|
When we receive the data, we're done.
|
|
A single result in the form of a promise is easy for the calling component to consume
|
|
and it helps that promises are widely understood by JavaScript programmers.
|
|
|
|
: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.
|
|
|
|
We start by creating `HeroSearchService` that sends search queries to our server's web api.
|
|
|
|
+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 — just a text box and a list of matching search results.
|
|
|
|
+makeExample('app/hero-search.component.html')
|
|
:marked
|
|
We'll also want to add styles for the new component.
|
|
+makeExample('app/hero-search.component.css')
|
|
: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
|
|
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`.
|
|
|
|
:marked
|
|
<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.
|
|
|
|
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
|
|
before passing along the latest string. We'll never make requests more frequently than 300ms.
|
|
|
|
* `distinctUntilChanged` ensures that we only send a request if the filter text changed.
|
|
There's no point in repeating a request for the same search term.
|
|
|
|
* `switchMap` calls our search service for each search term that makes it through the `debounce` and `distinctUntilChanged` gauntlet.
|
|
It cancels and discards previous search observables, returning only the latest search service observable.
|
|
|
|
.l-sub-section
|
|
:marked
|
|
The [switchMap operator](http://www.learnrxjs.io/operators/transformation/switchmap.html)
|
|
(formerly known as "flatMapLatest") is very clever.
|
|
|
|
Every qualifying key event can trigger an `http` method call.
|
|
Even with a 300ms pause between requests, we could have multiple HTTP requests in flight
|
|
and they may not return in the order sent.
|
|
|
|
`switchMap` preserves the original request order while returning
|
|
only the observable from the most recent `http` method call.
|
|
Results from prior calls are canceled and discarded.
|
|
|
|
We also short-circuit the `http` method call and return an observable containing an empty array
|
|
if the search text is empty.
|
|
|
|
Note that _canceling_ the `HeroSearchService` observable won't actually abort a pending HTTP request
|
|
until the service supports that feature, a topic for another day.
|
|
We are content for now to discard unwanted results.
|
|
:marked
|
|
* `catch` intercepts a failed observable.
|
|
Our simple example prints the error to the console; a real life application should do better.
|
|
Then we return an observable containing an empty array to clear the search result.
|
|
|
|
### Import RxJS operators
|
|
The RxJS operators are not available in Angular's base `Observable` implementation.
|
|
We have to extend `Observable` by *importing* them.
|
|
|
|
We could extend `Observable` with just the operators we need here by
|
|
including the pertinent `import` statements at the top of this file.
|
|
|
|
.l-sub-section
|
|
:marked
|
|
Many authorities say we should do just that.
|
|
:marked
|
|
We take a different approach in this example.
|
|
We combine all of the RxJS `Observable` extensions that _our entire app_ requires into a single RxJS imports file.
|
|
|
|
+makeExample('app/rxjs-extensions.ts')(format='.')
|
|
|
|
:marked
|
|
We load them all at once by importing `rxjs-extensions` at the top of `AppModule`.
|
|
|
|
+makeExcerpt('app/app.module.ts', 'rxjs-extensions')(format='.')
|
|
|
|
:marked
|
|
### Add the search component to the dashboard
|
|
|
|
We add the hero search HTML element to the bottom of the `DashboardComponent` template.
|
|
|
|
+makeExample('app/dashboard.component.html')(format='.')
|
|
|
|
- var _declarations = _docsFor == 'dart' ? 'directives' : 'declarations'
|
|
- var declFile = _docsFor == 'dart' ? 'app/dashboard.component.ts' : 'app/app.module.ts'
|
|
:marked
|
|
Finally, we import `HeroSearchComponent` from
|
|
<span ngio-ex>hero-search.component.ts</span>
|
|
and add it to the `!{_declarations}` !{_array}:
|
|
|
|
+makeExcerpt(declFile, 'search')
|
|
|
|
: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")
|
|
|
|
.l-main-section
|
|
:marked
|
|
## Application structure and code
|
|
|
|
Review the sample source code in the <live-example></live-example> for this chapter.
|
|
Verify that we have the following structure:
|
|
|
|
block filetree
|
|
.filetree
|
|
.file angular2-tour-of-heroes
|
|
.children
|
|
.file app
|
|
.children
|
|
.file app.component.ts
|
|
.file app.component.css
|
|
.file app.module.ts
|
|
.file app.routing.ts
|
|
.file dashboard.component.css
|
|
.file dashboard.component.html
|
|
.file dashboard.component.ts
|
|
.file hero.ts
|
|
.file hero-detail.component.css
|
|
.file hero-detail.component.html
|
|
.file hero-detail.component.ts
|
|
.file hero-search.component.html (new)
|
|
.file hero-search.component.css (new)
|
|
.file hero-search.component.ts (new)
|
|
.file hero-search.service.ts (new)
|
|
.file rxjs-extensions.ts
|
|
.file hero.service.ts
|
|
.file heroes.component.css
|
|
.file heroes.component.html
|
|
.file heroes.component.ts
|
|
.file main.ts
|
|
.file in-memory-data.service.ts (new)
|
|
.file node_modules ...
|
|
.file typings ...
|
|
.file index.html
|
|
.file package.json
|
|
.file styles.css
|
|
.file systemjs.config.js
|
|
.file tsconfig.json
|
|
.file typings.json
|
|
|
|
.l-main-section
|
|
:marked
|
|
## 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 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 !{_Observable}s.
|
|
|
|
Here are the files we added or changed in this chapter.
|
|
|
|
block file-summary
|
|
+makeTabs(
|
|
`toh-6/ts/app/app.component.ts,
|
|
toh-6/ts/app/app.module.ts,
|
|
toh-6/ts/app/heroes.component.ts,
|
|
toh-6/ts/app/heroes.component.html,
|
|
toh-6/ts/app/heroes.component.css,
|
|
toh-6/ts/app/hero-detail.component.ts,
|
|
toh-6/ts/app/hero-detail.component.html,
|
|
toh-6/ts/app/hero.service.ts,
|
|
toh-6/ts/app/in-memory-data.service.ts`,
|
|
',,,,,,,,',
|
|
`app.comp...ts,
|
|
app.mod...ts,
|
|
heroes.comp...ts,
|
|
heroes.comp...html,
|
|
heroes.comp...css,
|
|
hero-detail.comp...ts,
|
|
hero-detail.comp...html,
|
|
hero.service.ts,
|
|
in-memory-data.service.ts`
|
|
)
|
|
|
|
+makeTabs(
|
|
`toh-6/ts/app/hero-search.service.ts,
|
|
toh-6/ts/app/hero-search.component.ts,
|
|
toh-6/ts/app/hero-search.component.html,
|
|
toh-6/ts/app/hero-search.component.css,
|
|
toh-6/ts/app/rxjs-extensions.ts`,
|
|
null,
|
|
`hero-search.service.ts,
|
|
hero-search.component.ts,
|
|
hero-search.component.html,
|
|
hero-search.component.css,
|
|
rxjs-extensions.ts`
|
|
)
|