angular-cn/aio/content/tutorial/toh-pt5.md

611 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Routing
There are new requirements for the Tour of Heroes app:
* Add a *Dashboard* view.
* Add the ability to navigate between the *Heroes* and *Dashboard* views.
* When users click a hero name in either view, navigate to a detail view of the selected hero.
* When users click a *deep link* in an email, open the detail view for a particular hero.
When youre done, users will be able to navigate the app like this:
<figure>
<img src='generated/images/guide/toh/nav-diagram.png' alt="View navigations">
</figure>
## Add the _AppRoutingModule_
An Angular best practice is to load and configure the router in a separate, top-level module
that is dedicated to routing and imported by the root `AppModule`.
By convention, the module class name is `AppRoutingModule` and it belongs in the `app-routing.module.ts` in the `src/app` folder.
Use the CLI to generate it.
<code-example language="sh" class="code-shell">
ng generate module app-routing --flat --module=app
</code-example>
<div class="l-sub-section">
`--flat` puts the file in `src/app` instead of its own folder.<br>
`--module=app` tells the CLI to register it in the `imports` array of the `AppModule`.
</div>
The generated file looks like this:
<code-example path="toh-pt5/src/app/app-routing.module.0.ts"
title="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.
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"
title="src/app/app-routing.module.ts (v1)">
</code-example>
### Add 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:
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"
region="heroes-route">
</code-example>
Once you've finished setting up, the router will match that URL to `path: 'heroes'`
and display the `HeroesComponent`.
### _RouterModule.forRoot()_
You first must initialize the router and start it listening for browser location changes.
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:
<code-example path="toh-pt5/src/app/app-routing.module.ts"
region="ngmodule-imports">
</code-example>
<div class="l-sub-section">
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,
and performs the initial navigation based on the current browser URL.
</div>
## Add _RouterOutlet_
Open the `AppComponent` template replace the `<app-heroes>` element with a `<router-outlet>` element.
<code-example path="toh-pt5/src/app/app.component.html"
region="outlet"
title="src/app/app.component.html (router-outlet)">
</code-example>
You removed `<app-heroes>` because you will only display the `HeroesComponent` when the user navigates to it.
The `<router-outlet>` tells the router where to display routed views.
<div class="l-sub-section">
The `RouterOutlet` is one of the router directives that became available to the `AppComponent`
because `AppModule` imports `AppRoutingModule` which exported `RouterModule`.
</div>
#### Try it
You should still be running with this CLI command.
<code-example language="sh" class="code-shell">
ng serve
</code-example>
The browser should refresh and display the app title but not the list of heroes.
Look at the browser's address bar.
The URL ends in `/`.
The route path to `HeroesComponent` is `/heroes`.
Append `/heroes` to the URL in the browser address bar.
You should see the familiar heroes master/detail view.
{@a routerlink}
## 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.
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"
title="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)
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,
but not the heroes list.
Click the link.
The address bar updates to `/heroes` and the list of heroes appears.
<div class="l-sub-section">
Make this and future navigation links look better by adding private CSS styles to `app.component.css`
as listed in the [final code review](#appcomponent) below.
</div>
## Add a dashboard view
Routing makes more sense when there are multiple views.
So far there's only the heroes view.
Add a `DashboardComponent` using the CLI:
<code-example language="sh" class="code-shell">
ng generate component dashboard
</code-example>
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:
<code-tabs>
<code-pane
title="src/app/dashboard/dashboard.component.html" path="toh-pt5/src/app/dashboard/dashboard.component.1.html">
</code-pane>
<code-pane
title="src/app/dashboard/dashboard.component.ts" path="toh-pt5/src/app/dashboard/dashboard.component.ts">
</code-pane>
<code-pane
title="src/app/dashboard/dashboard.component.css" path="toh-pt5/src/app/dashboard/dashboard.component.css">
</code-pane>
</code-tabs>
The _template_ presents a grid of hero name links.
* The `*ngFor` repeater creates as many links as are in the component's `heroes` array.
* The links are styled as colored blocks by the `dashboard.component.css`.
* The links don't go anywhere yet but [they will shortly](#hero-details).
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`.
This `getHeroes` reduces the number of heroes displayed to four
(2nd, 3rd, 4th, and 5th).
<code-example path="toh-pt5/src/app/dashboard/dashboard.component.ts" region="getHeroes">
</code-example>
### Add the dashboard route
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"
title="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>
### Add a default route
When the app starts, the browsers 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>
This route redirects a URL that fully matches the empty path to the route whose path is `'/dashboard'`.
After the browser refreshes, the router loads the `DashboardComponent`
and the browser address bar shows the `/dashboard` URL.
### Add dashboard link to the shell
The user should be able to navigate back and forth between the
`DashboardComponent` and the `HeroesComponent` by clicking links in the
navigation area near the top of the page.
Add a dashboard navigation link to the `AppComponent` shell template, just above the *Heroes* link.
<code-example path="toh-pt5/src/app/app.component.html" title="src/app/app.component.html">
</code-example>
After the browser refreshes you can navigate freely between the two views by clicking the links.
{@a hero-details}
## Navigating to hero details
The `HeroDetailsComponent` displays details of a selected hero.
At the moment the `HeroDetailsComponent` is only visible at the bottom of the `HeroesComponent`
The user should be able to get to these details in three ways.
1. By clicking a hero in the dashboard.
1. By clicking a hero in the heroes list.
1. By pasting a "deep link" URL into the browser address bar that identifies the hero to display.
In this section, you'll enable navigation to the `HeroDetailsComponent`
and liberate it from the `HeroesComponent`.
### Delete _hero details_ from _HeroesComponent_
When the user clicks a hero item in the `HeroesComponent`,
the app should navigate to the `HeroDetailComponent`,
replacing the heroes list view with the hero detail view.
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.
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`.
Open `AppRoutingModule` and import `HeroDetailComponent`.
<code-example
path="toh-pt5/src/app/app-routing.module.ts"
region="import-herodetail"
title="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>
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"
title="src/app/app-routing.module.ts (all routes)">
</code-example>
### _DashboardComponent_ hero links
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"
title="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 interation's `hero.id` into each
[`routerLink`](#routerlink).
{@a heroes-component-links}
### _HeroesComponent_ hero links
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"
title="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
is the same as in the dashboard template
<code-example
path="toh-pt5/src/app/heroes/heroes.component.html"
region="list"
title="src/app/heroes/heroes.component.html (list with links)">
</code-example>
You'll have to fix the private stylesheet (`heroes.component.css`) to make
the list look as it did before.
Revised styles are in the [final code review](#heroescomponent) at the bottom of this guide.
#### Remove dead code (optional)
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"
title="src/app/heroes/heroes.component.ts (cleaned up)" linenums="false">
</code-example>
## Routable *HeroDetailComponent*
Previously, the parent `HeroesComponent` set the `HeroDetailComponent.hero`
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_.
* 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"
title="src/app/hero-detail/hero-detail.component.ts">
</code-example>
{@a hero-detail-ctor}
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>
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.
The [`HeroService`](tutorial/toh-pt4) gets hero data from the remote server
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
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>
The `route.snapshot` is a static image of the route information shortly after the component was created.
The `paramMap` is a dictionary of route parameter values extracted from the URL.
The `"id"` key returns the `id` of the hero to fetch.
Route parameters are always strings.
The JavaScript (+) operator converts the string to a number,
which is what a hero `id` should be.
The browser refreshes and the app crashes with a compiler error.
`HeroService` doesn't have a `getHero()` method.
Add it now.
### Add *HeroService.getHero()*
Open `HeroService` and add this `getHero()` method
<code-example
path="toh-pt5/src/app/hero.service.ts"
region="getHero"
title="src/app/hero.service.ts (getHero)">
</code-example>
<div class="alert is-important">
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>
Like [`getHeroes()`](tutorial/toh-pt4#observable-heroservice),
`getHero()` has an asynchronous signature.
It returns a _mock hero_ as an `Observable`, using the RxJS `of()` function.
You'll be able to re-implement `getHero()` as a real `Http` request
without having to change the `HeroDetailComponent` that calls it.
#### Try it
The browser refreshes and the app is working again.
You can click a hero in the dashboard or in the heroes list and navigate to that hero's detail view.
If you paste `localhost:4200/detail/11` in the browser address bar,
the router navigates to the detail view for the hero with `id: 11`, "Mr. Nice".
{@a goback}
### Find the way back
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.
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"
title="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
in the browser's history stack
using the `Location` service that you [injected previously](#hero-detail-ctor).
<code-example path="toh-pt5/src/app/hero-detail/hero-detail.component.ts" region="goBack" title="src/app/hero-detail/hero-detail.component.ts (goBack)">
</code-example>
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_ and _AppModule_
<code-tabs>
<code-pane
title="src/app/app-routing.module.ts"
path="toh-pt5/src/app/app-routing.module.ts">
</code-pane>
<code-pane
title="src/app/app.module.ts"
path="toh-pt5/src/app/app.module.ts">
</code-pane>
</code-tabs>
{@a appcomponent}
#### _AppComponent_
<code-tabs>
<code-pane
title="src/app/app.component.html"
path="toh-pt5/src/app/app.component.html">
</code-pane>
<code-pane
title="src/app/app.component.css"
path="toh-pt5/src/app/app.component.css">
</code-pane>
</code-tabs>
{@a dashboardcomponent}
#### _DashboardComponent_
<code-tabs>
<code-pane
title="src/app/dashboard/dashboard.component.html" path="toh-pt5/src/app/dashboard/dashboard.component.html">
</code-pane>
<code-pane
title="src/app/dashboard/dashboard.component.ts" path="toh-pt5/src/app/dashboard/dashboard.component.ts">
</code-pane>
<code-pane
title="src/app/dashboard/dashboard.component.css" path="toh-pt5/src/app/dashboard/dashboard.component.css">
</code-pane>
</code-tabs>
{@a heroescomponent}
#### _HeroesComponent_
<code-tabs>
<code-pane
title="src/app/heroes/heroes.component.html" path="toh-pt5/src/app/heroes/heroes.component.html">
</code-pane>
<code-pane
title="src/app/heroes/heroes.component.ts"
path="toh-pt5/src/app/heroes/heroes.component.ts">
</code-pane>
<code-pane
title="src/app/heroes/heroes.component.css"
path="toh-pt5/src/app/heroes/heroes.component.css">
</code-pane>
</code-tabs>
{@a herodetailcomponent}
#### _HeroDetailComponent_
<code-tabs>
<code-pane
title="src/app/hero-detail/hero-detail.component.html" path="toh-pt5/src/app/hero-detail/hero-detail.component.html">
</code-pane>
<code-pane
title="src/app/hero-detail/hero-detail.component.ts" path="toh-pt5/src/app/hero-detail/hero-detail.component.ts">
</code-pane>
<code-pane
title="src/app/hero-detail/hero-detail.component.css" path="toh-pt5/src/app/hero-detail/hero-detail.component.css">
</code-pane>
</code-tabs>
## Summary
* 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 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.
* You used router link parameters to navigate to the detail view of a user-selected hero.
* You shared the `HeroService` among multiple components.