Milestone 3: Heroes feature
You've seen how to navigate using the
RouterLink directive. Now you'll learn the following:- Organize the app and routes into feature areas using modules.
- Navigate imperatively from one component to another.
- Pass required and optional information in route parameters.
This example recreates the heroes feature in the "Services" episode of the Tour of Heroes tutorial, and you'll be copying much of the code from the Tour of Heroes: Services example code / download example .
Here's how the user will experience this version of the app:

A typical application has multiple feature areas, each dedicated to a particular business purpose.
While you could continue to add files to the
src/app/ folder, that is unrealistic and ultimately not maintainable. Most developers prefer to put each feature area in its own folder.
You are about to break up the app into different feature modules, each with its own concerns. Then you'll import into the main module and navigate among them.
Add heroes functionality
Follow these steps:
- Create the
src/app/heroesfolder; you'll be adding files implementing hero management there. - Delete the placeholder
hero-list.component.tsthat's in theappfolder. - Create a new
hero-list.component.tsundersrc/app/heroes. - Copy into it the contents of the
app.component.tsfrom theTour of Heroes: Services example code / download example . - Make a few minor but necessary changes:
- Delete the
selector(routed components don't need them). - Delete the
. - Relabel the
to.HEROES
- Delete the
at the bottom of the template. - Rename the
AppComponentclass toHeroListComponent.
- Delete the
- Copy the
hero-detail.component.tsand thehero.service.tsfiles into theheroessubfolder. - Create a (pre-routing)
heroes.module.tsin the heroes folder that looks like this:
- import { NgModule } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { FormsModule } from '@angular/forms';
-
- import { HeroListComponent } from './hero-list.component';
- import { HeroDetailComponent } from './hero-detail.component';
-
- import { HeroService } from './hero.service';
-
- @NgModule({
- imports: [
- CommonModule,
- FormsModule,
- ],
- declarations: [
- HeroListComponent,
- HeroDetailComponent
- ],
- providers: [ HeroService ]
- })
- export class HeroesModule {}
When you're done, you'll have these hero management files:
src/app/heroes
Hero feature routing requirements
The heroes feature has two interacting components, the hero list and the hero detail. The list view is self-sufficient; you navigate to it, it gets a list of heroes and displays them.
The detail view is different. It displays a particular hero. It can't know which hero to show on its own. That information must come from outside.
When the user selects a hero from the list, the app should navigate to the detail view and show that hero. You tell the detail view which hero to display by including the selected hero's id in the route URL.
Hero feature route configuration
Create a new
heroes-routing.module.ts in the heroes folder using the same techniques you learned while creating the AppRoutingModule.
- import { NgModule } from '@angular/core';
- import { RouterModule, Routes } from '@angular/router';
-
- import { HeroListComponent } from './hero-list.component';
- import { HeroDetailComponent } from './hero-detail.component';
-
- const heroesRoutes: Routes = [
- { path: 'heroes', component: HeroListComponent },
- { path: 'hero/:id', component: HeroDetailComponent }
- ];
-
- @NgModule({
- imports: [
- RouterModule.forChild(heroesRoutes)
- ],
- exports: [
- RouterModule
- ]
- })
- export class HeroRoutingModule { }
Put the routing module file in the same folder as its companion module file. Here both
heroes-routing.module.ts and heroes.module.ts are in the same src/app/heroes folder.
Consider giving each feature module its own route configuration file. It may seem like overkill early when the feature routes are simple. But routes have a tendency to grow more complex and consistency in patterns pays off over time.
Import the hero components from their new locations in the
src/app/heroes/ folder, define the two hero routes, and export the HeroRoutingModule class.
Now that you have routes for the
Heroes module, register them with the Router via the RouterModule almostas you did in the AppRoutingModule.
There is a small but critical difference. In the
AppRoutingModule, you used the static RouterModule.forRoot method to register the routes and application level service providers. In a feature module you use the static forChild method.
Only call
RouterModule.forRoot in the root AppRoutingModule (or the AppModule if that's where you register top level application routes). In any other module, you must call the RouterModule.forChild method to register additional routes.Add the routing module to the HeroesModule
Add the
HeroRoutingModule to the HeroModule just as you added AppRoutingModule to the AppModule.
Open
heroes.module.ts. Import the HeroRoutingModule token from heroes-routing.module.ts and add it to the imports array of the HeroesModule. The finished HeroesModule looks like this:
- import { NgModule } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { FormsModule } from '@angular/forms';
-
- import { HeroListComponent } from './hero-list.component';
- import { HeroDetailComponent } from './hero-detail.component';
-
- import { HeroService } from './hero.service';
-
- import { HeroRoutingModule } from './heroes-routing.module';
-
- @NgModule({
- imports: [
- CommonModule,
- FormsModule,
- HeroRoutingModule
- ],
- declarations: [
- HeroListComponent,
- HeroDetailComponent
- ],
- providers: [ HeroService ]
- })
- export class HeroesModule {}
Remove duplicate hero routes
The hero routes are currently defined in two places: in the
HeroesRoutingModule, by way of the HeroesModule, and in the AppRoutingModule.
Routes provided by feature modules are combined together into their imported module's routes by the router. This allows you to continue defining the feature module routes without modifying the main route configuration.
But you don't want to define the same routes twice. Remove the
HeroListComponent import and the /heroesroute from the app-routing.module.ts.
Leave the default and the wildcard routes! These are concerns at the top level of the application itself.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CrisisListComponent } from './crisis-list.component';
// import { HeroListComponent } from './hero-list.component'; // <-- delete="" line="" span="" this="">
import { PageNotFoundComponent } from './not-found.component';
const appRoutes: Routes = [
{ path: 'crisis-center', component: CrisisListComponent },
// { path: 'heroes', component: HeroListComponent }, // <-- delete="" line="" span="" this="">
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{ enableTracing: true } // <-- debugging="" only="" purposes="" span="">
)
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}-->-->-->
Import hero module into AppModule
The heroes feature module is ready, but the application doesn't know about the
HeroesModule yet. Open app.module.ts and revise it as follows.
Remove the
HeroListComponent from the AppModule's declarations because it's now provided by the HeroesModule. This is important. There can be only one owner for a declared component. In this case, the Heroes module is the owner of the Heroes components and is making them available to components in the AppModule via the HeroesModule.
As a result, the
AppModule no longer has specific knowledge of the hero feature, its components, or its route details. You can evolve the hero feature with more components and different routes. That's a key benefit of creating a separate module for each feature area.
After these steps, the
AppModule should look like this:
- import { NgModule } from '@angular/core';
- import { BrowserModule } from '@angular/platform-browser';
- import { FormsModule } from '@angular/forms';
-
- import { AppComponent } from './app.component';
- import { AppRoutingModule } from './app-routing.module';
- import { HeroesModule } from './heroes/heroes.module';
-
- import { CrisisListComponent } from './crisis-list.component';
- import { PageNotFoundComponent } from './not-found.component';
-
- @NgModule({
- imports: [
- BrowserModule,
- FormsModule,
- HeroesModule,
- AppRoutingModule
- ],
- declarations: [
- AppComponent,
- CrisisListComponent,
- PageNotFoundComponent
- ],
- bootstrap: [ AppComponent ]
- })
- export class AppModule { }
Module import order matters
Look at the module
imports array. Notice that the AppRoutingModule is last. Most importantly, it comes afterthe HeroesModule.imports: [
BrowserModule,
FormsModule,
HeroesModule,
AppRoutingModule
],
The order of route configuration matters. The router accepts the first route that matches a navigation request path.
When all routes were in one
AppRoutingModule, you put the default and wildcard routes last, after the /heroes route, so that the router had a chance to match a URL to the /heroes route before hitting the wildcard route and navigating to "Page not found".
The routes are no longer in one file. They are distributed across two modules,
AppRoutingModule and HeroesRoutingModule.
Each routing module augments the route configuration in the order of import. If you list
AppRoutingModulefirst, the wildcard route will be registered before the hero routes. The wildcard route—which matches everyURL—will intercept the attempt to navigate to a hero route.
Reverse the routing modules and see for yourself that a click of the heroes link results in "Page not found". Learn about inspecting the runtime router configuration below.
Route definition with a parameter
Return to the
HeroesRoutingModule and look at the route definitions again. The route to HeroDetailComponent has a twist.{ path: 'hero/:id', component: HeroDetailComponent }
Notice the
:id token in the path. That creates a slot in the path for a Route Parameter. In this case, the router will insert the id of a hero into that slot.
If you tell the router to navigate to the detail component and display "Magneta", you expect a hero id to appear in the browser URL like this:
localhost:3000/hero/15
If a user enters that URL into the browser address bar, the router should recognize the pattern and go to the same "Magneta" detail view.
Embedding the route parameter token,
:id, in the route definition path is a good choice for this scenario because the id is required by the HeroDetailComponent and because the value 15 in the path clearly distinguishes the route to "Magneta" from a route for some other hero.Setting the route parameters in the list view
After navigating to the
HeroDetailComponent, you expect to see the details of the selected hero. You need two pieces of information: the routing path to the component and the hero's id.
Accordingly, the link parameters array has two items: the routing path and a route parameter that specifies the
id of the selected hero.['/hero', hero.id] // { 15 }
The router composes the destination URL from the array like this:
localhost:3000/hero/15.
How does the target
HeroDetailComponent learn about that id? Don't analyze the URL. Let the router do it.
The router extracts the route parameter (
id:15) from the URL and supplies it to the HeroDetailComponent via the ActivatedRoute service.Activated Route in action
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
Import the
switchMap operator because you need it later to process the Observable route parameters.import { switchMap } from 'rxjs/operators';
As usual, you write a constructor that asks Angular to inject services that the component requires and reference them as private variables.
constructor(
private route: ActivatedRoute,
private router: Router,
private service: HeroService
) {}
Later, in the
ngOnInit method, you use the ActivatedRoute service to retrieve the parameters for the route, pull the hero id from the parameters and retrieve the hero to display.ngOnInit() {
this.hero$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) =>
this.service.getHero(params.get('id')))
);
}
The
paramMap processing is a bit tricky. When the map changes, you get() the id parameter from the changed parameters.
Then you tell the
HeroService to fetch the hero with that id and return the result of the HeroService request.
You might think to use the RxJS
map operator. But the HeroService returns an Observable. So you flatten the Observable with the switchMap operator instead.
The
switchMap operator also cancels previous in-flight requests. If the user re-navigates to this route with a new id while the HeroService is still retrieving the old id, switchMap discards that old request and returns the hero for the new id.
The observable
Subscription will be handled by the AsyncPipe and the component's hero property will be (re)set with the retrieved hero.ParamMap API
The
ParamMap API is inspired by the URLSearchParams interface. It provides methods to handle parameter access for both route parameters (paramMap) and query parameters (queryParamMap).| Member | Description |
|---|---|
has(name) |
Returns
true if the parameter name is in the map of parameters. |
get(name) |
Returns the parameter name value (a
string) if present, or null if the parameter name is not in the map. Returns the first element if the parameter value is actually an array of values. |
getAll(name) |
Returns a
string array of the parameter name value if found, or an empty array if the parameter name value is not in the map. Use getAll when a single parameter could have multiple values. |
keys |
Returns a
string array of all parameter names in the map. |
Observable paramMap and component reuse
In this example, you retrieve the route parameter map from an
Observable. That implies that the route parameter map can change during the lifetime of this component.
They might. By default, the router re-uses a component instance when it re-navigates to the same component type without visiting a different component first. The route parameters could change each time.
Suppose a parent component navigation bar had "forward" and "back" buttons that scrolled through the list of heroes. Each click navigated imperatively to the
HeroDetailComponent with the next or previous id.
You don't want the router to remove the current
HeroDetailComponent instance from the DOM only to re-create it for the next id. That could be visibly jarring. Better to simply re-use the same component instance and update the parameter.
Unfortunately,
ngOnInit is only called once per component instantiation. You need a way to detect when the route parameters change from within the same instance. The observable paramMap property handles that beautifully.
When subscribing to an observable in a component, you almost always arrange to unsubscribe when the component is destroyed.
There are a few exceptional observables where this is not necessary. The
ActivatedRouteobservables are among the exceptions.
The
ActivatedRoute and its observables are insulated from the Router itself. The Router destroys a routed component when it is no longer needed and the injected ActivatedRoute dies with it.
Feel free to unsubscribe anyway. It is harmless and never a bad practice.
Snapshot: the no-observable alternative
This application won't re-use the
HeroDetailComponent. The user always returns to the hero list to select another hero to view. There's no way to navigate from one hero detail to another hero detail without visiting the list component in between. Therefore, the router creates a new HeroDetailComponent instance every time.
When you know for certain that a
HeroDetailComponent instance will never, never, ever be re-used, you can simplify the code with the snapshot.
The
route.snapshot provides the initial value of the route parameter map. You can access the parameters directly without subscribing or adding observable operators. It's much simpler to write and read:ngOnInit() {
let id = this.route.snapshot.paramMap.get('id');
this.hero$ = this.service.getHero(id);
}
Remember: you only get the initial value of the parameter map with this technique. Stick with the observable
paramMap approach if there's even a chance that the router could re-use the component. This sample stays with the observable paramMap strategy just in case.Navigating back to the list component
The
HeroDetailComponent has a "Back" button wired to its gotoHeroes method that navigates imperatively back to the HeroListComponent.
The router
navigate method takes the same one-item link parameters array that you can bind to a [routerLink] directive. It holds the path to the HeroListComponent:gotoHeroes() {
this.router.navigate(['/heroes']);
}
Route Parameters: Required or optional?
Use route parameters to specify a required parameter value within the route URL as you do when navigating to the
HeroDetailComponent in order to view the hero with id 15:localhost:3000/hero/15
You can also add optional information to a route request. For example, when returning to the heroes list from the hero detail view, it would be nice if the viewed hero was preselected in the list.

You'll implement this feature in a moment by including the viewed hero's
id in the URL as an optional parameter when returning from the HeroDetailComponent.
Optional information takes other forms. Search criteria are often loosely structured, e.g.,
name='wind*'. Multiple values are common—after='12/31/2015' & before='1/1/2017'—in no particular order—before='1/1/2017' & after='12/31/2015'— in a variety of formats—during='currentYear'.
These kinds of parameters don't fit easily in a URL path. Even if you could define a suitable URL token scheme, doing so greatly complicates the pattern matching required to translate an incoming URL to a named route.
Optional parameters are the ideal vehicle for conveying arbitrarily complex information during navigation. Optional parameters aren't involved in pattern matching and afford flexibility of expression.
The router supports navigation with optional parameters as well as required route parameters. Define optional parameters in a separate object after you define the required route parameters.
In general, prefer a required route parameter when the value is mandatory (for example, if necessary to distinguish one route path from another); prefer an optional parameter when the value is optional, complex, and/or multivariate.
Heroes list: optionally selecting a hero
When navigating to the
HeroDetailComponent you specified the required id of the hero-to-edit in the route parameter and made it the second item of the link parameters array.['/hero', hero.id] // { 15 }
The router embedded the
id value in the navigation URL because you had defined it as a route parameter with an :id placeholder token in the route path:{ path: 'hero/:id', component: HeroDetailComponent }
When the user clicks the back button, the
HeroDetailComponent constructs another link parameters arraywhich it uses to navigate back to the HeroListComponent.gotoHeroes() {
this.router.navigate(['/heroes']);
}
This array lacks a route parameter because you had no reason to send information to the
HeroListComponent.
Now you have a reason. You'd like to send the id of the current hero with the navigation request so that the
HeroListComponent can highlight that hero in its list. This is a nice-to-have feature; the list will display perfectly well without it.
Send the
id with an object that contains an optional id parameter. For demonstration purposes, there's an extra junk parameter (foo) in the object that the HeroListComponent should ignore. Here's the revised navigation statement:gotoHeroes(hero: Hero) {
let heroId = hero ? hero.id : null;
// Pass along the hero id if available
// so that the HeroList component can select that hero.
// Include a junk 'foo' property for fun.
this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
}
The application still works. Clicking "back" returns to the hero list view.
Look at the browser address bar.
It should look something like this, depending on where you run it:
localhost:3000/heroes;id=15;foo=foo
The
id value appears in the URL as (;id=15;foo=foo), not in the URL path. The path for the "Heroes" route doesn't have an :id token.
The optional route parameters are not separated by "?" and "&" as they would be in the URL query string. They are separated by semicolons ";" This is matrix URL notation—something you may not have seen before.
Matrix URL notation is an idea first introduced in a 1996 proposal by the founder of the web, Tim Berners-Lee.
Although matrix notation never made it into the HTML standard, it is legal and it became popular among browser routing systems as a way to isolate parameters belonging to parent and child routes. The Router is such a system and provides support for the matrix notation across browsers.
The syntax may seem strange to you but users are unlikely to notice or care as long as the URL can be emailed and pasted into a browser address bar as this one can.
Route parameters in the ActivatedRoute service
The list of heroes is unchanged. No hero row is highlighted.
The live example / download example does highlight the selected row because it demonstrates the final state of the application which includes the steps you're about to cover. At the moment this guide is describing the state of affairs prior to those steps.
The
HeroListComponent isn't expecting any parameters at all and wouldn't know what to do with them. You can change that.
Previously, when navigating from the
HeroListComponent to the HeroDetailComponent, you subscribed to the route parameter map Observable and made it available to the HeroDetailComponent in the ActivatedRouteservice. You injected that service in the constructor of the HeroDetailComponent.
This time you'll be navigating in the opposite direction, from the
HeroDetailComponent to the HeroListComponent.
First you extend the router import statement to include the
ActivatedRoute service symbol:import { ActivatedRoute, ParamMap } from '@angular/router';
Import the
switchMap operator to perform an operation on the Observable of route parameter map.import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
Then you inject the
ActivatedRoute in the HeroListComponent constructor.export class HeroListComponent implements OnInit {
heroes$: Observable<Hero[]>;
private selectedId: number;
constructor(
private service: HeroService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.heroes$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
// (+) before `params.get()` turns the string into a number
this.selectedId = +params.get('id');
return this.service.getHeroes();
})
);
}
}
The
ActivatedRoute.paramMap property is an Observable map of route parameters. The paramMap emits a new map of values that includes id when the user navigates to the component. In ngOnInit you subscribe to those values, set the selectedId, and get the heroes.
Update the template with a class binding. The binding adds the
selected CSS class when the comparison returns true and removes it when false. Look for it within the repeated
tag as shown here:template: `
HEROES
- ngFor="let hero of heroes$ | async"
[class.selected]="hero.id === selectedId">
<a [routerLink]="['/hero', hero.id]">
{{ hero.id }}{{ hero.name }}
</a>
`
When the user navigates from the heroes list to the "Magneta" hero and back, "Magneta" appears selected:

The optional
foo route parameter is harmless and continues to be ignored.Adding animations to the routed component
The heroes feature module is almost complete, but what is a feature without some smooth transitions?
This section shows you how to add some animations to the
HeroDetailComponent.
First import
BrowserAnimationsModule:import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
imports: [
BrowserAnimationsModule
Create an
animations.ts file in the root src/app/ folder. The contents look like this:import { animate, state, style, transition, trigger } from '@angular/animations';
// Component transition animations
export const slideInDownAnimation =
trigger('routeAnimation', [
state('*',
style({
opacity: 1,
transform: 'translateX(0)'
})
),
transition(':enter', [
style({
opacity: 0,
transform: 'translateX(-100%)'
}),
animate('0.2s ease-in')
]),
transition(':leave', [
animate('0.5s ease-out', style({
opacity: 0,
transform: 'translateY(100%)'
}))
])
]);
This file does the following:
- Imports the animation symbols that build the animation triggers, control state, and manage transitions between states.
- Exports a constant named
slideInDownAnimationset to an animation trigger namedrouteAnimation; animated components will refer to this name. - Specifies the wildcard state ,
*, that matches any animation state that the route component is in. - Defines two transitions, one to ease the component in from the left of the screen as it enters the application view (
:enter), the other to animate the component down as it leaves the application view (:leave).
You could create more triggers with different transitions for other route components. This trigger is sufficient for the current milestone.
Back in the
HeroDetailComponent, import the slideInDownAnimation from './animations.ts. Add the HostBinding decorator to the imports from @angular/core; you'll need it in a moment.
Then add three
@HostBinding properties to the class to set the animation and styles for the route component's element.@HostBinding('@routeAnimation') routeAnimation = true;
@HostBinding('style.display') display = 'block';
@HostBinding('style.position') position = 'absolute';
The
'@routeAnimation' passed to the first @HostBinding matches the name of the slideInDownAnimationtrigger, routeAnimation. Set the routeAnimation property to true because you only care about the :enterand :leave states.
The other two
@HostBinding properties style the display and position of the component.
The
HeroDetailComponent will ease in from the left when routed to and will slide down when navigating away.
Applying route animations to individual components works for a simple demo, but in a real life app, it is better to animate routes based on route paths.
Milestone 3 wrap up
You've learned how to do the following:
- Organize the app into feature areas.
- Navigate imperatively from one component to another.
- Pass information along in route parameters and subscribe to them in the component.
- Import the feature area NgModule into the
AppModule. - Apply animations to the route component.
After these changes, the folder structure looks like this:
router-sample
Here are the relevant files for this version of the sample application.
Comments
Post a Comment