docs: clarify toh (#28571)

PR Close #28571
This commit is contained in:
Kapunahele Wong 2019-01-31 14:25:30 -05:00 committed by Miško Hevery
parent cb848b9410
commit 8f084d7214
14 changed files with 456 additions and 567 deletions

View File

@ -1,8 +1,4 @@
/* HeroesComponent's private CSS styles */
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
@ -19,18 +15,18 @@
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
.heroes li.selected {
background-color: #CFD8DC;
color: white;
}
.heroes li.selected:hover {
background-color: #BBD8DC;
color: white;
}
.heroes .badge {
display: inline-block;

View File

@ -34,4 +34,7 @@ export class HeroesComponent implements OnInit {
this.selectedHero = hero;
}
// #enddocregion on-select
// #docregion component
}
// #enddocregion component

View File

@ -1,8 +1,4 @@
/* HeroesComponent's private CSS styles */
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
@ -19,18 +15,18 @@
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
.heroes li.selected {
background-color: #CFD8DC;
color: white;
}
.heroes li.selected:hover {
background-color: #BBD8DC;
color: white;
}
.heroes .badge {
display: inline-block;

View File

@ -1,8 +1,4 @@
/* HeroesComponent's private CSS styles */
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
@ -19,18 +15,18 @@
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
.heroes li.selected {
background-color: #CFD8DC;
color: white;
}
.heroes li.selected:hover {
background-color: #BBD8DC;
color: white;
}
.heroes .badge {
display: inline-block;
@ -43,8 +39,6 @@
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
const routes: Routes = [
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -7,9 +7,7 @@ import { RouterModule, Routes } from '@angular/router';
// #docregion import-dashboard
import { DashboardComponent } from './dashboard/dashboard.component';
// #enddocregion import-dashboard
// #docregion heroes-route
import { HeroesComponent } from './heroes/heroes.component';
// #enddocregion heroes-route
// #docregion import-herodetail
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
// #enddocregion import-herodetail
@ -39,7 +37,9 @@ const routes: Routes = [
imports: [ RouterModule.forRoot(routes) ],
// #enddocregion ngmodule-imports
// #docregion v1
// #docregion export-routermodule
exports: [ RouterModule ]
// #enddocregion export-routermodule
})
export class AppRoutingModule {}
// #enddocregion , v1

View File

@ -23,13 +23,17 @@ import { HeroSearchComponent } from './hero-search/hero-search.component';
// #docregion v1
import { MessagesComponent } from './messages/messages.component';
// #docregion import-httpclientmodule
@NgModule({
imports: [
// #enddocregion import-httpclientmodule
BrowserModule,
FormsModule,
AppRoutingModule,
// #docregion in-mem-web-api-imports
// #docregion import-httpclientmodule
HttpClientModule,
// #enddocregion import-httpclientmodule
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
@ -38,7 +42,9 @@ import { MessagesComponent } from './messages/messages.component';
InMemoryDataService, { dataEncapsulation: false }
)
// #enddocregion in-mem-web-api-imports
// #docregion import-httpclientmodule
],
// #enddocregion import-httpclientmodule
declarations: [
AppComponent,
DashboardComponent,
@ -50,6 +56,9 @@ import { MessagesComponent } from './messages/messages.component';
// #docregion v1
],
bootstrap: [ AppComponent ]
// #docregion import-httpclientmodule
})
// #enddocregion import-httpclientmodule
export class AppModule { }
// #enddocregion , v1

View File

@ -13,11 +13,6 @@ import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from './hero';
import { MessageService } from './message.service';
// #docregion http-options
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
// #enddocregion http-options
@Injectable({ providedIn: 'root' })
export class HeroService {
@ -26,6 +21,12 @@ export class HeroService {
private heroesUrl = 'api/heroes'; // URL to web api
// #enddocregion heroesUrl
// #docregion http-options
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
// #enddocregion http-options
// #docregion ctor
constructor(
private http: HttpClient,
@ -96,7 +97,7 @@ export class HeroService {
// #docregion addHero
/** POST: add a new hero to the server */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
@ -109,7 +110,7 @@ export class HeroService {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, httpOptions).pipe(
return this.http.delete<Hero>(url, this.httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
@ -119,7 +120,7 @@ export class HeroService {
// #docregion updateHero
/** PUT: update the hero on the server */
updateHero (hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);

View File

@ -13,13 +13,11 @@ Using the Angular CLI, generate a new component named `heroes`.
</code-example>
The CLI creates a new folder, `src/app/heroes/`, and generates
the four files of the `HeroesComponent`.
the three files of the `HeroesComponent` along with a test file.
The `HeroesComponent` class file is as follows:
<code-example
path="toh-pt1/src/app/heroes/heroes.component.ts" region="v1"
header="app/heroes/heroes.component.ts (initial version)" linenums="false">
<code-example path="toh-pt1/src/app/heroes/heroes.component.ts" region="v1" header="app/heroes/heroes.component.ts (initial version)" linenums="false">
</code-example>
You always import the `Component` symbol from the Angular core library
@ -38,13 +36,13 @@ The CLI generated three metadata properties:
The [CSS element selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors),
`'app-heroes'`, matches the name of the HTML element that identifies this component within a parent component's template.
The `ngOnInit` is a [lifecycle hook](guide/lifecycle-hooks#oninit).
Angular calls `ngOnInit` shortly after creating a component.
The `ngOnInit()` is a [lifecycle hook](guide/lifecycle-hooks#oninit).
Angular calls `ngOnInit()` shortly after creating a component.
It's a good place to put initialization logic.
Always `export` the component class so you can `import` it elsewhere ... like in the `AppModule`.
### Add a _hero_ property
### Add a `hero` property
Add a `hero` property to the `HeroesComponent` for a hero named "Windstorm."
@ -54,17 +52,17 @@ Add a `hero` property to the `HeroesComponent` for a hero named "Windstorm."
### Show the hero
Open the `heroes.component.html` template file.
Delete the default text generated by the Angular CLI and
replace it with a data binding to the new `hero` property.
Delete the default text generated by the Angular CLI and
replace it with a data binding to the new `hero` property.
<code-example path="toh-pt1/src/app/heroes/heroes.component.1.html" header="heroes.component.html" region="show-hero-1" linenums="false">
</code-example>
## Show the _HeroesComponent_ view
## Show the `HeroesComponent` view
To display the `HeroesComponent`, you must add it to the template of the shell `AppComponent`.
Remember that `app-heroes` is the [element selector](#selector) for the `HeroesComponent`.
Remember that `app-heroes` is the [element selector](#selector) for the `HeroesComponent`.
So add an `<app-heroes>` element to the `AppComponent` template file, just below the title.
<code-example path="toh-pt1/src/app/app.component.html" header="src/app/app.component.html" linenums="false">
@ -102,10 +100,7 @@ The page no longer displays properly because you changed the hero from a string
Update the binding in the template to announce the hero's name
and show both `id` and `name` in a details layout like this:
<code-example
path="toh-pt1/src/app/heroes/heroes.component.1.html"
region="show-hero-2"
header="heroes.component.html (HeroesComponent's template)" linenums="false">
<code-example path="toh-pt1/src/app/heroes/heroes.component.1.html" region="show-hero-2" header="heroes.component.html (HeroesComponent's template)" linenums="false">
</code-example>
The browser refreshes and displays the hero's information.
@ -113,14 +108,12 @@ The browser refreshes and displays the hero's information.
## Format with the _UppercasePipe_
Modify the `hero.name` binding like this.
<code-example
path="toh-pt1/src/app/heroes/heroes.component.html"
region="pipe">
<code-example path="toh-pt1/src/app/heroes/heroes.component.html" header="src/app/heroes/heroes.component.html" region="pipe">
</code-example>
The browser refreshes and now the hero's name is displayed in capital letters.
The word `uppercase` in the interpolation binding,
The word `uppercase` in the interpolation binding,
right after the pipe operator ( | ),
activates the built-in `UppercasePipe`.
@ -133,7 +126,7 @@ Users should be able to edit the hero name in an `<input>` textbox.
The textbox should both _display_ the hero's `name` property
and _update_ that property as the user types.
That means data flow from the component class _out to the screen_ and
That means data flows from the component class _out to the screen_ and
from the screen _back to the class_.
To automate that data flow, setup a two-way data binding between the `<input>` form element and the `hero.name` property.
@ -146,7 +139,7 @@ Refactor the details area in the `HeroesComponent` template so it looks like thi
</code-example>
**[(ngModel)]** is Angular's two-way data binding syntax.
**[(ngModel)]** is Angular's two-way data binding syntax.
Here it binds the `hero.name` property to the HTML textbox so that data can flow _in both directions:_ from the `hero.name` property to the textbox, and from the textbox back to the `hero.name`.
@ -162,7 +155,7 @@ Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'.
</code-example>
Although `ngModel` is a valid Angular directive, it isn't available by default.
Although `ngModel` is a valid Angular directive, it isn't available by default.
It belongs to the optional `FormsModule` and you must _opt-in_ to using it.
@ -170,7 +163,7 @@ It belongs to the optional `FormsModule` and you must _opt-in_ to using it.
Angular needs to know how the pieces of your application fit together
and what other files and libraries the app requires.
This information is called _metadata_
This information is called _metadata_.
Some of the metadata is in the `@Component` decorators that you added to your component classes.
Other critical metadata is in [`@NgModule`](guide/ngmodules) decorators.
@ -182,7 +175,7 @@ This is where you _opt-in_ to the `FormsModule`.
### Import _FormsModule_
Open `AppModule` (`app.module.ts`) and import the `FormsModule` symbol from the `@angular/forms` library.
Open `AppModule` (`app.module.ts`) and import the `FormsModule` symbol from the `@angular/forms` library.
<code-example path="toh-pt1/src/app/app.module.ts" header="app.module.ts (FormsModule symbol import)"
region="formsmodule-js-import">
@ -190,13 +183,13 @@ Open `AppModule` (`app.module.ts`) and import the `FormsModule` symbol from the
Then add `FormsModule` to the `@NgModule` metadata's `imports` array, which contains a list of external modules that the app needs.
<code-example path="toh-pt1/src/app/app.module.ts" header="app.module.ts ( @NgModule imports)"
<code-example path="toh-pt1/src/app/app.module.ts" header="app.module.ts (@NgModule imports)"
region="ng-imports">
</code-example>
When the browser refreshes, the app should work again. You can edit the hero's name and see the changes reflected immediately in the `<h2>` above the textbox.
### Declare _HeroesComponent_
### Declare `HeroesComponent`
Every component must be declared in _exactly one_ [NgModule](guide/ngmodules).
@ -206,11 +199,11 @@ So why did the application work?
It worked because the Angular CLI declared `HeroesComponent` in the `AppModule` when it generated that component.
Open `src/app/app.module.ts` and find `HeroesComponent` imported near the top.
<code-example path="toh-pt1/src/app/app.module.ts" region="heroes-import" >
<code-example path="toh-pt1/src/app/app.module.ts" header="src/app/app.module.ts" region="heroes-import" >
</code-example>
The `HeroesComponent` is declared in the `@NgModule.declarations` array.
<code-example path="toh-pt1/src/app/app.module.ts" region="declarations">
<code-example path="toh-pt1/src/app/app.module.ts" header="src/app/app.module.ts" region="declarations">
</code-example>
Note that `AppModule` declares both application components, `AppComponent` and `HeroesComponent`.
@ -228,7 +221,7 @@ Your app should look like this <live-example></live-example>. Here are the code
<code-pane header="src/app/heroes/heroes.component.html" path="toh-pt1/src/app/heroes/heroes.component.html">
</code-pane>
<code-pane header="src/app/app.module.ts"
<code-pane header="src/app/app.module.ts"
path="toh-pt1/src/app/app.module.ts">
</code-pane>
@ -238,7 +231,7 @@ Your app should look like this <live-example></live-example>. Here are the code
<code-pane header="src/app/app.component.html" path="toh-pt1/src/app/app.component.html">
</code-pane>
<code-pane header="src/app/hero.ts"
<code-pane header="src/app/hero.ts"
path="toh-pt1/src/app/hero.ts">
</code-pane>
@ -247,10 +240,10 @@ Your app should look like this <live-example></live-example>. Here are the code
## Summary
* You used the CLI to create a second `HeroesComponent`.
* You displayed the `HeroesComponent` by adding it to the `AppComponent` shell.
* You displayed the `HeroesComponent` by adding it to the `AppComponent` shell.
* You applied the `UppercasePipe` to format the name.
* You used two-way data binding with the `ngModel` directive.
* You learned about the `AppModule`.
* You imported the `FormsModule` in the `AppModule` so that Angular would recognize and apply the `ngModel` directive.
* You imported the `FormsModule` in the `AppModule` so that Angular would recognize and apply the `ngModel` directive.
* You learned the importance of declaring components in the `AppModule`
and appreciated that the CLI declared it for you.

View File

@ -21,19 +21,17 @@ header="src/app/mock-heroes.ts">
## Displaying heroes
You're about to display the list of heroes at the top of the `HeroesComponent`.
Open the `HeroesComponent` class file and import the mock `HEROES`.
<code-example path="toh-pt2/src/app/heroes/heroes.component.ts" region="import-heroes" header="src/app/heroes/heroes.component.ts (import HEROES)">
</code-example>
In the same file (`HeroesComponent` class), define a component property called `heroes` to expose `HEROES` array for binding.
In the same file (`HeroesComponent` class), define a component property called `heroes` to expose the `HEROES` array for binding.
<code-example path="toh-pt2/src/app/heroes/heroes.component.ts" region="component">
<code-example path="toh-pt2/src/app/heroes/heroes.component.ts" header="src/app/heroes/heroes.component.ts" region="component">
</code-example>
### List heroes with _*ngFor_
### List heroes with `*ngFor`
Open the `HeroesComponent` template file and make the following changes:
@ -47,7 +45,7 @@ Make it look like this:
<code-example path="toh-pt2/src/app/heroes/heroes.component.1.html" region="list" header="heroes.component.html (heroes template)" linenums="false">
</code-example>
Now change the `<li>` to this:
That shows one hero. To list them all, add an `*ngFor` to the `<li>` to iterate through the list of heroes:
<code-example path="toh-pt2/src/app/heroes/heroes.component.1.html" region="li">
</code-example>
@ -55,10 +53,10 @@ Now change the `<li>` to this:
The [`*ngFor`](guide/template-syntax#ngFor) is Angular's _repeater_ directive.
It repeats the host element for each element in a list.
In this example
The syntax in this example is as follows:
* `<li>` is the host element
* `heroes` is the list from the `HeroesComponent` class.
* `<li>` is the host element.
* `heroes` holds the mock heroes list from the `HeroesComponent` class, the mock heroes list.
* `hero` holds the current hero object for each iteration through the list.
<div class="alert is-important">
@ -127,9 +125,10 @@ This is an example of Angular's [event binding](guide/template-syntax#event-bind
The parentheses around `click` tell Angular to listen for the `<li>` element's `click` event.
When the user clicks in the `<li>`, Angular executes the `onSelect(hero)` expression.
`onSelect()` is a `HeroesComponent` method that you're about to write.
Angular calls it with the `hero` object displayed in the clicked `<li>`,
the same `hero` defined previously in the `*ngFor` expression.
In the next section, define an `onSelect()` method in `HeroesComponent` to
display the hero that was defined in the `*ngFor` expression.
### Add the click event handler
@ -142,10 +141,11 @@ to the component's `selectedHero`.
<code-example path="toh-pt2/src/app/heroes/heroes.component.ts" region="on-select" header="src/app/heroes/heroes.component.ts (onSelect)" linenums="false">
</code-example>
### Update the details template
### Add a details section
The template still refers to the component's old `hero` property which no longer exists.
Rename `hero` to `selectedHero`.
Currently, you have a list in the component template. To click on a hero on the list
and reveal details about that hero, you need a section for the details to render in the
template. Add the following to `heroes.component.html` beneath the list section:
<code-example path="toh-pt2/src/app/heroes/heroes.component.html" region="selectedHero-details" header="heroes.component.html (selected hero details)" linenums="false">
</code-example>
@ -162,7 +162,7 @@ Open the browser developer tools and look in the console for an error message li
When the app starts, the `selectedHero` is `undefined` _by design_.
Binding expressions in the template that refer to properties of `selectedHero` &mdash; expressions like `{{selectedHero.name}}` &mdash; _must fail_ because there is no selected hero.
Binding expressions in the template that refer to properties of `selectedHero`&mdash;expressions like `{{selectedHero.name}}`&mdash;_must fail_ because there is no selected hero.
#### The fix - hide empty details with _*ngIf_
@ -192,7 +192,7 @@ The heroes appear in a list and details about the clicked hero appear at the bot
#### Why it works
When `selectedHero` is undefined, the `ngIf` removes the hero detail from the DOM. There are no `selectedHero` bindings to worry about.
When `selectedHero` is undefined, the `ngIf` removes the hero detail from the DOM. There are no `selectedHero` bindings to consider.
When the user picks a hero, `selectedHero` has a value and
`ngIf` puts the hero detail into the DOM.
@ -240,7 +240,7 @@ Here are the code files discussed on this page, including the `HeroesComponent`
<code-pane header="src/app/mock-heroes.ts" path="toh-pt2/src/app/mock-heroes.ts">
</code-pane>
<code-pane header="src/app/heroes/heroes.component.ts" path="toh-pt2/src/app/heroes/heroes.component.ts">
</code-pane>

View File

@ -72,7 +72,7 @@ Amend the `@angular/core` import statement to include the `Input` symbol.
Add a `hero` property, preceded by the `@Input()` decorator.
<code-example path="toh-pt3/src/app/hero-detail/hero-detail.component.ts" region="input-hero" linenums="false">
<code-example path="toh-pt3/src/app/hero-detail/hero-detail.component.ts" header="src/app/hero-detail/hero-detail.component.ts" region="input-hero" linenums="false">
</code-example>
That's the only change you should make to the `HeroDetailComponent` class.

View File

@ -11,18 +11,18 @@ Components shouldn't fetch or save data directly and they certainly shouldn't kn
They should focus on presenting data and delegate data access to a service.
In this tutorial, you'll create a `HeroService` that all application classes can use to get heroes.
Instead of creating that service with `new`,
you'll rely on Angular [*dependency injection*](guide/dependency-injection)
Instead of creating that service with `new`,
you'll rely on Angular [*dependency injection*](guide/dependency-injection)
to inject it into the `HeroesComponent` constructor.
Services are a great way to share information among classes that _don't know each other_.
You'll create a `MessageService` and inject it in two places:
1. in `HeroService` which uses the service to send a message.
2. in `MessagesComponent` which displays that message.
1. in `HeroService` which uses the service to send a message
2. in `MessagesComponent` which displays that message
## Create the _HeroService_
## Create the `HeroService`
Using the Angular CLI, create a service called `hero`.
@ -30,24 +30,24 @@ Using the Angular CLI, create a service called `hero`.
ng generate service hero
</code-example>
The command generates skeleton `HeroService` class in `src/app/hero.service.ts`
The `HeroService` class should look like the following example.
The command generates a skeleton `HeroService` class in `src/app/hero.service.ts` as follows:
<code-example path="toh-pt4/src/app/hero.service.1.ts" region="new"
header="src/app/hero.service.ts (new service)" linenums="false">
</code-example>
### _@Injectable()_ services
### `@Injectable()` services
Notice that the new service imports the Angular `Injectable` symbol and annotates
the class with the `@Injectable()` decorator. This marks the class as one that participates in the _dependency injection system_. The `HeroService` class is going to provide an injectable service, and it can also have its own injected dependencies.
It doesn't have any dependencies yet, but [it will soon](#inject-message-service).
The `@Injectable()` decorator accepts a metadata object for the service, the same way the `@Component()` decorator did for your component classes.
The `@Injectable()` decorator accepts a metadata object for the service, the same way the `@Component()` decorator did for your component classes.
### Get hero data
The `HeroService` could get hero data from anywhere&mdash;a web service, local storage, or a mock data source.
The `HeroService` could get hero data from anywhere&mdash;a web service, local storage, or a mock data source.
Removing data access from components means you can change your mind about the implementation anytime, without touching any components.
They don't know how the service works.
@ -56,27 +56,25 @@ The implementation in _this_ tutorial will continue to deliver _mock heroes_.
Import the `Hero` and `HEROES`.
<code-example path="toh-pt4/src/app/hero.service.ts" region="import-heroes">
<code-example path="toh-pt4/src/app/hero.service.ts" header="src/app/hero.service.ts" region="import-heroes">
</code-example>
Add a `getHeroes` method to return the _mock heroes_.
<code-example path="toh-pt4/src/app/hero.service.1.ts" region="getHeroes">
<code-example path="toh-pt4/src/app/hero.service.1.ts" header="src/app/hero.service.ts" region="getHeroes">
</code-example>
{@a provide}
## Provide the `HeroService`
You must make the `HeroService` available to the dependency injection system
before Angular can _inject_ it into the `HeroesComponent`,
as you will do [below](#inject). You do this by registering a _provider_. A provider is something that can create or deliver a service; in this case, it instantiates the `HeroService` class to provide the service.
You must make the `HeroService` available to the dependency injection system
before Angular can _inject_ it into the `HeroesComponent` by registering a _provider_. A provider is something that can create or deliver a service; in this case, it instantiates the `HeroService` class to provide the service.
Now, you need to make sure that the `HeroService` is registered as the provider of this service.
You are registering it with an _injector_, which is the object that is responsible for choosing and injecting the provider where it is required.
To make sure that the `HeroService` can provide this service, register it
with the _injector_, which is the object that is responsible for choosing
and injecting the provider where the app requires it.
By default, the Angular CLI command `ng generate service` registers a provider with the _root injector_ for your service by including provider metadata in the `@Injectable` decorator.
If you look at the `@Injectable()` statement right before the `HeroService` class definition, you can see that the `providedIn` metadata value is 'root':
By default, the Angular CLI command `ng generate service` registers a provider with the _root injector_ for your service by including provider metadata, that is `providedIn: 'root'` in the `@Injectable()` decorator.
```
@Injectable({
@ -84,8 +82,8 @@ If you look at the `@Injectable()` statement right before the `HeroService` clas
})
```
When you provide the service at the root level, Angular creates a single, shared instance of `HeroService` and injects into any class that asks for it.
Registering the provider in the `@Injectable` metadata also allows Angular to optimize an app by removing the service if it turns out not to be used after all.
When you provide the service at the root level, Angular creates a single, shared instance of `HeroService` and injects into any class that asks for it.
Registering the provider in the `@Injectable` metadata also allows Angular to optimize an app by removing the service if it turns out not to be used after all.
<div class="alert is-helpful">
@ -115,7 +113,7 @@ Import the `HeroService` instead.
Replace the definition of the `heroes` property with a simple declaration.
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" region="heroes">
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" header="src/app/heroes/heroes.component.ts" region="heroes">
</code-example>
{@a inject}
@ -124,24 +122,24 @@ Replace the definition of the `heroes` property with a simple declaration.
Add a private `heroService` parameter of type `HeroService` to the constructor.
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" region="ctor">
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" header="src/app/heroes/heroes.component.ts" region="ctor">
</code-example>
The parameter simultaneously defines a private `heroService` property and identifies it as a `HeroService` injection site.
When Angular creates a `HeroesComponent`, the [Dependency Injection](guide/dependency-injection) system
sets the `heroService` parameter to the singleton instance of `HeroService`.
sets the `heroService` parameter to the singleton instance of `HeroService`.
### Add _getHeroes()_
### Add `getHeroes()`
Create a function to retrieve the heroes from the service.
<code-example path="toh-pt4/src/app/heroes/heroes.component.1.ts" region="getHeroes">
<code-example path="toh-pt4/src/app/heroes/heroes.component.1.ts" header="src/app/heroes/heroes.component.ts" region="getHeroes">
</code-example>
{@a oninit}
### Call it in `ngOnInit`
### Call it in `ngOnInit()`
While you could call `getHeroes()` in the constructor, that's not the best practice.
@ -150,29 +148,29 @@ The constructor shouldn't _do anything_.
It certainly shouldn't call a function that makes HTTP requests to a remote server as a _real_ data service would.
Instead, call `getHeroes()` inside the [*ngOnInit lifecycle hook*](guide/lifecycle-hooks) and
let Angular call `ngOnInit` at an appropriate time _after_ constructing a `HeroesComponent` instance.
let Angular call `ngOnInit()` at an appropriate time _after_ constructing a `HeroesComponent` instance.
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" region="ng-on-init">
<code-example path="toh-pt4/src/app/heroes/heroes.component.ts" header="src/app/heroes/heroes.component.ts" region="ng-on-init">
</code-example>
### See it run
After the browser refreshes, the app should run as before,
After the browser refreshes, the app should run as before,
showing a list of heroes and a hero detail view when you click on a hero name.
## Observable data
The `HeroService.getHeroes()` method has a _synchronous signature_,
which implies that the `HeroService` can fetch heroes synchronously.
The `HeroesComponent` consumes the `getHeroes()` result
The `HeroesComponent` consumes the `getHeroes()` result
as if heroes could be fetched synchronously.
<code-example path="toh-pt4/src/app/heroes/heroes.component.1.ts" region="get-heroes">
<code-example path="toh-pt4/src/app/heroes/heroes.component.1.ts" header="src/app/heroes/heroes.component.ts" region="get-heroes">
</code-example>
This will not work in a real app.
You're getting away with it now because the service currently returns _mock heroes_.
But soon the app will fetch heroes from a remote server,
But soon the app will fetch heroes from a remote server,
which is an inherently _asynchronous_ operation.
The `HeroService` must wait for the server to respond,
@ -181,13 +179,11 @@ and the browser will not block while the service waits.
`HeroService.getHeroes()` must have an _asynchronous signature_ of some kind.
It can take a callback. It could return a `Promise`. It could return an `Observable`.
In this tutorial, `HeroService.getHeroes()` will return an `Observable`
in part because it will eventually use the Angular `HttpClient.get` method to fetch the heroes
because it will eventually use the Angular `HttpClient.get` method to fetch the heroes
and [`HttpClient.get()` returns an `Observable`](guide/http).
### Observable _HeroService_
### Observable `HeroService`
`Observable` is one of the key classes in the [RxJS library](http://reactivex.io/rxjs/).
@ -196,13 +192,12 @@ In this tutorial, you'll simulate getting data from the server with the RxJS `of
Open the `HeroService` file and import the `Observable` and `of` symbols from RxJS.
<code-example path="toh-pt4/src/app/hero.service.ts"
header="src/app/hero.service.ts (Observable imports)" region="import-observable">
<code-example path="toh-pt4/src/app/hero.service.ts" header="src/app/hero.service.ts (Observable imports)" region="import-observable">
</code-example>
Replace the `getHeroes` method with this one.
Replace the `getHeroes()` method with the following:
<code-example path="toh-pt4/src/app/hero.service.ts" region="getHeroes-1"></code-example>
<code-example path="toh-pt4/src/app/hero.service.ts" header="src/app/hero.service.ts" region="getHeroes-1"></code-example>
`of(HEROES)` returns an `Observable<Hero[]>` that emits _a single value_, the array of mock heroes.
@ -212,7 +207,7 @@ In the [HTTP tutorial](tutorial/toh-pt6), you'll call `HttpClient.get<Hero[]>()`
</div>
### Subscribe in _HeroesComponent_
### Subscribe in `HeroesComponent`
The `HeroService.getHeroes` method used to return a `Hero[]`.
Now it returns an `Observable<Hero[]>`.
@ -224,11 +219,11 @@ Find the `getHeroes` method and replace it with the following code
<code-tabs>
<code-pane header="heroes.component.ts (Observable)"
<code-pane header="heroes.component.ts (Observable)"
path="toh-pt4/src/app/heroes/heroes.component.ts" region="getHeroes">
</code-pane>
<code-pane header="heroes.component.ts (Original)"
<code-pane header="heroes.component.ts (Original)"
path="toh-pt4/src/app/heroes/heroes.component.1.ts" region="getHeroes">
</code-pane>
@ -242,9 +237,9 @@ or the browser could freeze the UI while it waited for the server's response.
That _won't work_ when the `HeroService` is actually making requests of a remote server.
The new version waits for the `Observable` to emit the array of heroes&mdash;
which could happen now or several minutes from now.
Then `subscribe` passes the emitted array to the callback,
The new version waits for the `Observable` to emit the array of heroes&mdash;which
could happen now or several minutes from now.
The `subscribe()` method passes the emitted array to the callback,
which sets the component's `heroes` property.
This asynchronous approach _will work_ when
@ -252,14 +247,14 @@ the `HeroService` requests heroes from the server.
## Show messages
In this section you will
This section guides you through the following:
* add a `MessagesComponent` that displays app messages at the bottom of the screen.
* create an injectable, app-wide `MessageService` for sending messages to be displayed
* inject `MessageService` into the `HeroService`
* display a message when `HeroService` fetches heroes successfully.
* adding a `MessagesComponent` that displays app messages at the bottom of the screen
* creating an injectable, app-wide `MessageService` for sending messages to be displayed
* injecting `MessageService` into the `HeroService`
* displaying a message when `HeroService` fetches heroes successfully
### Create _MessagesComponent_
### Create `MessagesComponent`
Use the CLI to create the `MessagesComponent`.
@ -269,18 +264,18 @@ Use the CLI to create the `MessagesComponent`.
The CLI creates the component files in the `src/app/messages` folder and declares the `MessagesComponent` in `AppModule`.
Modify the `AppComponent` template to display the generated `MessagesComponent`
Modify the `AppComponent` template to display the generated `MessagesComponent`.
<code-example
header = "/src/app/app.component.html"
header = "src/app/app.component.html"
path="toh-pt4/src/app/app.component.html">
</code-example>
You should see the default paragraph from `MessagesComponent` at the bottom of the page.
### Create the _MessageService_
### Create the `MessageService`
Use the CLI to create the `MessageService` in `src/app`.
Use the CLI to create the `MessageService` in `src/app`.
<code-example language="sh" class="code-shell">
ng generate service message
@ -288,9 +283,7 @@ Use the CLI to create the `MessageService` in `src/app`.
Open `MessageService` and replace its contents with the following.
<code-example
header = "/src/app/message.service.ts"
path="toh-pt4/src/app/message.service.ts">
<code-example header = "src/app/message.service.ts" path="toh-pt4/src/app/message.service.ts">
</code-example>
The service exposes its cache of `messages` and two methods: one to `add()` a message to the cache and another to `clear()` the cache.
@ -298,19 +291,19 @@ The service exposes its cache of `messages` and two methods: one to `add()` a me
{@a inject-message-service}
### Inject it into the `HeroService`
Re-open the `HeroService` and import the `MessageService`.
In `HeroService`, import the `MessageService`.
<code-example
header = "/src/app/hero.service.ts (import MessageService)"
header = "src/app/hero.service.ts (import MessageService)"
path="toh-pt4/src/app/hero.service.ts" region="import-message-service">
</code-example>
Modify the constructor with a parameter that declares a private `messageService` property.
Angular will inject the singleton `MessageService` into that property
Angular will inject the singleton `MessageService` into that property
when it creates the `HeroService`.
<code-example
path="toh-pt4/src/app/hero.service.ts" region="ctor">
path="toh-pt4/src/app/hero.service.ts" header="src/app/hero.service.ts" region="ctor">
</code-example>
<div class="alert is-helpful">
@ -322,32 +315,29 @@ you inject the `MessageService` into the `HeroService` which is injected into th
### Send a message from `HeroService`
Modify the `getHeroes` method to send a message when the heroes are fetched.
Modify the `getHeroes()` method to send a message when the heroes are fetched.
<code-example path="toh-pt4/src/app/hero.service.ts" region="getHeroes">
<code-example path="toh-pt4/src/app/hero.service.ts" header="src/app/hero.service.ts" region="getHeroes">
</code-example>
### Display the message from `HeroService`
The `MessagesComponent` should display all messages,
The `MessagesComponent` should display all messages,
including the message sent by the `HeroService` when it fetches heroes.
Open `MessagesComponent` and import the `MessageService`.
<code-example
header = "/src/app/messages/messages.component.ts (import MessageService)"
path="toh-pt4/src/app/messages/messages.component.ts" region="import-message-service">
<code-example header="src/app/messages/messages.component.ts (import MessageService)" path="toh-pt4/src/app/messages/messages.component.ts" region="import-message-service">
</code-example>
Modify the constructor with a parameter that declares a **public** `messageService` property.
Angular will inject the singleton `MessageService` into that property
Angular will inject the singleton `MessageService` into that property
when it creates the `MessagesComponent`.
<code-example
path="toh-pt4/src/app/messages/messages.component.ts" region="ctor">
<code-example path="toh-pt4/src/app/messages/messages.component.ts" header="src/app/messages/messages.component.ts" region="ctor">
</code-example>
The `messageService` property **must be public** because you're about to bind to it in the template.
The `messageService` property **must be public** because you're going to bind to it in the template.
<div class="alert is-important">
@ -355,7 +345,7 @@ Angular only binds to _public_ component properties.
</div>
### Bind to the _MessageService_
### Bind to the `MessageService`
Replace the CLI-generated `MessagesComponent` template with the following.
@ -390,11 +380,11 @@ Here are the code files discussed on this page and your app should look like thi
<code-tabs>
<code-pane header="src/app/hero.service.ts"
<code-pane header="src/app/hero.service.ts"
path="toh-pt4/src/app/hero.service.ts">
</code-pane>
<code-pane header="src/app/message.service.ts"
<code-pane header="src/app/message.service.ts"
path="toh-pt4/src/app/message.service.ts">
</code-pane>

View File

@ -36,79 +36,74 @@ Use the CLI to generate it.
The generated file looks like this:
<code-example path="toh-pt5/src/app/app-routing.module.0.ts"
header="src/app/app-routing.module.ts (generated)">
<code-example path="toh-pt5/src/app/app-routing.module.0.ts" header="src/app/app-routing.module.ts (generated)">
</code-example>
You generally don't declare components in a routing module so you can delete the
`@NgModule.declarations` array and delete `CommonModule` references too.
Replace it with the following:
You'll configure the router with `Routes` in the `RouterModule`
so import those two symbols from the `@angular/router` library.
Add an `@NgModule.exports` array with `RouterModule` in it.
Exporting `RouterModule` makes router directives available for use
in the `AppModule` components that will need them.
`AppRoutingModule` looks like this now:
<code-example path="toh-pt5/src/app/app-routing.module.ts"
region="v1"
header="src/app/app-routing.module.ts (v1)">
<code-example path="toh-pt5/src/app/app-routing.module.1.ts" header="src/app/app-routing.module.ts (updated)">
</code-example>
### Add routes
First, `AppRoutingModule` imports `RouterModule` and `Routes` so the app can have routing functionality. The next import, `HeroesComponent`, will give the Router somewhere to go once you configure the routes.
*Routes* tell the router which view to display when a user clicks a link or
Notice that the `CommonModule` references and `declarations` array are unecessary, so are no
longer part of `AppRoutingModule`. The following sections explain the rest of the `AppRoutingModule` in more detail.
### Routes
The next part of the file is where you configure your routes.
*Routes* tell the Router which view to display when a user clicks a link or
pastes a URL into the browser address bar.
A typical Angular `Route` has two properties:
Since `AppRoutingModule` already imports `HeroesComponent`, you can use it in the `routes` array:
1. `path`: a string that matches the URL in the browser address bar.
1. `component`: the component that the router should create when navigating to this route.
You intend to navigate to the `HeroesComponent` when the URL is something like `localhost:4200/heroes`.
Import the `HeroesComponent` so you can reference it in a `Route`.
Then define an array of routes with a single `route` to that component.
<code-example path="toh-pt5/src/app/app-routing.module.ts"
<code-example path="toh-pt5/src/app/app-routing.module.ts" header="src/app/app-routing.module.ts"
region="heroes-route">
</code-example>
Once you've finished setting up, the router will match that URL to `path: 'heroes'`
and display the `HeroesComponent`.
A typical Angular `Route` has two properties:
### _RouterModule.forRoot()_
* `path`: a string that matches the URL in the browser address bar.
* `component`: the component that the router should create when navigating to this route.
You first must initialize the router and start it listening for browser location changes.
This tells the router to match that URL to `path: 'heroes'`
and display the `HeroesComponent` when the URL is something like `localhost:4200/heroes`.
Add `RouterModule` to the `@NgModule.imports` array and
configure it with the `routes` in one step by calling
`RouterModule.forRoot()` _within_ the `imports` array, like this:
### `RouterModule.forRoot()`
<code-example path="toh-pt5/src/app/app-routing.module.ts"
region="ngmodule-imports">
The `@NgModule` metadata initializes the router and starts it listening for browser location changes.
The following line adds the `RouterModule` to the `AppRoutingModule` `imports` array and
configures it with the `routes` in one step by calling
`RouterModule.forRoot()`:
<code-example path="toh-pt5/src/app/app-routing.module.ts" header="src/app/app-routing.module.ts" region="ngmodule-imports">
</code-example>
<div class="alert is-helpful">
The method is called `forRoot()` because you configure the router at the application's root level.
The `forRoot()` method supplies the service providers and directives needed for routing,
The `forRoot()` method supplies the service providers and directives needed for routing,
and performs the initial navigation based on the current browser URL.
</div>
## Add _RouterOutlet_
Next, `AppRoutingModule` exports `RouterModule` so it will be available throughout the app.
Open the `AppComponent` template and replace the `<app-heroes>` element with a `<router-outlet>` element.
<code-example path="toh-pt5/src/app/app.component.html"
region="outlet"
header="src/app/app.component.html (router-outlet)">
<code-example path="toh-pt5/src/app/app-routing.module.ts" header="src/app/app-routing.module.ts (exports array)" region="export-routermodule">
</code-example>
You removed `<app-heroes>` because you will only display the `HeroesComponent` when the user navigates to it.
## Add `RouterOutlet`
Open the `AppComponent` template and replace the `<app-heroes>` element with a `<router-outlet>` element.
<code-example path="toh-pt5/src/app/app.component.html" region="outlet" header="src/app/app.component.html (router-outlet)">
</code-example>
The `AppComponent` template no longer needs `<app-heroes>` because the app will only display the `HeroesComponent` when the user navigates to it.
The `<router-outlet>` tells the router where to display routed views.
@ -129,7 +124,7 @@ You should still be running with this CLI command.
The browser should refresh and display the app title but not the list of heroes.
Look at the browser's address bar.
Look at the browser's address bar.
The URL ends in `/`.
The route path to `HeroesComponent` is `/heroes`.
@ -140,29 +135,26 @@ You should see the familiar heroes master/detail view.
## Add a navigation link (`routerLink`)
Users shouldn't have to paste a route URL into the address bar.
They should be able to click a link to navigate.
Ideally, users should be able to click a link to navigate rather
than pasting a route URL into the address bar.
Add a `<nav>` element and, within that, an anchor element that, when clicked,
Add a `<nav>` element and, within that, an anchor element that, when clicked,
triggers navigation to the `HeroesComponent`.
The revised `AppComponent` template looks like this:
<code-example
path="toh-pt5/src/app/app.component.html"
region="heroes"
header="src/app/app.component.html (heroes RouterLink)">
<code-example path="toh-pt5/src/app/app.component.html" region="heroes" header="src/app/app.component.html (heroes RouterLink)">
</code-example>
A [`routerLink` attribute](#routerlink) is set to `"/heroes"`,
the string that the router matches to the route to `HeroesComponent`.
The `routerLink` is the selector for the [`RouterLink` directive](#routerlink)
The `routerLink` is the selector for the [`RouterLink` directive](/api/router/RouterLink)
that turns user clicks into router navigations.
It's another of the public directives in the `RouterModule`.
The browser refreshes and displays the app title and heroes link,
The browser refreshes and displays the app title and heroes link,
but not the heroes list.
Click the link.
Click the link.
The address bar updates to `/heroes` and the list of heroes appears.
<div class="alert is-helpful">
@ -176,7 +168,7 @@ as listed in the [final code review](#appcomponent) below.
## Add a dashboard view
Routing makes more sense when there are multiple views.
So far there's only the heroes view.
So far there's only the heroes view.
Add a `DashboardComponent` using the CLI:
@ -186,18 +178,18 @@ Add a `DashboardComponent` using the CLI:
The CLI generates the files for the `DashboardComponent` and declares it in `AppModule`.
Replace the default file content in these three files as follows and then return for a little discussion:
Replace the default file content in these three files as follows:
<code-tabs>
<code-pane
<code-pane
header="src/app/dashboard/dashboard.component.html" path="toh-pt5/src/app/dashboard/dashboard.component.1.html">
</code-pane>
<code-pane
<code-pane
header="src/app/dashboard/dashboard.component.ts" path="toh-pt5/src/app/dashboard/dashboard.component.ts">
</code-pane>
<code-pane
<code-pane
header="src/app/dashboard/dashboard.component.css" path="toh-pt5/src/app/dashboard/dashboard.component.css">
</code-pane>
</code-tabs>
@ -211,11 +203,11 @@ The _template_ presents a grid of hero name links.
The _class_ is similar to the `HeroesComponent` class.
* It defines a `heroes` array property.
* The constructor expects Angular to inject the `HeroService` into a private `heroService` property.
* The `ngOnInit()` lifecycle hook calls `getHeroes`.
* The `ngOnInit()` lifecycle hook calls `getHeroes()`.
This `getHeroes` returns the sliced list of heroes at positions 1 and 5, returning only four of the Top Heroes (2nd, 3rd, 4th, and 5th).
This `getHeroes()` returns the sliced list of heroes at positions 1 and 5, returning only four of the Top Heroes (2nd, 3rd, 4th, and 5th).
<code-example path="toh-pt5/src/app/dashboard/dashboard.component.ts" region="getHeroes">
<code-example path="toh-pt5/src/app/dashboard/dashboard.component.ts" header="src/app/dashboard/dashboard.component.ts" region="getHeroes">
</code-example>
### Add the dashboard route
@ -224,29 +216,24 @@ To navigate to the dashboard, the router needs an appropriate route.
Import the `DashboardComponent` in the `AppRoutingModule`.
<code-example
path="toh-pt5/src/app/app-routing.module.ts"
region="import-dashboard"
header="src/app/app-routing.module.ts (import DashboardComponent)">
<code-example path="toh-pt5/src/app/app-routing.module.ts" region="import-dashboard" header="src/app/app-routing.module.ts (import DashboardComponent)">
</code-example>
Add a route to the `AppRoutingModule.routes` array that matches a path to the `DashboardComponent`.
<code-example
path="toh-pt5/src/app/app-routing.module.ts"
region="dashboard-route">
<code-example path="toh-pt5/src/app/app-routing.module.ts" header="src/app/app-routing.module.ts" region="dashboard-route">
</code-example>
### Add a default route
When the app starts, the browsers address bar points to the web site's root.
When the app starts, the browser's address bar points to the web site's root.
That doesn't match any existing route so the router doesn't navigate anywhere.
The space below the `<router-outlet>` is blank.
To make the app navigate to the dashboard automatically, add the following
route to the `AppRoutingModule.Routes` array.
<code-example path="toh-pt5/src/app/app-routing.module.ts" region="redirect-route">
<code-example path="toh-pt5/src/app/app-routing.module.ts" header="src/app/app-routing.module.ts" region="redirect-route">
</code-example>
This route redirects a URL that fully matches the empty path to the route whose path is `'/dashboard'`.
@ -292,36 +279,28 @@ The heroes list view should no longer show hero details as it does now.
Open the `HeroesComponent` template (`heroes/heroes.component.html`) and
delete the `<app-hero-detail>` element from the bottom.
Clicking a hero item now does nothing.
Clicking a hero item now does nothing.
You'll [fix that shortly](#heroes-component-links) after you enable routing to the `HeroDetailComponent`.
### Add a _hero detail_ route
A URL like `~/detail/11` would be a good URL for navigating to the *Hero Detail* view of the hero whose `id` is `11`.
A URL like `~/detail/11` would be a good URL for navigating to the *Hero Detail* view of the hero whose `id` is `11`.
Open `AppRoutingModule` and import `HeroDetailComponent`.
<code-example
path="toh-pt5/src/app/app-routing.module.ts"
region="import-herodetail"
header="src/app/app-routing.module.ts (import HeroDetailComponent)">
<code-example path="toh-pt5/src/app/app-routing.module.ts" region="import-herodetail" header="src/app/app-routing.module.ts (import HeroDetailComponent)">
</code-example>
Then add a _parameterized_ route to the `AppRoutingModule.routes` array that matches the path pattern to the _hero detail_ view.
<code-example
path="toh-pt5/src/app/app-routing.module.ts"
region="detail-route">
<code-example path="toh-pt5/src/app/app-routing.module.ts" header="src/app/app-routing.module.ts" region="detail-route">
</code-example>
The colon (:) in the `path` indicates that `:id` is a placeholder for a specific hero `id`.
At this point, all application routes are in place.
<code-example
path="toh-pt5/src/app/app-routing.module.ts"
region="routes"
header="src/app/app-routing.module.ts (all routes)">
<code-example path="toh-pt5/src/app/app-routing.module.ts" region="routes" header="src/app/app-routing.module.ts (all routes)">
</code-example>
### `DashboardComponent` hero links
@ -331,14 +310,14 @@ The `DashboardComponent` hero links do nothing at the moment.
Now that the router has a route to `HeroDetailComponent`,
fix the dashboard hero links to navigate via the _parameterized_ dashboard route.
<code-example
path="toh-pt5/src/app/dashboard/dashboard.component.html"
region="click"
<code-example
path="toh-pt5/src/app/dashboard/dashboard.component.html"
region="click"
header="src/app/dashboard/dashboard.component.html (hero links)">
</code-example>
You're using Angular [interpolation binding](guide/template-syntax#interpolation) within the `*ngFor` repeater
to insert the current iteration's `hero.id` into each
You're using Angular [interpolation binding](guide/template-syntax#interpolation) within the `*ngFor` repeater
to insert the current iteration's `hero.id` into each
[`routerLink`](#routerlink).
{@a heroes-component-links}
@ -347,21 +326,15 @@ to insert the current iteration's `hero.id` into each
The hero items in the `HeroesComponent` are `<li>` elements whose click events
are bound to the component's `onSelect()` method.
<code-example
path="toh-pt4/src/app/heroes/heroes.component.html"
region="list"
header="src/app/heroes/heroes.component.html (list with onSelect)">
<code-example path="toh-pt4/src/app/heroes/heroes.component.html" region="list" header="src/app/heroes/heroes.component.html (list with onSelect)">
</code-example>
Strip the `<li>` back to just its `*ngFor`,
wrap the badge and name in an anchor element (`<a>`),
and add a `routerLink` attribute to the anchor that
and add a `routerLink` attribute to the anchor that
is the same as in the dashboard template
<code-example
path="toh-pt5/src/app/heroes/heroes.component.html"
region="list"
header="src/app/heroes/heroes.component.html (list with links)">
<code-example path="toh-pt5/src/app/heroes/heroes.component.html" region="list" header="src/app/heroes/heroes.component.html (list with links)">
</code-example>
You'll have to fix the private stylesheet (`heroes.component.css`) to make
@ -370,19 +343,16 @@ Revised styles are in the [final code review](#heroescomponent) at the bottom of
#### Remove dead code (optional)
While the `HeroesComponent` class still works,
While the `HeroesComponent` class still works,
the `onSelect()` method and `selectedHero` property are no longer used.
It's nice to tidy up and you'll be grateful to yourself later.
Here's the class after pruning away the dead code.
<code-example
path="toh-pt5/src/app/heroes/heroes.component.ts"
region="class"
header="src/app/heroes/heroes.component.ts (cleaned up)" linenums="false">
<code-example path="toh-pt5/src/app/heroes/heroes.component.ts" region="class" header="src/app/heroes/heroes.component.ts (cleaned up)" linenums="false">
</code-example>
## Routable *HeroDetailComponent*
## Routable `HeroDetailComponent`
Previously, the parent `HeroesComponent` set the `HeroDetailComponent.hero`
property and the `HeroDetailComponent` displayed the hero.
@ -390,18 +360,16 @@ property and the `HeroDetailComponent` displayed the hero.
`HeroesComponent` doesn't do that anymore.
Now the router creates the `HeroDetailComponent` in response to a URL such as `~/detail/11`.
The `HeroDetailComponent` needs a new way to obtain the _hero-to-display_.
The `HeroDetailComponent` needs a new way to obtain the hero-to-display.
This section explains the following:
* Get the route that created it,
* Get the route that created it
* Extract the `id` from the route
* Acquire the hero with that `id` from the server via the `HeroService`
Add the following imports:
<code-example
path="toh-pt5/src/app/hero-detail/hero-detail.component.ts"
region="added-imports"
header="src/app/hero-detail/hero-detail.component.ts">
<code-example path="toh-pt5/src/app/hero-detail/hero-detail.component.ts" region="added-imports" header="src/app/hero-detail/hero-detail.component.ts">
</code-example>
{@a hero-detail-ctor}
@ -409,27 +377,25 @@ Add the following imports:
Inject the `ActivatedRoute`, `HeroService`, and `Location` services
into the constructor, saving their values in private fields:
<code-example
path="toh-pt5/src/app/hero-detail/hero-detail.component.ts" region="ctor">
<code-example path="toh-pt5/src/app/hero-detail/hero-detail.component.ts" header="toh-pt5/src/app/hero-detail/hero-detail.component.ts" region="ctor">
</code-example>
The [`ActivatedRoute`](api/router/ActivatedRoute) holds information about the route to this instance of the `HeroDetailComponent`.
This component is interested in the route's bag of parameters extracted from the URL.
The _"id"_ parameter is the `id` of the hero to display.
This component is interested in the route's parameters extracted from the URL.
The "id" parameter is the `id` of the hero to display.
The [`HeroService`](tutorial/toh-pt4) gets hero data from the remote server
and this component will use it to get the _hero-to-display_.
and this component will use it to get the hero-to-display.
The [`location`](api/common/Location) is an Angular service for interacting with the browser.
You'll use it [later](#goback) to navigate back to the view that navigated here.
### Extract the _id_ route parameter
### Extract the `id` route parameter
In the `ngOnInit()` [lifecycle hook](guide/lifecycle-hooks#oninit)
call `getHero()` and define it as follows.
<code-example
path="toh-pt5/src/app/hero-detail/hero-detail.component.ts" region="ngOnInit">
<code-example path="toh-pt5/src/app/hero-detail/hero-detail.component.ts" header="src/app/hero-detail/hero-detail.component.ts" region="ngOnInit">
</code-example>
The `route.snapshot` is a static image of the route information shortly after the component was created.
@ -447,18 +413,14 @@ Add it now.
### Add `HeroService.getHero()`
Open `HeroService` and add this `getHero()` method
Open `HeroService` and add the following `getHero()` method with the `id` after the `getHeroes()` method:
<code-example
path="toh-pt5/src/app/hero.service.ts"
region="getHero"
header="src/app/hero.service.ts (getHero)">
<code-example path="toh-pt5/src/app/hero.service.ts" region="getHero" header="src/app/hero.service.ts (getHero)">
</code-example>
<div class="alert is-important">
Note the backticks ( &#96; ) that
define a JavaScript
Note the backticks ( &#96; ) that define a JavaScript
[_template literal_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) for embedding the `id`.
</div>
@ -481,7 +443,7 @@ the router navigates to the detail view for the hero with `id: 11`, "Dr Nice".
### Find the way back
By clicking the browser's back button,
By clicking the browser's back button,
you can go back to the hero list or dashboard view,
depending upon which sent you to the detail view.
@ -490,13 +452,10 @@ It would be nice to have a button on the `HeroDetail` view that can do that.
Add a *go back* button to the bottom of the component template and bind it
to the component's `goBack()` method.
<code-example
path="toh-pt5/src/app/hero-detail/hero-detail.component.html"
region="back-button"
header="src/app/hero-detail/hero-detail.component.html (back button)">
<code-example path="toh-pt5/src/app/hero-detail/hero-detail.component.html" region="back-button" header="src/app/hero-detail/hero-detail.component.html (back button)">
</code-example>
Add a `goBack()` _method_ to the component class that navigates backward one step
Add a `goBack()` _method_ to the component class that navigates backward one step
in the browser's history stack
using the `Location` service that you [injected previously](#hero-detail-ctor).
@ -509,95 +468,93 @@ Refresh the browser and start clicking.
Users can navigate around the app, from the dashboard to hero details and back,
from heroes list to the mini detail to the hero details and back to the heroes again.
You've met all of the navigational requirements that propelled this page.
## Final code review
Here are the code files discussed on this page and your app should look like this <live-example></live-example>.
{@a approutingmodule}
{@a appmodule}
#### _AppRoutingModule_, _AppModule_, and _HeroService_
#### `AppRoutingModule`, `AppModule`, and `HeroService`
<code-tabs>
<code-pane
header="src/app/app-routing.module.ts"
<code-pane
header="src/app/app-routing.module.ts"
path="toh-pt5/src/app/app-routing.module.ts">
</code-pane>
<code-pane
header="src/app/app.module.ts"
<code-pane
header="src/app/app.module.ts"
path="toh-pt5/src/app/app.module.ts">
</code-pane>
<code-pane
header="src/app/hero.service.ts"
<code-pane
header="src/app/hero.service.ts"
path="toh-pt5/src/app/hero.service.ts">
</code-pane>
</code-tabs>
{@a appcomponent}
#### _AppComponent_
#### `AppComponent`
<code-tabs>
<code-pane
<code-pane
header="src/app/app.component.html"
path="toh-pt5/src/app/app.component.html">
</code-pane>
<code-pane
<code-pane
header="src/app/app.component.css"
path="toh-pt5/src/app/app.component.css">
</code-pane>
</code-tabs>
{@a dashboardcomponent}
#### _DashboardComponent_
#### `DashboardComponent`
<code-tabs>
<code-pane
<code-pane
header="src/app/dashboard/dashboard.component.html" path="toh-pt5/src/app/dashboard/dashboard.component.html">
</code-pane>
<code-pane
<code-pane
header="src/app/dashboard/dashboard.component.ts" path="toh-pt5/src/app/dashboard/dashboard.component.ts">
</code-pane>
<code-pane
<code-pane
header="src/app/dashboard/dashboard.component.css" path="toh-pt5/src/app/dashboard/dashboard.component.css">
</code-pane>
</code-tabs>
{@a heroescomponent}
#### _HeroesComponent_
#### `HeroesComponent`
<code-tabs>
<code-pane
<code-pane
header="src/app/heroes/heroes.component.html" path="toh-pt5/src/app/heroes/heroes.component.html">
</code-pane>
<code-pane
header="src/app/heroes/heroes.component.ts"
<code-pane
header="src/app/heroes/heroes.component.ts"
path="toh-pt5/src/app/heroes/heroes.component.ts">
</code-pane>
<code-pane
header="src/app/heroes/heroes.component.css"
<code-pane
header="src/app/heroes/heroes.component.css"
path="toh-pt5/src/app/heroes/heroes.component.css">
</code-pane>
</code-tabs>
{@a herodetailcomponent}
#### _HeroDetailComponent_
#### `HeroDetailComponent`
<code-tabs>
<code-pane
<code-pane
header="src/app/hero-detail/hero-detail.component.html" path="toh-pt5/src/app/hero-detail/hero-detail.component.html">
</code-pane>
<code-pane
<code-pane
header="src/app/hero-detail/hero-detail.component.ts" path="toh-pt5/src/app/hero-detail/hero-detail.component.ts">
</code-pane>
<code-pane
<code-pane
header="src/app/hero-detail/hero-detail.component.css" path="toh-pt5/src/app/hero-detail/hero-detail.component.css">
</code-pane>
</code-tabs>
@ -606,7 +563,7 @@ Here are the code files discussed on this page and your app should look like thi
* You added the Angular router to navigate among different components.
* You turned the `AppComponent` into a navigation shell with `<a>` links and a `<router-outlet>`.
* You configured the router in an `AppRoutingModule`
* You configured the router in an `AppRoutingModule`
* You defined simple routes, a redirect route, and a parameterized route.
* You used the `routerLink` directive in anchor elements.
* You refactored a tightly-coupled master/detail view into a routed detail view.

View File

@ -11,174 +11,147 @@ When you're done with this page, the app should look like this <live-example></l
## Enable HTTP services
`HttpClient` is Angular's mechanism for communicating with a remote server over HTTP.
`HttpClient` is Angular's mechanism for communicating with a remote server over HTTP.
To make `HttpClient` available everywhere in the app:
Make `HttpClient` available everywhere in the app in two steps. First, add it to the root `AppModule` by importing it:
* open the root `AppModule`
* import the `HttpClientModule` symbol from `@angular/common/http`
<code-example
path="toh-pt6/src/app/app.module.ts"
region="import-http-client"
header="src/app/app.module.ts (Http Client import)">
<code-example path="toh-pt6/src/app/app.module.ts" region="import-http-client" header="src/app/app.module.ts (HttpClientModule import)">
</code-example>
Next, still in the `AppModule`, add `HttpClient` to the `imports` array:
<code-example path="toh-pt6/src/app/app.module.ts" region="import-httpclientmodule" header="src/app/app.module.ts (imports array excerpt)">
</code-example>
* add it to the `@NgModule.imports` array
## Simulate a data server
This tutorial sample _mimics_ communication with a remote data server by using the
[_In-memory Web API_](https://github.com/angular/in-memory-web-api "In-memory Web API") module.
This tutorial sample mimics communication with a remote data server by using the
[In-memory Web API](https://github.com/angular/in-memory-web-api "In-memory Web API") module.
After installing the module, the app will make requests to and receive responses from the `HttpClient`
without knowing that the *In-memory Web API* is intercepting those requests,
applying them to an in-memory data store, and returning simulated responses.
This facility is a great convenience for the tutorial.
You won't have to set up a server to learn about `HttpClient`.
It may also be convenient in the early stages of your own app development when
the server's web api is ill-defined or not yet implemented.
By using the In-memory Web API, you won't have to set up a server to learn about `HttpClient`.
<div class="alert is-important">
**Important:** the *In-memory Web API* module has nothing to do with HTTP in Angular.
**Important:** the In-memory Web API module has nothing to do with HTTP in Angular.
If you're just _reading_ this tutorial to learn about `HttpClient`, you can [skip over](#import-heroes) this step.
If you're _coding along_ with this tutorial, stay here and add the *In-memory Web API* now.
If you're just reading this tutorial to learn about `HttpClient`, you can [skip over](#import-heroes) this step.
If you're coding along with this tutorial, stay here and add the In-memory Web API now.
</div>
Install the *In-memory Web API* package from _npm_
Install the In-memory Web API package from npm with the following command:
<code-example language="sh" class="code-shell">
npm install angular-in-memory-web-api --save
</code-example>
In the `AppModule`, import the `HttpClientInMemoryWebApiModule` and the `InMemoryDataService` class,
which you will create in a moment.
The class `src/app/in-memory-data.service.ts` is generated by the following command:
<code-example language="sh" class="code-shell">
ng generate service InMemoryData
<code-example path="toh-pt6/src/app/app.module.ts" region="import-in-mem-stuff" header="src/app/app.module.ts (In-memory Web API imports)">
</code-example>
This class has the following content:
After the `HttpClientModule`, add the `HttpClientInMemoryWebApiModule`
to the `AppModule` `imports` array and configure it with the `InMemoryDataService`.
<code-example path="toh-pt6/src/app/in-memory-data.service.ts" region="init" header="src/app/in-memory-data.service.ts" linenums="false"></code-example>
This file replaces `mock-heroes.ts`, which is now safe to delete.
When your server is ready, detach the *In-memory Web API*, and the app's requests will go through to the server.
Now back to the `HttpClient` story.
Import the `HttpClientInMemoryWebApiModule` and the `InMemoryDataService` class.
<code-example
path="toh-pt6/src/app/app.module.ts"
region="import-in-mem-stuff"
header="src/app/app.module.ts (In-memory Web API imports)">
</code-example>
Add the `HttpClientInMemoryWebApiModule` to the `@NgModule.imports` array&mdash;
_after importing the `HttpClientModule`_,
&mdash;while configuring it with the `InMemoryDataService`.
<code-example
path="toh-pt6/src/app/app.module.ts"
region="in-mem-web-api-imports">
<code-example path="toh-pt6/src/app/app.module.ts" header="src/app/app.module.ts (imports array excerpt)" region="in-mem-web-api-imports">
</code-example>
The `forRoot()` configuration method takes an `InMemoryDataService` class
that primes the in-memory database.
Generate the class `src/app/in-memory-data.service.ts` with the following command:
<code-example language="sh" class="code-shell">
ng generate service InMemoryData
</code-example>
Replace the default contents of `in-memory-data.service.ts` with the following:
<code-example path="toh-pt6/src/app/in-memory-data.service.ts" region="init" header="src/app/in-memory-data.service.ts" linenums="false"></code-example>
The `in-memory-data.service.ts` file replaces `mock-heroes.ts`, which is now safe to delete.
When the server is ready, you'll detach the In-memory Web API, and the app's requests will go through to the server.
{@a import-heroes}
## Heroes and HTTP
Import some HTTP symbols that you'll need:
In the `HeroService`, import `HttpClient` and `HttpHeaders`:
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="import-httpclient"
header="src/app/hero.service.ts (import HTTP symbols)">
<code-example path="toh-pt6/src/app/hero.service.ts" region="import-httpclient" header="src/app/hero.service.ts (import HTTP symbols)">
</code-example>
Inject `HttpClient` into the constructor in a private property called `http`.
Still in the `HeroService`, inject `HttpClient` into the constructor in a private property called `http`.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="ctor" >
<code-example path="toh-pt6/src/app/hero.service.ts" header="src/app/hero.service.ts" region="ctor" >
</code-example>
Keep injecting the `MessageService`. You'll call it so frequently that
you'll wrap it in a private `log()` method.
Notice that you keep injecting the `MessageService` but since you'll call it so frequently, wrap it in a private `log()` method:
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="log" >
<code-example path="toh-pt6/src/app/hero.service.ts" header="src/app/hero.service.ts" region="log" >
</code-example>
Define the `heroesUrl` of the form `:base/:collectionName` with the address of the heroes resource on the server.
Here `base` is the resource to which requests are made,
and `collectionName` is the heroes data object in the `in-memory-data-service.ts`.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="heroesUrl" >
<code-example path="toh-pt6/src/app/hero.service.ts" header="src/app/hero.service.ts" region="heroesUrl" >
</code-example>
### Get heroes with _HttpClient_
### Get heroes with `HttpClient`
The current `HeroService.getHeroes()`
The current `HeroService.getHeroes()`
uses the RxJS `of()` function to return an array of mock heroes
as an `Observable<Hero[]>`.
<code-example
path="toh-pt4/src/app/hero.service.ts"
region="getHeroes-1"
header="src/app/hero.service.ts (getHeroes with RxJs 'of()')">
<code-example path="toh-pt4/src/app/hero.service.ts" region="getHeroes-1" header="src/app/hero.service.ts (getHeroes with RxJs 'of()')">
</code-example>
Convert that method to use `HttpClient`
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="getHeroes-1">
Convert that method to use `HttpClient` as follows:
<code-example path="toh-pt6/src/app/hero.service.ts" header="src/app/hero.service.ts" region="getHeroes-1">
</code-example>
Refresh the browser. The hero data should successfully load from the
mock server.
You've swapped `of` for `http.get` and the app keeps working without any other changes
You've swapped `of()` for `http.get()` and the app keeps working without any other changes
because both functions return an `Observable<Hero[]>`.
### Http methods return one value
### `HttpClient` methods return one value
All `HttpClient` methods return an RxJS `Observable` of something.
HTTP is a request/response protocol.
HTTP is a request/response protocol.
You make a request, it returns a single response.
In general, an observable _can_ return multiple values over time.
An observable from `HttpClient` always emits a single value and then completes, never to emit again.
This particular `HttpClient.get` call returns an `Observable<Hero[]>`, literally "_an observable of hero arrays_". In practice, it will only return a single hero array.
This particular `HttpClient.get()` call returns an `Observable<Hero[]>`; that is, "_an observable of hero arrays_". In practice, it will only return a single hero array.
### _HttpClient.get_ returns response data
### `HttpClient.get()` returns response data
`HttpClient.get` returns the _body_ of the response as an untyped JSON object by default.
`HttpClient.get()` returns the body of the response as an untyped JSON object by default.
Applying the optional type specifier, `<Hero[]>` , gives you a typed result object.
The shape of the JSON data is determined by the server's data API.
The server's data API determines the shape of the JSON data.
The _Tour of Heroes_ data API returns the hero data as an array.
<div class="alert is-helpful">
Other APIs may bury the data that you want within an object.
You might have to dig that data out by processing the `Observable` result
with the RxJS `map` operator.
with the RxJS `map()` operator.
Although not discussed here, there's an example of `map` in the `getHeroNo404()`
Although not discussed here, there's an example of `map()` in the `getHeroNo404()`
method included in the sample source code.
</div>
@ -192,59 +165,51 @@ To catch errors, you **"pipe" the observable** result from `http.get()` through
Import the `catchError` symbol from `rxjs/operators`, along with some other operators you'll need later.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="import-rxjs-operators">
<code-example path="toh-pt6/src/app/hero.service.ts" header="src/app/hero.service.ts" region="import-rxjs-operators">
</code-example>
Now extend the observable result with the `.pipe()` method and
Now extend the observable result with the `pipe()` method and
give it a `catchError()` operator.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="getHeroes-2" >
<code-example path="toh-pt6/src/app/hero.service.ts" region="getHeroes-2" header="src/app/hero.service.ts">
</code-example>
The `catchError()` operator intercepts an **`Observable` that failed**.
It passes the error an _error handler_ that can do what it wants with the error.
It passes the error an error handler that can do what it wants with the error.
The following `handleError()` method reports the error and then returns an
innocuous result so that the application keeps working.
#### _handleError_
#### `handleError`
The following `handleError()` will be shared by many `HeroService` methods
so it's generalized to meet their different needs.
Instead of handling the error directly, it returns an _error handler_ function to `catchError` that it
Instead of handling the error directly, it returns an error handler function to `catchError` that it
has configured with both the name of the operation that failed and a safe return value.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="handleError">
<code-example path="toh-pt6/src/app/hero.service.ts" header="src/app/hero.service.ts" region="handleError">
</code-example>
After reporting the error to console, the handler constructs
a user friendly message and returns a safe value to the app so it can keep working.
After reporting the error to the console, the handler constructs
a user friendly message and returns a safe value to the app so the app can keep working.
Because each service method returns a different kind of `Observable` result,
`handleError()` takes a type parameter so it can return the safe value as the type that the app expects.
### Tap into the _Observable_
### Tap into the Observable
The `HeroService` methods will **tap** into the flow of observable values
and send a message (via `log()`) to the message area at the bottom of the page.
and send a message, via the `log()` method, to the message area at the bottom of the page.
They'll do that with the RxJS `tap` operator,
which _looks_ at the observable values, does _something_ with those values,
They'll do that with the RxJS `tap()` operator,
which looks at the observable values, does something with those values,
and passes them along.
The `tap` call back doesn't touch the values themselves.
The `tap()` call back doesn't touch the values themselves.
Here is the final version of `getHeroes` with the `tap` that logs the operation.
Here is the final version of `getHeroes()` with the `tap()` that logs the operation.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="getHeroes" >
<code-example path="toh-pt6/src/app/hero.service.ts" header="src/app/hero.service.ts" region="getHeroes" >
</code-example>
### Get hero by id
@ -254,20 +219,20 @@ Most web APIs support a _get by id_ request in the form `:baseURL/:id`.
Here, the _base URL_ is the `heroesURL` defined in the [Heroes and HTTP](tutorial/toh-pt6#heroes-and-http) section (`api/heroes`) and _id_ is
the number of the hero that you want to retrieve. For example, `api/heroes/11`.
Add a `HeroService.getHero()` method to make that request:
Update the `HeroService` `getHero()` method with the following to make that request:
<code-example path="toh-pt6/src/app/hero.service.ts" region="getHero" header="src/app/hero.service.ts"></code-example>
There are three significant differences from `getHeroes()`.
There are three significant differences from `getHeroes()`:
* it constructs a request URL with the desired hero's id.
* the server should respond with a single hero rather than an array of heroes.
* therefore, `getHero` returns an `Observable<Hero>` ("_an observable of Hero objects_")
* `getHero()` constructs a request URL with the desired hero's id.
* The server should respond with a single hero rather than an array of heroes.
* `getHero()` returns an `Observable<Hero>` ("_an observable of Hero objects_")
rather than an observable of hero _arrays_ .
## Update heroes
Edit a hero's name in the _hero detail_ view.
Edit a hero's name in the hero detail view.
As you type, the hero name updates the heading at the top of the page.
But when you click the "go back button", the changes are lost.
@ -279,24 +244,21 @@ binding that invokes a new component method named `save()`.
<code-example path="toh-pt6/src/app/hero-detail/hero-detail.component.html" region="save" header="src/app/hero-detail/hero-detail.component.html (save)"></code-example>
Add the following `save()` method, which persists hero name changes using the hero service
In the `HeroDetail` component class, add the following `save()` method, which persists hero name changes using the hero service
`updateHero()` method and then navigates back to the previous view.
<code-example path="toh-pt6/src/app/hero-detail/hero-detail.component.ts" region="save" header="src/app/hero-detail/hero-detail.component.ts (save)"></code-example>
#### Add _HeroService.updateHero()_
#### Add `HeroService.updateHero()`
The overall structure of the `updateHero()` method is similar to that of
`getHeroes()`, but it uses `http.put()` to persist the changed hero
on the server.
on the server. Add the following to the `HeroService`.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="updateHero"
header="src/app/hero.service.ts (update)">
<code-example path="toh-pt6/src/app/hero.service.ts" region="updateHero" header="src/app/hero.service.ts (update)">
</code-example>
The `HttpClient.put()` method takes three parameters
The `HttpClient.put()` method takes three parameters:
* the URL
* the data to update (the modified hero in this case)
* options
@ -304,20 +266,19 @@ The `HttpClient.put()` method takes three parameters
The URL is unchanged. The heroes web API knows which hero to update by looking at the hero's `id`.
The heroes web API expects a special header in HTTP save requests.
That header is in the `httpOptions` constant defined in the `HeroService`.
That header is in the `httpOptions` constant defined in the `HeroService`. Add the following to the `HeroService` class.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="http-options"
header="src/app/hero.service.ts">
<code-example path="toh-pt6/src/app/hero.service.ts" region="http-options" header="src/app/hero.service.ts">
</code-example>
Refresh the browser, change a hero name and save your change. Navigating to the previous view is implemented in the `save()` method defined in `HeroDetailComponent`.
Refresh the browser, change a hero name and save your change. The `save()`
method in `HeroDetailComponent`navigates to the previous view.
The hero now appears in the list with the changed name.
## Add a new hero
To add a hero, this app only needs the hero's name. You can use an `input`
To add a hero, this app only needs the hero's name. You can use an `<input>`
element paired with an add button.
Insert the following into the `HeroesComponent` template, just after
@ -325,29 +286,26 @@ the heading:
<code-example path="toh-pt6/src/app/heroes/heroes.component.html" region="add" header="src/app/heroes/heroes.component.html (add)"></code-example>
In response to a click event, call the component's click handler and then
clear the input field so that it's ready for another name.
In response to a click event, call the component's click handler, `add()`, and then
clear the input field so that it's ready for another name. Add the following to the
`HeroesComponent` class:
<code-example path="toh-pt6/src/app/heroes/heroes.component.ts" region="add" header="src/app/heroes/heroes.component.ts (add)"></code-example>
When the given name is non-blank, the handler creates a `Hero`-like object
from the name (it's only missing the `id`) and passes it to the services `addHero()` method.
When `addHero` saves successfully, the `subscribe` callback
When `addHero()` saves successfully, the `subscribe()` callback
receives the new hero and pushes it into to the `heroes` list for display.
You'll write `HeroService.addHero` in the next section.
#### Add _HeroService.addHero()_
Add the following `addHero()` method to the `HeroService` class.
<code-example path="toh-pt6/src/app/hero.service.ts" region="addHero" header="src/app/hero.service.ts (addHero)"></code-example>
`HeroService.addHero()` differs from `updateHero` in two ways.
`addHero()` differs from `updateHero()` in two ways:
* it calls `HttpClient.post()` instead of `put()`.
* it expects the server to generate an id for the new hero,
* It calls `HttpClient.post()` instead of `put()`.
* It expects the server to generate an id for the new hero,
which it returns in the `Observable<Hero>` to the caller.
Refresh the browser and add some heroes.
@ -359,7 +317,7 @@ Each hero in the heroes list should have a delete button.
Add the following button element to the `HeroesComponent` template, after the hero
name in the repeated `<li>` element.
<code-example path="toh-pt6/src/app/heroes/heroes.component.html" region="delete"></code-example>
<code-example path="toh-pt6/src/app/heroes/heroes.component.html" header="src/app/hero.service.ts" region="delete"></code-example>
The HTML for the list of heroes should look like this:
@ -369,7 +327,7 @@ To position the delete button at the far right of the hero entry,
add some CSS to the `heroes.component.css`. You'll find that CSS
in the [final review code](#heroescomponent) below.
Add the `delete()` handler to the component.
Add the `delete()` handler to the component class.
<code-example path="toh-pt6/src/app/heroes/heroes.component.ts" region="delete" header="src/app/heroes/heroes.component.ts (delete)"></code-example>
@ -379,31 +337,29 @@ The component's `delete()` method immediately removes the _hero-to-delete_ from
anticipating that the `HeroService` will succeed on the server.
There's really nothing for the component to do with the `Observable` returned by
`heroService.delete()`. **It must subscribe anyway**.
`heroService.delete()` **but it must subscribe anyway**.
<div class="alert is-important">
If you neglect to `subscribe()`, the service will not send the delete request to the server!
As a rule, an `Observable` _does nothing_ until something subscribes!
If you neglect to `subscribe()`, the service will not send the delete request to the server.
As a rule, an `Observable` _does nothing_ until something subscribes.
Confirm this for yourself by temporarily removing the `subscribe()`,
clicking "Dashboard", then clicking "Heroes".
You'll see the full list of heroes again.
</div>
#### Add _HeroService.deleteHero()_
Add a `deleteHero()` method to `HeroService` like this.
Next, add a `deleteHero()` method to `HeroService` like this.
<code-example path="toh-pt6/src/app/hero.service.ts" region="deleteHero" header="src/app/hero.service.ts (delete)"></code-example>
Note that
Note the following key points:
* it calls `HttpClient.delete`.
* the URL is the heroes resource URL plus the `id` of the hero to delete
* you don't send data as you did with `put` and `post`.
* you still send the `httpOptions`.
* `deleteHero()` calls `HttpClient.delete()`.
* The URL is the heroes resource URL plus the `id` of the hero to delete.
* You don't send data as you did with `put()` and `post()`.
* You still send the `httpOptions`.
Refresh the browser and try the new delete functionality.
@ -413,43 +369,36 @@ In this last exercise, you learn to chain `Observable` operators together
so you can minimize the number of similar HTTP requests
and consume network bandwidth economically.
You will add a *heroes search* feature to the *Dashboard*.
As the user types a name into a search box,
You will add a heroes search feature to the Dashboard.
As the user types a name into a search box,
you'll make repeated HTTP requests for heroes filtered by that name.
Your goal is to issue only as many requests as necessary.
#### _HeroService.searchHeroes_
#### `HeroService.searchHeroes()`
Start by adding a `searchHeroes` method to the `HeroService`.
Start by adding a `searchHeroes()` method to the `HeroService`.
<code-example
path="toh-pt6/src/app/hero.service.ts"
region="searchHeroes"
header="src/app/hero.service.ts">
<code-example path="toh-pt6/src/app/hero.service.ts" region="searchHeroes" header="src/app/hero.service.ts">
</code-example>
The method returns immediately with an empty array if there is no search term.
The rest of it closely resembles `getHeroes()`.
The only significant difference is the URL,
which includes a query string with the search term.
The rest of it closely resembles `getHeroes()`, the only significant difference being
the URL, which includes a query string with the search term.
### Add search to the Dashboard
Open the `DashboardComponent` _template_ and
Add the hero search element, `<app-hero-search>`, to the bottom of the `DashboardComponent` template.
Open the `DashboardComponent` template and
add the hero search element, `<app-hero-search>`, to the bottom of the markup.
<code-example
path="toh-pt6/src/app/dashboard/dashboard.component.html" header="src/app/dashboard/dashboard.component.html" linenums="false">
<code-example path="toh-pt6/src/app/dashboard/dashboard.component.html" header="src/app/dashboard/dashboard.component.html" linenums="false">
</code-example>
This template looks a lot like the `*ngFor` repeater in the `HeroesComponent` template.
Unfortunately, adding this element breaks the app.
Angular can't find a component with a selector that matches `<app-hero-search>`.
For this to work, the next step is to add a component with a selector that matches `<app-hero-search>`.
The `HeroSearchComponent` doesn't exist yet. Fix that.
### Create _HeroSearchComponent_
### Create `HeroSearchComponent`
Create a `HeroSearchComponent` with the CLI.
@ -457,70 +406,62 @@ Create a `HeroSearchComponent` with the CLI.
ng generate component hero-search
</code-example>
The CLI generates the three `HeroSearchComponent` files and adds the component to the `AppModule` declarations
The CLI generates the three `HeroSearchComponent` files and adds the component to the `AppModule` declarations.
Replace the generated `HeroSearchComponent` _template_ with a text box and a list of matching search results like this.
Replace the generated `HeroSearchComponent` template with an `<input>` and a list of matching search results, as follows.
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" header="src/app/hero-search/hero-search.component.html"></code-example>
Add private CSS styles to `hero-search.component.css`
as listed in the [final code review](#herosearchcomponent) below.
As the user types in the search box, an *input* event binding calls the component's `search()`
method with the new search box value.
As the user types in the search box, an input event binding calls the
component's `search()` method with the new search box value.
{@a asyncpipe}
### _AsyncPipe_
### `AsyncPipe`
As expected, the `*ngFor` repeats hero objects.
The `*ngFor` repeats hero objects. Notice that the `*ngFor` iterates over a list called `heroes$`, not `heroes`. The `$` is a convention that indicates `heroes$` is an `Observable`, not an array.
Look closely and you'll see that the `*ngFor` iterates over a list called `heroes$`, not `heroes`.
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" region="async"></code-example>
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" header="src/app/hero-search/hero-search.component.html" region="async"></code-example>
The `$` is a convention that indicates `heroes$` is an `Observable`, not an array.
The `*ngFor` can't do anything with an `Observable`.
But there's also a pipe character (`|`) followed by `async`,
which identifies Angular's `AsyncPipe`.
The `AsyncPipe` subscribes to an `Observable` automatically so you won't have to
Since `*ngFor` can't do anything with an `Observable`, use the
pipe character (`|`) followed by `async`. This identifies Angular's `AsyncPipe` and subscribes to an `Observable` automatically so you won't have to
do so in the component class.
### Fix the _HeroSearchComponent_ class
### Edit the `HeroSearchComponent` class
Replace the generated `HeroSearchComponent` class and metadata as follows.
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" header="src/app/hero-search/hero-search.component.ts"></code-example>
Notice the declaration of `heroes$` as an `Observable`
<code-example
path="toh-pt6/src/app/hero-search/hero-search.component.ts"
region="heroes-stream">
Notice the declaration of `heroes$` as an `Observable`:
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" header="src/app/hero-search/hero-search.component.ts" region="heroes-stream">
</code-example>
You'll set it in [`ngOnInit()`](#search-pipe).
You'll set it in [`ngOnInit()`](#search-pipe).
Before you do, focus on the definition of `searchTerms`.
### The _searchTerms_ RxJS subject
### The `searchTerms` RxJS subject
The `searchTerms` property is declared as an RxJS `Subject`.
The `searchTerms` property is an RxJS `Subject`.
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" region="searchTerms"></code-example>
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" header="src/app/hero-search/hero-search.component.ts" region="searchTerms"></code-example>
A `Subject` is both a source of _observable_ values and an `Observable` itself.
A `Subject` is both a source of observable values and an `Observable` itself.
You can subscribe to a `Subject` as you would any `Observable`.
You can also push values into that `Observable` by calling its `next(value)` method
as the `search()` method does.
The `search()` method is called via an _event binding_ to the
textbox's `input` event.
The event binding to the textbox's `input` event calls the `search()` method.
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" region="input"></code-example>
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.html" header="src/app/hero-search/hero-search.component.html" region="input"></code-example>
Every time the user types in the textbox, the binding calls `search()` with the textbox value, a "search term".
Every time the user types in the textbox, the binding calls `search()` with the textbox value, a "search term".
The `searchTerms` becomes an `Observable` emitting a steady stream of search terms.
{@a search-pipe}
@ -528,28 +469,24 @@ The `searchTerms` becomes an `Observable` emitting a steady stream of search ter
### Chaining RxJS operators
Passing a new search term directly to the `searchHeroes()` after every user keystroke would create an excessive amount of HTTP requests,
taxing server resources and burning through the cellular network data plan.
taxing server resources and burning through data plans.
Instead, the `ngOnInit()` method pipes the `searchTerms` observable through a sequence of RxJS operators that reduce the number of calls to the `searchHeroes()`,
ultimately returning an observable of timely hero search results (each a `Hero[]`).
Here's the code.
Here's a closer look at the code.
<code-example
path="toh-pt6/src/app/hero-search/hero-search.component.ts"
region="search">
<code-example path="toh-pt6/src/app/hero-search/hero-search.component.ts" header="src/app/hero-search/hero-search.component.ts" region="search">
</code-example>
Each operator works as follows:
* `debounceTime(300)` waits until the flow of new string events pauses for 300 milliseconds
before passing along the latest string. You'll never make requests more frequently than 300ms.
* `distinctUntilChanged()` ensures that a request is sent only if the filter text changed.
* `switchMap()` calls the search service for each search term that makes it through `debounce` and `distinctUntilChanged`.
* `switchMap()` calls the search service for each search term that makes it through `debounce()` and `distinctUntilChanged()`.
It cancels and discards previous search observables, returning only the latest search service observable.
@ -563,7 +500,7 @@ It cancels and discards previous search observables, returning only the latest s
`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.
Note that _canceling_ a previous `searchHeroes()` _Observable_
Note that canceling a previous `searchHeroes()` Observable
doesn't actually abort a pending HTTP request.
Unwanted results are simply discarded before they reach your application code.
@ -590,78 +527,78 @@ Here are the code files discussed on this page (all in the `src/app/` folder).
{@a heroservice}
{@a inmemorydataservice}
{@a appmodule}
#### _HeroService_, _InMemoryDataService_, _AppModule_
#### `HeroService`, `InMemoryDataService`, `AppModule`
<code-tabs>
<code-pane
header="hero.service.ts"
<code-pane
header="hero.service.ts"
path="toh-pt6/src/app/hero.service.ts">
</code-pane>
<code-pane
<code-pane
header="in-memory-data.service.ts"
path="toh-pt6/src/app/in-memory-data.service.ts">
</code-pane>
<code-pane
header="app.module.ts"
<code-pane
header="app.module.ts"
path="toh-pt6/src/app/app.module.ts">
</code-pane>
</code-tabs>
{@a heroescomponent}
#### _HeroesComponent_
#### `HeroesComponent`
<code-tabs>
<code-pane
header="heroes/heroes.component.html"
<code-pane
header="heroes/heroes.component.html"
path="toh-pt6/src/app/heroes/heroes.component.html">
</code-pane>
<code-pane
header="heroes/heroes.component.ts"
<code-pane
header="heroes/heroes.component.ts"
path="toh-pt6/src/app/heroes/heroes.component.ts">
</code-pane>
<code-pane
header="heroes/heroes.component.css"
<code-pane
header="heroes/heroes.component.css"
path="toh-pt6/src/app/heroes/heroes.component.css">
</code-pane>
</code-tabs>
{@a herodetailcomponent}
#### _HeroDetailComponent_
#### `HeroDetailComponent`
<code-tabs>
<code-pane
<code-pane
header="hero-detail/hero-detail.component.html"
path="toh-pt6/src/app/hero-detail/hero-detail.component.html">
</code-pane>
<code-pane
header="hero-detail/hero-detail.component.ts"
<code-pane
header="hero-detail/hero-detail.component.ts"
path="toh-pt6/src/app/hero-detail/hero-detail.component.ts">
</code-pane>
</code-tabs>
{@a dashboardcomponent}
#### _DashboardComponent_
#### `DashboardComponent`
<code-tabs>
<code-pane
<code-pane
header="src/app/dashboard/dashboard.component.html"
path="toh-pt6/src/app/dashboard/dashboard.component.html">
</code-pane>
</code-tabs>
{@a herosearchcomponent}
#### _HeroSearchComponent_
#### `HeroSearchComponent`
<code-tabs>
<code-pane
<code-pane
header="hero-search/hero-search.component.html"
path="toh-pt6/src/app/hero-search/hero-search.component.html">
</code-pane>
<code-pane
<code-pane
header="hero-search/hero-search.component.ts"
path="toh-pt6/src/app/hero-search/hero-search.component.ts">
</code-pane>
<code-pane
<code-pane
header="hero-search/hero-search.component.css"
path="toh-pt6/src/app/hero-search/hero-search.component.css">
</code-pane>