This tutorial provides an extensive overview of the Angular router. In this tutorial, you will build upon a basic router configuration to explore features such as child routes, route parameters, lazy load NgModules, guard routes, and preloading data to improve the user experience.
For a working example of the final version of the app, see the .
This guide describes development of a multi-page routed sample application. Along the way, it highlights key features of the router such as:
id
while routing to the "Hero Detail").CanActivate
guard (checking route access).CanActivateChild
guard (checking child route access).CanDeactivate
guard (ask permission to discard unsaved changes).Resolve
guard (pre-fetching route data).NgModule
.CanLoad
guard (check before loading feature module assets).This guide proceeds as a sequence of milestones as if you were building the app step-by-step, but assumes you are familiar with basic Angular concepts. For a general introduction to angular, see the Getting Started. For a more in-depth overview, see the Tour of Heroes tutorial.
To complete this tutorial, you should have a basic understanding of the following concepts:
You might find the Tour of Heroes tutorial helpful, but it is not required.
The sample application for this tutorial helps the Hero Employment Agency find crises for heroes to solve.
The application has three main feature areas:
Try it by clicking on this live example link.
The app renders with a row of navigation buttons and the Heroes view with its list of heroes.
Select one hero and the app takes you to a hero editing screen.
Alter the name. Click the "Back" button and the app returns to the heroes list which displays the changed hero name. Notice that the name change took effect immediately.
Had you clicked the browser's back button instead of the app's "Back" button, the app would have returned you to the heroes list as well. Angular app navigation updates the browser history as normal web navigation does.
Now click the Crisis Center link for a list of ongoing crises.
Select a crisis and the application takes you to a crisis editing screen. The Crisis Detail appears in a child component on the same page, beneath the list.
Alter the name of a crisis. Notice that the corresponding name in the crisis list does not change.
Unlike Hero Detail, which updates as you type, Crisis Detail changes are temporary until you either save or discard them by pressing the "Save" or "Cancel" buttons. Both buttons navigate back to the Crisis Center and its list of crises.
Click the browser back button or the "Heroes" link to activate a dialog.
You can say "OK" and lose your changes or click "Cancel" and continue editing.
Behind this behavior is the router's CanDeactivate
guard. The guard gives you a chance to clean-up or ask the user's permission before navigating away from the current view.
The Admin
and Login
buttons illustrate other router capabilities covered later in the guide.
Begin with a basic version of the app that navigates between two empty views.
Generate a sample application with the Angular CLI.
ng new angular-router-sample
A router must be configured with a list of route definitions.
Each definition translates to a Route object which has two things: a path
, the URL path segment for this route; and a component
, the component associated with this route.
The router draws upon its registry of definitions when the browser URL changes or when application code tells the router to navigate along a route path.
The first route does the following:
When the browser's location URL changes to match the path segment /crisis-center
, then the router activates an instance of the CrisisListComponent
and displays its view.
When the application requests navigation to the path /crisis-center
, the router activates an instance of CrisisListComponent
, displays its view, and updates the browser's address location and history with the URL for that path.
The first configuration defines an array of two routes with minimal paths leading to the CrisisListComponent
and HeroListComponent
.
Generate the CrisisList
and HeroList
components so that the router has something to render.
ng generate component crisis-list
ng generate component hero-list
Replace the contents of each component with the sample HTML below.
<h2>CRISIS CENTER</h2> <p>Get your crisis here</p>
<h2>HEROES</h2> <p>Get your heroes here</p>
Router
and Routes
In order to use the Router
, you must first register the RouterModule
from the @angular/router
package. Define an array of routes, appRoutes
, and pass them to the RouterModule.forRoot()
method. The RouterModule.forRoot()
method returns a module that contains the configured Router
service provider, plus other providers that the routing library requires. Once the application is bootstrapped, the Router
performs the initial navigation based on the current browser URL.
Note: The
RouterModule.forRoot()
method is a pattern used to register application-wide providers. Read more about application-wide providers in the Singleton services guide.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { HeroListComponent } from './hero-list/hero-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, ], bootstrap: [ AppComponent ] }) export class AppModule { }
Adding the configured
RouterModule
to theAppModule
is sufficient for minimal route configurations. However, as the application grows, refactor the routing configuration into a separate file and create a Routing Module. A routing module is a special type ofService Module
dedicated to routing.
Registering the RouterModule.forRoot()
in the AppModule
imports
array makes the Router
service available everywhere in the application.
The root AppComponent
is the application shell. It has a title, a navigation bar with two links, and a router outlet where the router renders components.
The router outlet serves as a placeholder where the routed components are rendered.
The corresponding component template looks like this:
<h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet>
You've created two routes in the app so far, one to /crisis-center
and the other to /heroes
. Any other URL causes the router to throw an error and crash the app.
Add a wildcard route to intercept invalid URLs and handle them gracefully. A wildcard route has a path consisting of two asterisks. It matches every URL. Thus, the router selects this wildcard route if it can't match a route earlier in the configuration. A wildcard route can navigate to a custom "404 Not Found" component or redirect to an existing route.
The router selects the route with a first match wins strategy. Because a wildcard route is the least specific route, place it last in the route configuration.
To test this feature, add a button with a RouterLink
to the HeroListComponent
template and set the link to a non-existant route called "/sidekicks"
.
<h2>HEROES</h2> <p>Get your heroes here</p> <button routerLink="/sidekicks">Go to sidekicks</button>
The application fails if the user clicks that button because you haven't defined a "/sidekicks"
route yet.
Instead of adding the "/sidekicks"
route, define a wildcard
route and have it navigate to a PageNotFoundComponent
.
{ path: '**', component: PageNotFoundComponent }
Create the PageNotFoundComponent
to display when users visit invalid URLs.
ng generate component page-not-found
<h2>Page not found</h2>
Now when the user visits /sidekicks
, or any other invalid URL, the browser displays "Page not found". The browser address bar continues to point to the invalid URL.
When the application launches, the initial URL in the browser bar is by default:
localhost:4200
That doesn't match any of the hard-coded routes which means the router falls through to the wildcard route and displays the PageNotFoundComponent
.
The application needs a default route to a valid page. The default page for this app is the list of heroes. The app should navigate there as if the user clicked the "Heroes" link or pasted localhost:4200/heroes
into the address bar.
Add a redirect
route that translates the initial relative URL (''
) to the desired default path (/heroes
).
Add the default route somewhere above the wildcard route. It's just above the wildcard route in the following excerpt showing the complete appRoutes
for this milestone.
const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
The browser address bar shows .../heroes
as if you'd navigated there directly.
A redirect route requires a pathMatch
property to tell the router how to match a URL to the path of a route. In this app, the router should select the route to the HeroListComponent
only when the entire URL matches ''
, so set the pathMatch
value to 'full'
.
Technically, pathMatch = 'full'
results in a route hit when the remaining, unmatched segments of the URL match ''
. In this example, the redirect is in a top level route so the remaining URL and the entire URL are the same thing.
The other possible pathMatch
value is 'prefix'
which tells the router to match the redirect route when the remaining URL begins with the redirect route's prefix path. This doesn't apply to this sample app because if the pathMatch
value were 'prefix'
, every URL would match ''
.
Try setting it to 'prefix'
and clicking the Go to sidekicks
button. Since that's a bad URL, you should see the "Page not found" page. Instead, you're still on the "Heroes" page. Enter a bad URL in the browser address bar. You're instantly re-routed to /heroes
. Every URL, good or bad, that falls through to this route definition is a match.
The default route should redirect to the HeroListComponent
only when the entire url is ''
. Remember to restore the redirect to pathMatch = 'full'
.
Learn more in Victor Savkin's post on redirects.
Your sample app can switch between two views when the user clicks a link.
Milestone 1 has covered how to do the following:
routerLink
and routerLinkActive
directives.router-outlet
to the shell template where views are displayed.RouterModule.forRoot()
.wildcard
route.The starter app's structure looks like this:
angular-router-sample src app crisis-list crisis-list.component.css crisis-list.component.html crisis-list.component.ts hero-list hero-list.component.css hero-list.component.html hero-list.component.ts page-not-found page-not-found.component.css page-not-found.component.html page-not-found.component.ts app.component.css app.component.html app.component.ts app.module.ts main.ts index.html styles.css tsconfig.json node_modules ... package.json
Here are the files in this milestone.
<h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet>
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { HeroListComponent } from './hero-list/hero-list.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
<h2>HEROES</h2> <p>Get your heroes here</p> <button routerLink="/sidekicks">Go to sidekicks</button>
<h2>CRISIS CENTER</h2> <p>Get your crisis here</p>
<h2>Page not found</h2>
<html lang="en"> <head> <!-- Set the base href --> <base href="/"> <title>Angular Router</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <app-root></app-root> </body> </html>
Routing module
This milestone shows you how to configure a special-purpose module called a Routing Module, which holds your app's routing configuration.
The Routing Module has several characteristics:
The sample routing application does not include routing by default. When you use the Angular CLI to create a project that does use routing, set the --routing
option for the project or app, and for each NgModule. When you create or initialize a new project (using the CLI ng new
command) or a new app (using the ng generate app
command), specify the --routing
option. This tells the CLI to include the @angular/router
npm package and create a file named app-routing.module.ts
. You can then use routing in any NgModule that you add to the project or app.
For example, the following command generates an NgModule that can use routing.
ng generate module my-module --routing
This creates a separate file named my-module-routing.module.ts
to store the NgModule's routes. The file includes an empty Routes
object that you can fill with routes to different components and NgModules.
Create an AppRouting
module in the /app
folder to contain the routing configuration.
ng generate module app-routing --module app --flat
Import the CrisisListComponent
, HeroListComponent
, and PageNotFoundComponent
symbols just like you did in the app.module.ts
. Then move the Router
imports and routing configuration, including RouterModule.forRoot()
, into this routing module.
Re-export the Angular RouterModule
by adding it to the module exports
array. By re-exporting the RouterModule
here, the components declared in AppModule
have access to router directives such as RouterLink
and RouterOutlet
.
After these steps, the file should look like this.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { HeroListComponent } from './hero-list/hero-list.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
Next, update the app.module.ts
file by removing RouterModule.forRoot
in the imports
array.
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 { CrisisListComponent } from './crisis-list/crisis-list.component'; import { HeroListComponent } from './hero-list/hero-list.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; @NgModule({ imports: [ BrowserModule, FormsModule, AppRoutingModule ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
Later, this guide shows you how to create multiple routing modules and import those routing modules in the correct order.
The application continues to work just the same, and you can use AppRoutingModule
as the central place to maintain future routing configuration.
The routing module, often called the AppRoutingModule
, replaces the routing configuration in the root or feature module.
The routing module is helpful as your app grows and when the configuration includes specialized guard and resolver services.
Some developers skip the routing module when the configuration is minimal and merge the routing configuration directly into the companion module (for example, AppModule
).
Most apps should implement a routing module for consistency. It keeps the code clean when configuration becomes complex. It makes testing the feature module easier. Its existence calls attention to the fact that a module is routed. It is where developers expect to find and expand routing configuration.
This milestone covers the following:
This sample app recreates the heroes feature in the "Services" section of the Tour of Heroes tutorial, and reuses much of the code from the .
A typical application has multiple feature areas, each dedicated to a particular business purpose with its own folder.
This section shows you how refactor the app into different feature modules, import them into the main module and navigate among them.
Follow these steps:
HeroesModule
with routing in the heroes folder and register it with the root AppModule
.ng generate module heroes/heroes --module app --flat --routing
Move the placeholder hero-list
folder that's in the app
folder into the heroes
folder.
Copy the contents of the heroes/heroes.component.html
from the "Services" tutorial into the hero-list.component.html
template.
<h2>
to <h2>HEROES</h2>
.<app-hero-detail>
component at the bottom of the template.Copy the contents of the heroes/heroes.component.css
from the live example into the hero-list.component.css
file.
Copy the contents of the heroes/heroes.component.ts
from the live example into the hero-list.component.ts
file.
HeroListComponent
.selector
to app-hero-list
.Selectors are not required for routed components because components are dynamically inserted when the page is rendered. However, they are useful for identifying and targeting them in your HTML element tree.
hero-detail
folder, the hero.ts
, hero.service.ts
, and mock-heroes.ts
files into the heroes
subfolder.message.service.ts
into the src/app
folder.message.service
in the hero.service.ts
file.Next, update the HeroesModule
metadata.
HeroDetailComponent
and HeroListComponent
to the declarations
array in the HeroesModule
.import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HeroListComponent } from './hero-list/hero-list.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { HeroesRoutingModule } from './heroes-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesRoutingModule ], declarations: [ HeroListComponent, HeroDetailComponent ] }) export class HeroesModule {}
The hero management file structure is as follows:
src/app/heroes hero-detail hero-detail.component.css hero-detail.component.html hero-detail.component.ts hero-list hero-list.component.css hero-list.component.html hero-list.component.ts hero.service.ts hero.ts heroes-routing.module.ts heroes.module.ts mock-heroes.ts
The heroes feature has two interacting components, the hero list and the hero detail. When you navigate to list view, it gets a list of heroes and displays them. When you click on a hero, the detail view has to display that particular hero.
You tell the detail view which hero to display by including the selected hero's id in the route URL.
Import the hero components from their new locations in the src/app/heroes/
folder and define the two hero routes.
Now that you have routes for the Heroes
module, register them with the Router
via the RouterModule
as you did in the AppRoutingModule
, with an important 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 rootAppRoutingModule
(or theAppModule
if that's where you register top level application routes). In any other module, you must call theRouterModule.forChild()
method to register additional routes.
The updated HeroesRoutingModule
looks like this:
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list/hero-list.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent }, { path: 'hero/:id', component: HeroDetailComponent } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroesRoutingModule { }
Consider giving each feature module its own route configuration file. Though the feature routes are currently minimal, routes have a tendency to grow more complex even in small apps.
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.
Remove the HeroListComponent
import and the /heroes
route from the app-routing.module.ts
.
Leave the default and the wildcard routes as these are still in use at the top level of the application.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; // import { HeroListComponent } from './hero-list/hero-list.component'; // <-- delete this line import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, // { path: 'heroes', component: HeroListComponent }, // <-- delete this line { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
Because the HeroesModule
now provides the HeroListComponent
, remove it from the AppModule
's declarations
array. Now that you have a separate HeroesModule
, you can evolve the hero feature with more components and different routes.
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/crisis-list.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, AppRoutingModule ], declarations: [ AppComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
Notice that in the module imports
array, the AppRoutingModule
is last and comes after the HeroesModule
.
imports: [ BrowserModule, FormsModule, HeroesModule, AppRoutingModule ],
The order of route configuration is important because 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".
Each routing module augments the route configuration in the order of import. If you listed AppRoutingModule
first, the wildcard route would be registered before the hero routes. The wildcard route—which matches every URL—would intercept the attempt to navigate to a hero route.
Reverse the routing modules to see a click of the heroes link resulting in "Page not found". Learn about inspecting the runtime router configuration below.
Return to the HeroesRoutingModule
and look at the route definitions again. The route to HeroDetailComponent
has an :id
token in the path.
{ path: 'hero/:id', component: HeroDetailComponent }
The :id
token creates a slot in the path for a Route Parameter. In this case, this configuration causes the router to 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:4200/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.
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.
<a [routerLink]="['/hero', hero.id]">
The router composes the destination URL from the array like this: localhost:4200/hero/15
.
The router extracts the route parameter (id:15
) from the URL and supplies it to the HeroDetailComponent
via the ActivatedRoute
service.
Activated Route
in actionImport the Router
, ActivatedRoute
, and ParamMap
tokens from the router package.
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';
Add the services as private variables to the constructor so that Angular injects them (makes them visible to the component).
constructor( private route: ActivatedRoute, private router: Router, private service: HeroService ) {}
In the ngOnInit()
method, 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'))) ); }
When the map changes, paramMap
gets 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.
The switchMap
operator does two things. It flattens the Observable<Hero>
that HeroService
returns and cancels previous pending 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
.
AsyncPipe
handles the observable subscription and the component's hero
property will be (re)set with the retrieved hero.
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 |
get(name) | Returns the parameter name value (a |
getAll(name) | Returns a |
keys | Returns a |
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.
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 wouldn't want the router to remove the current HeroDetailComponent
instance from the DOM only to re-create it for the next id
as this would re-render the view. For better UX, the router re-uses the same component instance and updates the parameter.
Since ngOnInit()
is only called once per component instantiation, you can detect when the route parameters change from within the same instance using the observable paramMap
property.
When subscribing to an observable in a component, you almost always unsubscribe when the component is destroyed.
However,
ActivatedRoute
observables are among the exceptions becauseActivatedRoute
and its observables are insulated from theRouter
itself. TheRouter
destroys a routed component when it is no longer needed along with the injectedActivatedRoute
.
snapshot
: the no-observable alternativeThis 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 be re-used, you can use snapshot
.
route.snapshot
provides the initial value of the route parameter map. You can access the parameters directly without subscribing or adding observable operators as in the following:
ngOnInit() { const id = this.route.snapshot.paramMap.get('id'); this.hero$ = this.service.getHero(id); }
snapshot
only gets the initial value of the parameter map with this technique. Use the observableparamMap
approach if there's a possibility that the router could re-use the component. This tutorial sample app uses with the observableparamMap
.
The HeroDetailComponent
"Back" button uses the 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']); }
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:4200/hero/15
You can also add optional information to a route request. For example, when returning to the hero-detail.component.ts
list from the hero detail view, it would be nice if the viewed hero were preselected in the list.
You implement this feature by including the viewed hero's id
in the URL as an optional parameter when returning from the HeroDetailComponent
.
Optional information can also include other forms such as:
name='wind*'
.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'
.As these kinds of parameters don't fit easily in a URL path, you can use optional parameters 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, use a required route parameter when the value is mandatory (for example, if necessary to distinguish one route path from another); and an optional parameter when the value is optional, complex, and/or multivariate.
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.
<a [routerLink]="['/hero', hero.id]">
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 array which it uses to navigate back to the HeroListComponent
.
gotoHeroes() { this.router.navigate(['/heroes']); }
This array lacks a route parameter because previously you didn't need to send information to the HeroListComponent
.
Now, send the id
of the current hero with the navigation request so that the HeroListComponent
can highlight that hero in its list.
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) { const 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:4200/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.
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. As such, the Router provides support for the matrix notation across browsers.
ActivatedRoute
serviceIn its current state of development, the list of heroes is unchanged. No hero row is highlighted.
The HeroListComponent
needs code that expects parameters.
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 ActivatedRoute
service. 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, extend the router import statement to include the ActivatedRoute
service symbol:
import { ActivatedRoute } 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';
Inject the ActivatedRoute
in the HeroListComponent
constructor.
export class HeroListComponent implements OnInit { heroes$: Observable<Hero[]>; selectedId: number; constructor( private service: HeroService, private route: ActivatedRoute ) {} ngOnInit() { this.heroes$ = this.route.paramMap.pipe( switchMap(params => { // (+) 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 <li>
tag as shown here:
<h2>HEROES</h2> <ul class="heroes"> <li *ngFor="let hero of heroes$ | async" [class.selected]="hero.id === selectedId"> <a [routerLink]="['/hero', hero.id]"> <span class="badge">{{ hero.id }}</span>{{ hero.name }} </a> </li> </ul> <button routerLink="/sidekicks">Go to sidekicks</button>
Add some styles to apply when the list item is selected.
.heroes li.selected { background-color: #CFD8DC; color: white; } .heroes li.selected:hover { background-color: #BBD8DC; }
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 the router continues to ignore it.
This section shows you how to add some animations to the HeroDetailComponent
.
First, import the BrowserAnimationsModule
and add it to the imports
array:
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ imports: [ BrowserAnimationsModule, ], })
Next, add a data
object to the routes for HeroListComponent
and HeroDetailComponent
. Transitions are based on states
and you use the animation
data from the route to provide a named animation state
for the transitions.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list/hero-list.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent, data: { animation: 'heroes' } }, { path: 'hero/:id', component: HeroDetailComponent, data: { animation: 'hero' } } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroesRoutingModule { }
Create an animations.ts
file in the root src/app/
folder. The contents look like this:
import { trigger, animateChild, group, transition, animate, style, query } from '@angular/animations'; // Routable animations export const slideInAnimation = trigger('routeAnimation', [ transition('heroes <=> hero', [ style({ position: 'relative' }), query(':enter, :leave', [ style({ position: 'absolute', top: 0, left: 0, width: '100%' }) ]), query(':enter', [ style({ left: '-100%'}) ]), query(':leave', animateChild()), group([ query(':leave', [ animate('300ms ease-out', style({ left: '100%'})) ]), query(':enter', [ animate('300ms ease-out', style({ left: '0%'})) ]) ]), query(':enter', animateChild()), ]) ]);
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 slideInAnimation
set to an animation trigger named routeAnimation
.
Defines one transition when switching back and forth from the heroes
and hero
routes to ease the component in from the left of the screen as it enters the application view (:enter
), the other to animate the component to the right as it leaves the application view (:leave
).
Back in the AppComponent
, import the RouterOutlet
token from the @angular/router
package and the slideInAnimation
from './animations.ts
.
Add an animations
array to the @Component
metadata that contains the slideInAnimation
.
import { RouterOutlet } from '@angular/router'; import { slideInAnimation } from './animations'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.css'], animations: [ slideInAnimation ] })
In order to use the routable animations, wrap the RouterOutlet
inside an element, use the @routeAnimation
trigger, and bind it to the element.
For the @routeAnimation
transitions to key off states, provide it with the data
from the ActivatedRoute
. The RouterOutlet
is exposed as an outlet
template variable, so you bind a reference to the router outlet. This example uses a variable of routerOutlet
.
<h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div>
The @routeAnimation
property is bound to the getAnimationData()
with the provided routerOutlet
reference, so the next step is to define that function in the AppComponent
. The getAnimationData()
function returns the animation property from the data
provided through the ActivatedRoute
. The animation
property matches the transition
names you used in the slideInAnimation
defined in animations.ts
.
export class AppComponent { getAnimationData(outlet: RouterOutlet) { return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation; } }
When switching between the two routes, the HeroDetailComponent
and HeroListComponent
now ease in from the left when routed to and will slide to the right when navigating away.
This section has covered the following:
AppModule
.After these changes, the folder structure is as follows:
angular-router-sample src app crisis-list crisis-list.component.css crisis-list.component.html crisis-list.component.ts heroes hero-detail hero-detail.component.css hero-detail.component.html hero-detail.component.ts hero-list hero-list.component.css hero-list.component.html hero-list.component.ts hero.service.ts hero.ts heroes-routing.module.ts heroes.module.ts mock-heroes.ts page-not-found page-not-found.component.css page-not-found.component.html page-not-found.component.ts animations.ts app.component.css app.component.html app.component.ts app.module.ts app-routing.module.ts main.ts message.service.ts index.html styles.css tsconfig.json node_modules ... package.json
Here are the relevant files for this version of the sample application.
import { trigger, animateChild, group, transition, animate, style, query } from '@angular/animations'; // Routable animations export const slideInAnimation = trigger('routeAnimation', [ transition('heroes <=> hero', [ style({ position: 'relative' }), query(':enter, :leave', [ style({ position: 'absolute', top: 0, left: 0, width: '100%' }) ]), query(':enter', [ style({ left: '-100%'}) ]), query(':leave', animateChild()), group([ query(':leave', [ animate('300ms ease-out', style({ left: '100%'})) ]), query(':enter', [ animate('300ms ease-out', style({ left: '0%'})) ]) ]), query(':enter', animateChild()), ]) ]);
<h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div>
import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { slideInAnimation } from './animations'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.css'], animations: [ slideInAnimation ] }) export class AppComponent { getAnimationData(outlet: RouterOutlet) { return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation; } }
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, HeroesModule, AppRoutingModule ], declarations: [ AppComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; /* . . . */ import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, /* . . . */ { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
/* HeroListComponent's private CSS styles */ .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { position: relative; cursor: pointer; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes a { color: #888; text-decoration: none; position: relative; display: block; } .heroes a:hover { color:#607D8B; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; min-width: 16px; text-align: right; margin-right: .8em; border-radius: 4px 0 0 4px; } button { background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; font-family: Arial; } button:hover { background-color: #cfd8dc; } button.delete { position: relative; left: 194px; top: -32px; background-color: gray !important; color: white; } .heroes li.selected { background-color: #CFD8DC; color: white; } .heroes li.selected:hover { background-color: #BBD8DC; }
<h2>HEROES</h2> <ul class="heroes"> <li *ngFor="let hero of heroes$ | async" [class.selected]="hero.id === selectedId"> <a [routerLink]="['/hero', hero.id]"> <span class="badge">{{ hero.id }}</span>{{ hero.name }} </a> </li> </ul> <button routerLink="/sidekicks">Go to sidekicks</button>
// TODO: Feature Componetized like CrisisCenter import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HeroService } from '../hero.service'; import { Hero } from '../hero'; @Component({ selector: 'app-hero-list', templateUrl: './hero-list.component.html', styleUrls: ['./hero-list.component.css'] }) export class HeroListComponent implements OnInit { heroes$: Observable<Hero[]>; selectedId: number; constructor( private service: HeroService, private route: ActivatedRoute ) {} ngOnInit() { this.heroes$ = this.route.paramMap.pipe( switchMap(params => { // (+) before `params.get()` turns the string into a number this.selectedId = +params.get('id'); return this.service.getHeroes(); }) ); } }
<h2>HEROES</h2> <div *ngIf="hero$ | async as hero"> <h3>"{{ hero.name }}"</h3> <div> <label>Id: </label>{{ hero.id }}</div> <div> <label>Name: </label> <input [(ngModel)]="hero.name" placeholder="name"/> </div> <p> <button (click)="gotoHeroes(hero)">Back</button> </p> </div>
import { switchMap } from 'rxjs/operators'; import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { HeroService } from '../hero.service'; import { Hero } from '../hero'; @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit { hero$: Observable<Hero>; constructor( private route: ActivatedRoute, private router: Router, private service: HeroService ) {} ngOnInit() { this.hero$ = this.route.paramMap.pipe( switchMap((params: ParamMap) => this.service.getHero(params.get('id'))) ); } gotoHeroes(hero: Hero) { const 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' }]); } } /* this.router.navigate(['/superheroes', { id: heroId, foo: 'foo' }]); */
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; import { MessageService } from '../message.service'; @Injectable({ providedIn: 'root', }) export class HeroService { constructor(private messageService: MessageService) { } getHeroes(): Observable<Hero[]> { // TODO: send the message _after_ fetching the heroes this.messageService.add('HeroService: fetched heroes'); return of(HEROES); } getHero(id: number | string) { return this.getHeroes().pipe( // (+) before `id` turns the string into a number map((heroes: Hero[]) => heroes.find(hero => hero.id === +id)) ); } }
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HeroListComponent } from './hero-list/hero-list.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { HeroesRoutingModule } from './heroes-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesRoutingModule ], declarations: [ HeroListComponent, HeroDetailComponent ] }) export class HeroesModule {}
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list/hero-list.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent, data: { animation: 'heroes' } }, { path: 'hero/:id', component: HeroDetailComponent, data: { animation: 'hero' } } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroesRoutingModule { }
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MessageService { messages: string[] = []; add(message: string) { this.messages.push(message); } clear() { this.messages = []; } }
This section shows you how to add child routes and use relative routing in your app.
To add more features to the app's current crisis center, take similar steps as for the heroes feature:
crisis-center
subfolder in the src/app
folder.app/heroes
into the new crisis-center
folder.crisis-center.module.ts
and crisis-center-routing.module.ts
.Use mock crises instead of mock heroes:
import { Crisis } from './crisis'; export const CRISES: Crisis[] = [ { id: 1, name: 'Dragon Burning Cities' }, { id: 2, name: 'Sky Rains Great White Sharks' }, { id: 3, name: 'Giant Asteroid Heading For Earth' }, { id: 4, name: 'Procrastinators Meeting Delayed Again' }, ];
The resulting crisis center is a foundation for introducing a new concept—child routing. You can leave Heroes in its current state as a contrast with the Crisis Center.
In keeping with the Separation of Concerns principle, changes to the Crisis Center don't affect the
AppModule
or any other feature's component.
This section shows you how to organize the crisis center to conform to the following recommended pattern for Angular applications:
If your app had many feature areas, the app component trees might look like this:
Generate a CrisisCenter
component in the crisis-center
folder:
ng generate component crisis-center/crisis-center
Update the component template with the following markup:
<h2>CRISIS CENTER</h2> <router-outlet></router-outlet>
The CrisisCenterComponent
has the following in common with the AppComponent
:
AppComponent
is the root of the entire application.AppComponent
is a shell to manage the high-level workflow.Like most shells, the CrisisCenterComponent
class is minimal because it has no business logic, and its template has no links, just a title and <router-outlet>
for the crisis center child component.
As a host page for the "Crisis Center" feature, generate a CrisisCenterHome
component in the crisis-center
folder.
ng generate component crisis-center/crisis-center-home
Update the template with a welcome message to the Crisis Center
.
<p>Welcome to the Crisis Center</p>
Update the crisis-center-routing.module.ts
you renamed after copying it from heroes-routing.module.ts
file. This time, you define child routes within the parent crisis-center
route.
const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ];
Notice that the parent crisis-center
route has a children
property with a single route containing the CrisisListComponent
. The CrisisListComponent
route also has a children
array with two routes.
These two routes navigate to the crisis center child components, CrisisCenterHomeComponent
and CrisisDetailComponent
, respectively.
There are important differences in the way the router treats child routes.
The router displays the components of these routes in the RouterOutlet
of the CrisisCenterComponent
, not in the RouterOutlet
of the AppComponent
shell.
The CrisisListComponent
contains the crisis list and a RouterOutlet
to display the Crisis Center Home
and Crisis Detail
route components.
The Crisis Detail
route is a child of the Crisis List
. The router reuses components by default, so the Crisis Detail
component will be re-used as you select different crises. In contrast, back in the Hero Detail
route, the component was recreated each time you selected a different hero from the list of heroes.
At the top level, paths that begin with /
refer to the root of the application. But child routes extend the path of the parent route. With each step down the route tree, you add a slash followed by the route path, unless the path is empty.
Apply that logic to navigation within the crisis center for which the parent path is /crisis-center
.
To navigate to the CrisisCenterHomeComponent
, the full URL is /crisis-center
(/crisis-center
+ ''
+ ''
).
To navigate to the CrisisDetailComponent
for a crisis with id=2
, the full URL is /crisis-center/2
(/crisis-center
+ ''
+ '/2'
).
The absolute URL for the latter example, including the localhost
origin, is as follows:
localhost:4200/crisis-center/2
Here's the complete crisis-center-routing.module.ts
file with its imports.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
AppModule
routesAs with the HeroesModule
, you must add the CrisisCenterModule
to the imports
array of the AppModule
before the AppRoutingModule
:
import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CrisisCenterRoutingModule } from './crisis-center-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, CrisisCenterRoutingModule ], declarations: [ CrisisCenterComponent, CrisisListComponent, CrisisCenterHomeComponent, CrisisDetailComponent ] }) export class CrisisCenterModule {}
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesModule, CrisisCenterModule, AppRoutingModule ], declarations: [ AppComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
Remove the initial crisis center route from the app-routing.module.ts
because now the HeroesModule
and the CrisisCenter
modules provide the feature routes.
The app-routing.module.ts
file retains the top-level application routes such as the default and wildcard routes.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
While building out the crisis center feature, you navigated to the crisis detail route using an absolute path that begins with a slash.
The router matches such absolute paths to routes starting from the top of the route configuration.
You could continue to use absolute paths like this to navigate inside the Crisis Center feature, but that pins the links to the parent routing structure. If you changed the parent /crisis-center
path, you would have to change the link parameters array.
You can free the links from this dependency by defining paths that are relative to the current URL segment. Navigation within the feature area remains intact even if you change the parent route path to the feature.
The router supports directory-like syntax in a link parameters list to help guide route name lookup:
./
orno leading slash
is relative to the current level.
../
to go up one level in the route path.You can combine relative navigation syntax with an ancestor path. If you must navigate to a sibling route, you could use the
../<sibling>
convention to go up one level, then over and down the sibling route path.
To navigate a relative path with the Router.navigate
method, you must supply the ActivatedRoute
to give the router knowledge of where you are in the current route tree.
After the link parameters array, add an object with a relativeTo
property set to the ActivatedRoute
. The router then calculates the target URL based on the active route's location.
Always specify the complete absolute path when calling router's
navigateByUrl()
method.
You've already injected the ActivatedRoute
that you need to compose the relative navigation path.
When using a RouterLink
to navigate instead of the Router
service, you'd use the same link parameters array, but you wouldn't provide the object with the relativeTo
property. The ActivatedRoute
is implicit in a RouterLink
directive.
Update the gotoCrises()
method of the CrisisDetailComponent
to navigate back to the Crisis Center list using relative path navigation.
// Relative navigation back to the crises this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
Notice that the path goes up a level using the ../
syntax. If the current crisis id
is 3
, the resulting path back to the crisis list is /crisis-center/;id=3;foo=foo
.
You decide to give users a way to contact the crisis center. When a user clicks a "Contact" button, you want to display a message in a popup view.
The popup should stay open, even when switching between pages in the application, until the user closes it by sending the message or canceling. Clearly you can't put the popup in the same outlet as the other pages.
Until now, you've defined a single outlet and you've nested child routes under that outlet to group routes together. The router only supports one primary unnamed outlet per template.
A template can also have any number of named outlets. Each named outlet has its own set of routes with their own components. Multiple outlets can display different content, determined by different routes, all at the same time.
Add an outlet named "popup" in the AppComponent
, directly below the unnamed outlet.
<div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet>
That's where a popup will go, once you learn how to route a popup component to it.
Named outlets are the targets of secondary routes.
Secondary routes look like primary routes and you configure them the same way. They differ in a few key respects.
Generate a new component to compose the message.
ng generate component compose-message
It displays a short form with a header, an input box for the message, and two buttons, "Send" and "Cancel".
Here's the component, its template and styles:
:host { position: relative; bottom: 10%; }
<h3>Contact Crisis Center</h3> <div *ngIf="details"> {{ details }} </div> <div> <div> <label>Message: </label> </div> <div> <textarea [(ngModel)]="message" rows="10" cols="35" [disabled]="sending"></textarea> </div> </div> <p *ngIf="!sending"> <button (click)="send()">Send</button> <button (click)="cancel()">Cancel</button> </p>
import { Component, HostBinding } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-compose-message', templateUrl: './compose-message.component.html', styleUrls: ['./compose-message.component.css'] }) export class ComposeMessageComponent { details: string; message: string; sending = false; constructor(private router: Router) {} send() { this.sending = true; this.details = 'Sending Message...'; setTimeout(() => { this.sending = false; this.closePopup(); }, 1000); } cancel() { this.closePopup(); } closePopup() { // Providing a `null` value to the named outlet // clears the contents of the named outlet this.router.navigate([{ outlets: { popup: null }}]); } }
It looks similar to any other component in this guide, but there are two key differences.
Note that the send()
method simulates latency by waiting a second before "sending" the message and closing the popup.
The closePopup()
method closes the popup view by navigating to the popup outlet with a null
which the section on clearing secondary routes covers.
Open the AppRoutingModule
and add a new compose
route to the appRoutes
.
{ path: 'compose', component: ComposeMessageComponent, outlet: 'popup' },
In addition to the path
and component
properties, there's a new property called outlet
, which is set to 'popup'
. This route now targets the popup outlet and the ComposeMessageComponent
will display there.
To give users a way to open the popup, add a "Contact" link to the AppComponent
template.
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
Although the compose
route is configured to the "popup" outlet, that's not sufficient for connecting the route to a RouterLink
directive. You have to specify the named outlet in a link parameters array and bind it to the RouterLink
with a property binding.
The link parameters array contains an object with a single outlets
property whose value is another object keyed by one (or more) outlet names. In this case there is only the "popup" outlet property and its value is another link parameters array that specifies the compose
route.
In other words, when the user clicks this link, the router displays the component associated with the compose
route in the popup
outlet.
This
outlets
object within an outer object was unnecessary when there was only one route and one unnamed outlet.The router assumed that your route specification targeted the unnamed primary outlet and created these objects for you.
Routing to a named outlet has revealed a router feature: you can target multiple outlets with multiple routes in the same
RouterLink
directive.
Navigate to the Crisis Center and click "Contact". you should see something like the following URL in the browser address bar.
http://.../crisis-center(popup:compose)
The relevant part of the URL follows the ...
:
crisis-center
is the primary navigation.popup
), a colon
separator, and the secondary route path (compose
).Click the Heroes link and look at the URL again.
http://.../heroes(popup:compose)
The primary navigation part has changed; the secondary route is the same.
The router is keeping track of two separate branches in a navigation tree and generating a representation of that tree in the URL.
You can add many more outlets and routes, at the top level and in nested levels, creating a navigation tree with many branches and the router will generate the URLs to go with it.
You can tell the router to navigate an entire tree at once by filling out the outlets
object and then pass that object inside a link parameters array to the router.navigate
method.
Like regular outlets, secondary outlets persists until you navigate away to a new component.
Each secondary outlet has its own navigation, independent of the navigation driving the primary outlet. Changing a current route that displays in the primary outlet has no effect on the popup outlet. That's why the popup stays visible as you navigate among the crises and heroes.
The closePopup()
method again:
closePopup() { // Providing a `null` value to the named outlet // clears the contents of the named outlet this.router.navigate([{ outlets: { popup: null }}]); }
Clicking the "send" or "cancel" buttons clears the popup view. The closePopup()
function navigates imperatively with the Router.navigate()
method, passing in a link parameters array.
Like the array bound to the Contact RouterLink
in the AppComponent
, this one includes an object with an outlets
property. The outlets
property value is another object with outlet names for keys. The only named outlet is 'popup'
.
This time, the value of 'popup'
is null
. That's not a route, but it is a legitimate value. Setting the popup RouterOutlet
to null
clears the outlet and removes the secondary popup route from the current URL.
At the moment, any user can navigate anywhere in the application anytime, but sometimes you need to control access to different parts of your app for various reasons. Some of which may include the following:
You add guards to the route configuration to handle these scenarios.
A guard's return value controls the router's behavior:
true
, the navigation process continues.false
, the navigation process stops and the user stays put.UrlTree
, the current navigation cancels and a new navigation is initiated to the UrlTree
returned.Note: The guard can also tell the router to navigate elsewhere, effectively canceling the current navigation. When doing so inside a guard, the guard should return
false
;
The guard might return its boolean answer synchronously. But in many cases, the guard can't produce an answer synchronously. The guard could ask the user a question, save changes to the server, or fetch fresh data. These are all asynchronous operations.
Accordingly, a routing guard can return an Observable<boolean>
or a Promise<boolean>
and the router will wait for the observable to resolve to true
or false
.
Note: The observable provided to the
Router
must also complete. If the observable does not complete, the navigation does not continue.
The router supports multiple guard interfaces:
CanActivate
to mediate navigation to a route.
CanActivateChild
to mediate navigation to a child route.
CanDeactivate
to mediate navigation away from the current route.
Resolve
to perform route data retrieval before route activation.
CanLoad
to mediate navigation to a feature module loaded asynchronously.
You can have multiple guards at every level of a routing hierarchy. The router checks the CanDeactivate
and CanActivateChild
guards first, from the deepest child route to the top. Then it checks the CanActivate
guards from the top down to the deepest child route. If the feature module is loaded asynchronously, the CanLoad
guard is checked before the module is loaded. If any guard returns false, pending guards that have not completed will be canceled, and the entire navigation is canceled.
There are several examples over the next few sections.
CanActivate
: requiring authenticationApplications often restrict access to a feature area based on who the user is. You could permit access only to authenticated users or to users with a specific role. You might block or limit access until the user's account is activated.
The CanActivate
guard is the tool to manage these navigation business rules.
This section guides you through extending the crisis center with some new administrative features. Start by adding a new feature module named AdminModule
.
Generate an admin
folder with a feature module file and a routing configuration file.
ng generate module admin --routing
Next, generate the supporting components.
ng generate component admin/admin-dashboard
ng generate component admin/admin
ng generate component admin/manage-crises
ng generate component admin/manage-heroes
The admin feature file structure looks like this:
src/app/admin admin admin.component.css admin.component.html admin.component.ts admin-dashboard admin-dashboard.component.css admin-dashboard.component.html admin-dashboard.component.ts manage-crises manage-crises.component.css manage-crises.component.html manage-crises.component.ts manage-heroes manage-heroes.component.css manage-heroes.component.html manage-heroes.component.ts admin.module.ts admin-routing.module.ts
The admin feature module contains the AdminComponent
used for routing within the feature module, a dashboard route and two unfinished components to manage crises and heroes.
<h3>ADMIN</h3> <nav> <a routerLink="./" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Dashboard</a> <a routerLink="./crises" routerLinkActive="active">Manage Crises</a> <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a> </nav> <router-outlet></router-outlet>
<p>Dashboard</p>
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AdminComponent } from './admin/admin.component'; import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component'; import { ManageCrisesComponent } from './manage-crises/manage-crises.component'; import { ManageHeroesComponent } from './manage-heroes/manage-heroes.component'; import { AdminRoutingModule } from './admin-routing.module'; @NgModule({ imports: [ CommonModule, AdminRoutingModule ], declarations: [ AdminComponent, AdminDashboardComponent, ManageCrisesComponent, ManageHeroesComponent ] }) export class AdminModule {}
<p>Manage your crises here</p>
<p>Manage your heroes here</p>
Although the admin dashboard
RouterLink
only contains a relative slash without an additional URL segment, it is a match to any route within the admin feature area. You only want theDashboard
link to be active when the user visits that route. Adding an additional binding to theDashboard
routerLink,[routerLinkActiveOptions]="{ exact: true }"
, marks the./
link as active when the user navigates to the/admin
URL and not when navigating to any of the child routes.
The initial admin routing configuration:
const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
The child route under the AdminComponent
has a path
and a children
property but it's not using a component
. This defines a component-less route.
To group the Crisis Center
management routes under the admin
path a component is unnecessary. Additionally, a component-less route makes it easier to guard child routes.
Next, import the AdminModule
into app.module.ts
and add it to the imports
array to register the admin routes.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { AdminModule } from './admin/admin.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesModule, CrisisCenterModule, AdminModule, AppRoutingModule ], declarations: [ AppComponent, ComposeMessageComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
Add an "Admin" link to the AppComponent
shell so that users can get to this feature.
<h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet>
Currently, every route within the Crisis Center is open to everyone. The new admin feature should be accessible only to authenticated users.
Write a canActivate()
guard method to redirect anonymous users to the login page when they try to enter the admin area.
Generate an AuthGuard
in the auth
folder.
ng generate guard auth/auth
To demonstrate the fundamentals, this example only logs to the console, returns
true immediately, and allows navigation to proceed:
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { console.log('AuthGuard#canActivate called'); return true; } }
Next, open admin-routing.module.ts
, import the AuthGuard
class, and update the admin route with a canActivate
guard property that references it:
import { AuthGuard } from '../auth/auth.guard'; const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ], } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
The admin feature is now protected by the guard, but the guard requires more customization to work fully.
AuthGuard
Make the AuthGuard
mimic authentication.
The AuthGuard
should call an application service that can login a user and retain information about the current user. Generate a new AuthService
in the auth
folder:
ng generate service auth/auth
Update the AuthService
to log in the user:
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { tap, delay } from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class AuthService { isLoggedIn = false; // store the URL so we can redirect after logging in redirectUrl: string; login(): Observable<boolean> { return of(true).pipe( delay(1000), tap(val => this.isLoggedIn = true) ); } logout(): void { this.isLoggedIn = false; } }
Although it doesn't actually log in, it has an isLoggedIn
flag to tell you whether the user is authenticated. Its login()
method simulates an API call to an external service by returning an observable that resolves successfully after a short pause. The redirectUrl
property stores the URL that the user wanted to access so you can navigate to it after authentication.
To keep things minimal, this example redirects unauthenticated users to
/admin
.
Revise the AuthGuard
to call the AuthService
.
import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { const url: string = state.url; return this.checkLogin(url); } checkLogin(url: string): true|UrlTree { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Redirect to the login page return this.router.parseUrl('/login'); } }
Notice that you inject the AuthService
and the Router
in the constructor. You haven't provided the AuthService
yet but it's good to know that you can inject helpful services into routing guards.
This guard returns a synchronous boolean result. If the user is logged in, it returns true and the navigation continues.
The ActivatedRouteSnapshot
contains the future route that will be activated and the RouterStateSnapshot
contains the future RouterState
of the application, should you pass through the guard check.
If the user is not logged in, you store the attempted URL the user came from using the RouterStateSnapshot.url
and tell the router to redirect to a login page—a page you haven't created yet. Returning a UrlTree
tells the Router
to cancel the current navigation and schedule a new one to redirect the user.
LoginComponent
You need a LoginComponent
for the user to log in to the app. After logging in, you'll redirect to the stored URL if available, or use the default URL. There is nothing new about this component or the way you use it in the router configuration.
ng generate component auth/login
Register a /login
route in the auth/auth-routing.module.ts
. In app.module.ts
, import and add the AuthModule
to the AppModule
imports.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { AuthModule } from './auth/auth.module'; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, HeroesModule, AuthModule, AppRoutingModule, ], declarations: [ AppComponent, ComposeMessageComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
<h2>LOGIN</h2> <p>{{message}}</p> <p> <button (click)="login()" *ngIf="!authService.isLoggedIn">Login</button> <button (click)="logout()" *ngIf="authService.isLoggedIn">Logout</button> </p>
import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent { message: string; constructor(public authService: AuthService, public router: Router) { this.setMessage(); } setMessage() { this.message = 'Logged ' + (this.authService.isLoggedIn ? 'in' : 'out'); } login() { this.message = 'Trying to log in ...'; this.authService.login().subscribe(() => { this.setMessage(); if (this.authService.isLoggedIn) { // Usually you would use the redirect URL from the auth service. // However to keep the example simple, we will always redirect to `/admin`. const redirectUrl = '/admin'; // Redirect the user this.router.navigate([redirectUrl]); } }); } logout() { this.authService.logout(); this.setMessage(); } }
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { LoginComponent } from './login/login.component'; import { AuthRoutingModule } from './auth-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, AuthRoutingModule ], declarations: [ LoginComponent ] }) export class AuthModule {}
CanActivateChild
: guarding child routesYou can also protect child routes with the CanActivateChild
guard. The CanActivateChild
guard is similar to the CanActivate
guard. The key difference is that it runs before any child route is activated.
You protected the admin feature module from unauthorized access. You should also protect child routes within the feature module.
Extend the AuthGuard
to protect when navigating between the admin
routes. Open auth.guard.ts
and add the CanActivateChild
interface to the imported tokens from the router package.
Next, implement the canActivateChild()
method which takes the same arguments as the canActivate()
method: an ActivatedRouteSnapshot
and RouterStateSnapshot
. The canActivateChild()
method can return an Observable<boolean|UrlTree>
or Promise<boolean|UrlTree>
for async checks and a boolean
or UrlTree
for sync checks. This one returns either true
to allow the user to access the admin feature module or UrlTree
to redirect the user to the login page instead:
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, UrlTree } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { const url: string = state.url; return this.checkLogin(url); } canActivateChild( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { return this.canActivate(route, state); } /* . . . */ }
Add the same AuthGuard
to the component-less
admin route to protect all other child routes at one time instead of adding the AuthGuard
to each route individually.
const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', canActivateChild: [AuthGuard], children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
CanDeactivate
: handling unsaved changesBack in the "Heroes" workflow, the app accepts every change to a hero immediately without validation.
In the real world, you might have to accumulate the users changes, validate across fields, validate on the server, or hold changes in a pending state until the user confirms them as a group or cancels and reverts all changes.
When the user navigates away, you can let the user decide what to do with unsaved changes. If the user cancels, you'll stay put and allow more changes. If the user approves, the app can save.
You still might delay navigation until the save succeeds. If you let the user move to the next screen immediately and saving were to fail (perhaps the data is ruled invalid), you would lose the context of the error.
You need to stop the navigation while you wait, asynchronously, for the server to return with its answer.
The CanDeactivate
guard helps you decide what to do with unsaved changes and how to proceed.
Users update crisis information in the CrisisDetailComponent
. Unlike the HeroDetailComponent
, the user changes do not update the crisis entity immediately. Instead, the app updates the entity when the user presses the Save button and discards the changes when the user presses the Cancel button.
Both buttons navigate back to the crisis list after save or cancel.
cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); }
In this scenario, the user could click the heroes link, cancel, push the browser back button, or navigate away without saving.
This example app asks the user to be explicit with a confirmation dialog box that waits asynchronously for the user's response.
You could wait for the user's answer with synchronous, blocking code, however, the app is more responsive—and can do other work—by waiting for the user's answer asynchronously.
Generate a Dialog
service to handle user confirmation.
ng generate service dialog
Add a confirm()
method to the DialogService
to prompt the user to confirm their intent. The window.confirm
is a blocking action that displays a modal dialog and waits for user interaction.
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; /** * Async modal dialog service * DialogService makes this app easier to test by faking this service. * TODO: better modal implementation that doesn't use window.confirm */ @Injectable({ providedIn: 'root', }) export class DialogService { /** * Ask user to confirm an action. `message` explains the action and choices. * Returns observable resolving to `true`=confirm or `false`=cancel */ confirm(message?: string): Observable<boolean> { const confirmation = window.confirm(message || 'Is it OK?'); return of(confirmation); } }
It returns an Observable
that resolves when the user eventually decides what to do: either to discard changes and navigate away (true
) or to preserve the pending changes and stay in the crisis editor (false
).
Generate a guard that checks for the presence of a canDeactivate()
method in a component—any component.
ng generate guard can-deactivate
Paste the following code into your guard.
import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable({ providedIn: 'root', }) export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; } }
While the guard doesn't have to know which component has a deactivate method, it can detect that the CrisisDetailComponent
component has the canDeactivate()
method and call it. The guard not knowing the details of any component's deactivation method makes the guard reusable.
Alternatively, you could make a component-specific CanDeactivate
guard for the CrisisDetailComponent
. The canDeactivate()
method provides you with the current instance of the component
, the current ActivatedRoute
, and RouterStateSnapshot
in case you needed to access some external information. This would be useful if you only wanted to use this guard for this component and needed to get the component's properties or confirm whether the router should allow navigation away from it.
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { CrisisDetailComponent } from './crisis-center/crisis-detail/crisis-detail.component'; @Injectable({ providedIn: 'root', }) export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> { canDeactivate( component: CrisisDetailComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | boolean { // Get the Crisis Center ID console.log(route.paramMap.get('id')); // Get the current URL console.log(state.url); // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!component.crisis || component.crisis.name === component.editName) { return true; } // Otherwise ask the user with the dialog service and return its // observable which resolves to true or false when the user decides return component.dialogService.confirm('Discard changes?'); } }
Looking back at the CrisisDetailComponent
, it implements the confirmation workflow for unsaved changes.
canDeactivate(): Observable<boolean> | boolean { // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!this.crisis || this.crisis.name === this.editName) { return true; } // Otherwise ask the user with the dialog service and return its // observable which resolves to true or false when the user decides return this.dialogService.confirm('Discard changes?'); }
Notice that the canDeactivate()
method can return synchronously; it returns true
immediately if there is no crisis or there are no pending changes. But it can also return a Promise
or an Observable
and the router will wait for that to resolve to truthy (navigate) or falsy (stay on the current route).
Add the Guard
to the crisis detail route in crisis-center-routing.module.ts
using the canDeactivate
array property.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard] }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
Now you have given the user a safeguard against unsaved changes.
Resolve
: pre-fetching component dataIn the Hero Detail
and Crisis Detail
, the app waited until the route was activated to fetch the respective hero or crisis.
If you were using a real world API, there might be some delay before the data to display is returned from the server. You don't want to display a blank component while waiting for the data.
To improve this behavior, you can pre-fetch data from the server using a resolver so it's ready the moment the route is activated. This also allows you to handle errors before routing to the component. There's no point in navigating to a crisis detail for an id
that doesn't have a record. It'd be better to send the user back to the Crisis List
that shows only valid crisis centers.
In summary, you want to delay rendering the routed component until all necessary data has been fetched.
At the moment, the CrisisDetailComponent
retrieves the selected crisis. If the crisis is not found, the router navigates back to the crisis list view.
The experience might be better if all of this were handled first, before the route is activated. A CrisisDetailResolver
service could retrieve a Crisis
or navigate away, if the Crisis
did not exist, before activating the route and creating the CrisisDetailComponent
.
Generate a CrisisDetailResolver
service file within the Crisis Center
feature area.
ng generate service crisis-center/crisis-detail-resolver
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class CrisisDetailResolverService { constructor() { } }
Move the relevant parts of the crisis retrieval logic in CrisisDetailComponent.ngOnInit()
into the CrisisDetailResolverService
. Import the Crisis
model, CrisisService
, and the Router
so you can navigate elsewhere if you can't fetch the crisis.
Be explicit and implement the Resolve
interface with a type of Crisis
.
Inject the CrisisService
and Router
and implement the resolve()
method. That method could return a Promise
, an Observable
, or a synchronous return value.
The CrisisService.getCrisis()
method returns an observable in order to prevent the route from loading until the data is fetched. The Router
guards require an observable to complete
, which means it has emitted all of its values. You use the take
operator with an argument of 1
to ensure that the Observable
completes after retrieving the first value from the Observable returned by the getCrisis()
method.
If it doesn't return a valid Crisis
, then return an empty Observable
, cancel the previous in-progress navigation to the CrisisDetailComponent
, and navigate the user back to the CrisisListComponent
. The updated resolver service looks like this:
import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { Observable, of, EMPTY } from 'rxjs'; import { mergeMap, take } from 'rxjs/operators'; import { CrisisService } from './crisis.service'; import { Crisis } from './crisis'; @Injectable({ providedIn: 'root', }) export class CrisisDetailResolverService implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> | Observable<never> { const id = route.paramMap.get('id'); return this.cs.getCrisis(id).pipe( take(1), mergeMap(crisis => { if (crisis) { return of(crisis); } else { // id not found this.router.navigate(['/crisis-center']); return EMPTY; } }) ); } }
Import this resolver in the crisis-center-routing.module.ts
and add a resolve
object to the CrisisDetailComponent
route configuration.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolverService } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
The CrisisDetailComponent
should no longer fetch the crisis. When you re-configured the route, you changed where the crisis is. Update the CrisisDetailComponent
to get the crisis from the ActivatedRoute.data.crisis
property instead;
ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); }
Note the following three important points:
The router's Resolve
interface is optional. The CrisisDetailResolverService
doesn't inherit from a base class. The router looks for that method and calls it if found.
The router calls the resolver in any case where the the user could navigate away so you don't have to code for each use case.
Returning an empty Observable
in at least one resolver will cancel navigation.
The relevant Crisis Center code for this milestone follows.
<h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/superheroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a routerLink="/login" routerLinkActive="active">Login</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet>
<p>Welcome to the Crisis Center</p>
<h2>CRISIS CENTER</h2> <router-outlet></router-outlet>
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolverService } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
<ul class="crises"> <li *ngFor="let crisis of crises$ | async" [class.selected]="crisis.id === selectedId"> <a [routerLink]="[crisis.id]"> <span class="badge">{{ crisis.id }}</span>{{ crisis.name }} </a> </li> </ul> <router-outlet></router-outlet>
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CrisisService } from '../crisis.service'; import { Crisis } from '../crisis'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; @Component({ selector: 'app-crisis-list', templateUrl: './crisis-list.component.html', styleUrls: ['./crisis-list.component.css'] }) export class CrisisListComponent implements OnInit { crises$: Observable<Crisis[]>; selectedId: number; constructor( private service: CrisisService, private route: ActivatedRoute ) {} ngOnInit() { this.crises$ = this.route.paramMap.pipe( switchMap(params => { this.selectedId = +params.get('id'); return this.service.getCrises(); }) ); } }
<div *ngIf="crisis"> <h3>"{{ editName }}"</h3> <div> <label>Id: </label>{{ crisis.id }}</div> <div> <label>Name: </label> <input [(ngModel)]="editName" placeholder="name"/> </div> <p> <button (click)="save()">Save</button> <button (click)="cancel()">Cancel</button> </p> </div>
import { Component, OnInit, HostBinding } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { Crisis } from '../crisis'; import { DialogService } from '../../dialog.service'; @Component({ selector: 'app-crisis-detail', templateUrl: './crisis-detail.component.html', styleUrls: ['./crisis-detail.component.css'] }) export class CrisisDetailComponent implements OnInit { crisis: Crisis; editName: string; constructor( private route: ActivatedRoute, private router: Router, public dialogService: DialogService ) {} ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); } cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); } canDeactivate(): Observable<boolean> | boolean { // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!this.crisis || this.crisis.name === this.editName) { return true; } // Otherwise ask the user with the dialog service and return its // observable which resolves to true or false when the user decides return this.dialogService.confirm('Discard changes?'); } gotoCrises() { const crisisId = this.crisis ? this.crisis.id : null; // Pass along the crisis id if available // so that the CrisisListComponent can select that crisis. // Add a totally useless `foo` parameter for kicks. // Relative navigation back to the crises this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route }); } }
import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { Observable, of, EMPTY } from 'rxjs'; import { mergeMap, take } from 'rxjs/operators'; import { CrisisService } from './crisis.service'; import { Crisis } from './crisis'; @Injectable({ providedIn: 'root', }) export class CrisisDetailResolverService implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> | Observable<never> { const id = route.paramMap.get('id'); return this.cs.getCrisis(id).pipe( take(1), mergeMap(crisis => { if (crisis) { return of(crisis); } else { // id not found this.router.navigate(['/crisis-center']); return EMPTY; } }) ); } }
import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MessageService } from '../message.service'; import { Crisis } from './crisis'; import { CRISES } from './mock-crises'; @Injectable({ providedIn: 'root', }) export class CrisisService { static nextCrisisId = 100; private crises$: BehaviorSubject<Crisis[]> = new BehaviorSubject<Crisis[]>(CRISES); constructor(private messageService: MessageService) { } getCrises() { return this.crises$; } getCrisis(id: number | string) { return this.getCrises().pipe( map(crises => crises.find(crisis => crisis.id === +id)) ); } }
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; /** * Async modal dialog service * DialogService makes this app easier to test by faking this service. * TODO: better modal implementation that doesn't use window.confirm */ @Injectable({ providedIn: 'root', }) export class DialogService { /** * Ask user to confirm an action. `message` explains the action and choices. * Returns observable resolving to `true`=confirm or `false`=cancel */ confirm(message?: string): Observable<boolean> { const confirmation = window.confirm(message || 'Is it OK?'); return of(confirmation); } }
Guards
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, UrlTree } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { const url: string = state.url; return this.checkLogin(url); } canActivateChild( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { return this.canActivate(route, state); } checkLogin(url: string): true|UrlTree { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Redirect to the login page return this.router.parseUrl('/login'); } }
import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable({ providedIn: 'root', }) export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; } }
In the route parameters section, you only dealt with parameters specific to the route. However, you can use query parameters to get optional parameters available to all routes.
Fragments refer to certain elements on the page identified with an id
attribute.
Update the AuthGuard
to provide a session_id
query that will remain after navigating to another route.
Add an anchor
element so you can jump to a certain point on the page.
Add the NavigationExtras
object to the router.navigate()
method that navigates you to the /login
route.
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, NavigationExtras, UrlTree } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { const url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { return this.canActivate(route, state); } checkLogin(url: string): true|UrlTree { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Create a dummy session id const sessionId = 123456789; // Set our navigation extras object // that contains our global query params and fragment const navigationExtras: NavigationExtras = { queryParams: { session_id: sessionId }, fragment: 'anchor' }; // Redirect to the login page with extras return this.router.createUrlTree(['/login'], navigationExtras); } }
You can also preserve query parameters and fragments across navigations without having to provide them again when navigating. In the LoginComponent
, you'll add an object as the second argument in the router.navigateUrl()
function and provide the queryParamsHandling
and preserveFragment
to pass along the current query parameters and fragment to the next route.
// Set our navigation extras object // that passes on our global query params and fragment const navigationExtras: NavigationExtras = { queryParamsHandling: 'preserve', preserveFragment: true }; // Redirect the user this.router.navigate([redirectUrl], navigationExtras);
The
queryParamsHandling
feature also provides amerge
option, which preserves and combines the current query parameters with any provided query parameters when navigating.
To navigate to the Admin Dashboard route after logging in, update admin-dashboard.component.ts
to handle the query parameters and fragment.
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ selector: 'app-admin-dashboard', templateUrl: './admin-dashboard.component.html', styleUrls: ['./admin-dashboard.component.css'] }) export class AdminDashboardComponent implements OnInit { sessionId: Observable<string>; token: Observable<string>; constructor(private route: ActivatedRoute) {} ngOnInit() { // Capture the session ID if available this.sessionId = this.route .queryParamMap .pipe(map(params => params.get('session_id') || 'None')); // Capture the fragment if available this.token = this.route .fragment .pipe(map(fragment => fragment || 'None')); } }
Query parameters and fragments are also available through the ActivatedRoute
service. Just like route parameters, the query parameters and fragments are provided as an Observable
. The updated Crisis Admin component feeds the Observable
directly into the template using the AsyncPipe
.
Now, you can click on the Admin button, which takes you to the Login page with the provided queryParamMap
and fragment
. After you click the login button, notice that you have been redirected to the Admin Dashboard
page with the query parameters and fragment still intact in the address bar.
You can use these persistent bits of information for things that need to be provided across pages like authentication tokens or session ids.
The
query params
andfragment
can also be preserved using aRouterLink
with thequeryParamsHandling
andpreserveFragment
bindings respectively.
As you've worked through the milestones, the application has naturally gotten larger. At some point you'll reach a point where the application takes a long time to load.
To remedy this issue, use asynchronous routing, which loads feature modules lazily, on request. Lazy loading has multiple benefits.
You're already part of the way there. By organizing the application into modules—AppModule
, HeroesModule
, AdminModule
and CrisisCenterModule
—you have natural candidates for lazy loading.
Some modules, like AppModule
, must be loaded from the start. But others can and should be lazy loaded. The AdminModule
, for example, is needed by a few authorized users, so you should only load it when requested by the right people.
Change the admin
path in the admin-routing.module.ts
from 'admin'
to an empty string, ''
, the empty path.
Use empty path routes to group routes together without adding any additional path segments to the URL. Users will still visit /admin
and the AdminComponent
still serves as the Routing Component containing child routes.
Open the AppRoutingModule
and add a new admin
route to its appRoutes
array.
Give it a loadChildren
property instead of a children
property. The loadChildren
property takes a function that returns a promise using the browser's built-in syntax for lazy loading code using dynamic imports import('...')
. The path is the location of the AdminModule
(relative to the app root). After the code is requested and loaded, the Promise
resolves an object that contains the NgModule
, in this case the AdminModule
.
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), },
Note: When using absolute paths, the
NgModule
file location must begin withsrc/app
in order to resolve correctly. For custom path mapping with absolute paths, you must configure thebaseUrl
andpaths
properties in the projecttsconfig.json
.
When the router navigates to this route, it uses the loadChildren
string to dynamically load the AdminModule
. Then it adds the AdminModule
routes to its current route configuration. Finally, it loads the requested route to the destination admin component.
The lazy loading and re-configuration happen just once, when the route is first requested; the module and routes are available immediately for subsequent requests.
Angular provides a built-in module loader that supports SystemJS to load modules asynchronously. If you were using another bundling tool, such as Webpack, you would use the Webpack mechanism for asynchronously loading modules.
Take the final step and detach the admin feature set from the main application. The root AppModule
must neither load nor reference the AdminModule
or its files.
In app.module.ts
, remove the AdminModule
import statement from the top of the file and remove the AdminModule
from the NgModule's imports
array.
CanLoad
: guarding unauthorized loading of feature modulesYou're already protecting the AdminModule
with a CanActivate
guard that prevents unauthorized users from accessing the admin feature area. It redirects to the login page if the user is not authorized.
But the router is still loading the AdminModule
even if the user can't visit any of its components. Ideally, you'd only load the AdminModule
if the user is logged in.
Add a CanLoad
guard that only loads the AdminModule
once the user is logged in and attempts to access the admin feature area.
The existing AuthGuard
already has the essential logic in its checkLogin()
method to support the CanLoad
guard.
Open auth.guard.ts
. Import the CanLoad
interface from @angular/router
. Add it to the AuthGuard
class's implements
list. Then implement canLoad()
as follows:
canLoad(route: Route): boolean { const url = `/${route.path}`; return this.checkLogin(url); }
The router sets the canLoad()
method's route
parameter to the intended destination URL. The checkLogin()
method redirects to that URL once the user has logged in.
Now import the AuthGuard
into the AppRoutingModule
and add the AuthGuard
to the canLoad
array property for the admin
route. The completed admin route looks like this:
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canLoad: [AuthGuard] },
In addition to loading modules on-demand, you can load modules asynchronously with preloading.
The AppModule
is eagerly loaded when the application starts, meaning that it loads right away. Now the AdminModule
loads only when the user clicks on a link, which is called lazy loading.
Preloading allows you to load modules in the background so that the data is ready to render when the user activates a particular route. Consider the Crisis Center. It isn't the first view that a user sees. By default, the Heroes are the first view. For the smallest initial payload and fastest launch time, you should eagerly load the AppModule
and the HeroesModule
.
You could lazy load the Crisis Center. But you're almost certain that the user will visit the Crisis Center within minutes of launching the app. Ideally, the app would launch with just the AppModule
and the HeroesModule
loaded and then, almost immediately, load the CrisisCenterModule
in the background. By the time the user navigates to the Crisis Center, its module will have been loaded and ready.
After each successful navigation, the router looks in its configuration for an unloaded module that it can preload. Whether it preloads a module, and which modules it preloads, depends upon the preload strategy.
The Router
offers two preloading strategies:
The router either never preloads, or preloads every lazy loaded module. The Router
also supports custom preloading strategies for fine control over which modules to preload and when.
This section guides you through updating the CrisisCenterModule
to load lazily by default and use the PreloadAllModules
strategy to load all lazy loaded modules.
Update the route configuration to lazy load the CrisisCenterModule
. Take the same steps you used to configure AdminModule
for lazy loading.
Change the crisis-center
path in the CrisisCenterRoutingModule
to an empty string.
Add a crisis-center
route to the AppRoutingModule
.
Set the loadChildren
string to load the CrisisCenterModule
.
Remove all mention of the CrisisCenterModule
from app.module.ts
.
Here are the updated modules before enabling preload:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { AuthModule } from './auth/auth.module'; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, HeroesModule, AuthModule, AppRoutingModule, ], declarations: [ AppComponent, ComposeMessageComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
import { NgModule } from '@angular/core'; import { RouterModule, Routes, } from '@angular/router'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { AuthGuard } from './auth/auth.guard'; const appRoutes: Routes = [ { path: 'compose', component: ComposeMessageComponent, outlet: 'popup' }, { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canLoad: [AuthGuard] }, { path: 'crisis-center', loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule) }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, ) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: '', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolverService } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
You could try this now and confirm that the CrisisCenterModule
loads after you click the "Crisis Center" button.
To enable preloading of all lazy loaded modules, import the PreloadAllModules
token from the Angular router package.
The second argument in the RouterModule.forRoot()
method takes an object for additional configuration options. The preloadingStrategy
is one of those options. Add the PreloadAllModules
token to the forRoot()
call:
RouterModule.forRoot( appRoutes, { enableTracing: true, // <-- debugging purposes only preloadingStrategy: PreloadAllModules } )
This configures the Router
preloader to immediately load all lazy loaded routes (routes with a loadChildren
property).
When you visit http://localhost:4200
, the /heroes
route loads immediately upon launch and the router starts loading the CrisisCenterModule
right after the HeroesModule
loads.
Currently, the AdminModule
does not preload because CanLoad
is blocking it.
CanLoad
blocks preloadThe PreloadAllModules
strategy does not load feature areas protected by a CanLoad guard.
You added a CanLoad
guard to the route in the AdminModule
a few steps back to block loading of that module until the user is authorized. That CanLoad
guard takes precedence over the preload strategy.
If you want to preload a module as well as guard against unauthorized access, remove the canLoad()
guard method and rely on the canActivate() guard alone.
Preloading every lazy loaded module works well in many situations. However, in consideration of things such as low bandwidth and user metrics, you can use a custom preloading strategy for specific feature modules.
This section guides you through adding a custom strategy that only preloads routes whose data.preload
flag is set to true
. Recall that you can add anything to the data
property of a route.
Set the data.preload
flag in the crisis-center
route in the AppRoutingModule
.
{ path: 'crisis-center', loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule), data: { preload: true } },
Generate a new SelectivePreloadingStrategy
service.
ng generate service selective-preloading-strategy
Replace the contents of selective-preloading-strategy.service.ts
with the following:
import { Injectable } from '@angular/core'; import { PreloadingStrategy, Route } from '@angular/router'; import { Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class SelectivePreloadingStrategyService implements PreloadingStrategy { preloadedModules: string[] = []; preload(route: Route, load: () => Observable<any>): Observable<any> { if (route.data && route.data.preload) { // add the route path to the preloaded module array this.preloadedModules.push(route.path); // log the route path to the console console.log('Preloaded: ' + route.path); return load(); } else { return of(null); } } }
SelectivePreloadingStrategyService
implements the PreloadingStrategy
, which has one method, preload()
.
The router calls the preload()
method with two arguments:
An implementation of preload
must return an Observable
. If the route does preload, it returns the observable returned by calling the loader function. If the route does not preload, it returns an Observable
of null
.
In this sample, the preload()
method loads the route if the route's data.preload
flag is truthy.
As a side-effect, SelectivePreloadingStrategyService
logs the path
of a selected route in its public preloadedModules
array.
Shortly, you'll extend the AdminDashboardComponent
to inject this service and display its preloadedModules
array.
But first, make a few changes to the AppRoutingModule
.
SelectivePreloadingStrategyService
into AppRoutingModule
.PreloadAllModules
strategy in the call to forRoot()
with this SelectivePreloadingStrategyService
.SelectivePreloadingStrategyService
strategy to the AppRoutingModule
providers array so you can inject it elsewhere in the app.Now edit the AdminDashboardComponent
to display the log of preloaded routes.
SelectivePreloadingStrategyService
.preloadedModules
array.Now the file is as follows:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { SelectivePreloadingStrategyService } from '../../selective-preloading-strategy.service'; @Component({ selector: 'app-admin-dashboard', templateUrl: './admin-dashboard.component.html', styleUrls: ['./admin-dashboard.component.css'] }) export class AdminDashboardComponent implements OnInit { sessionId: Observable<string>; token: Observable<string>; modules: string[]; constructor( private route: ActivatedRoute, preloadStrategy: SelectivePreloadingStrategyService ) { this.modules = preloadStrategy.preloadedModules; } ngOnInit() { // Capture the session ID if available this.sessionId = this.route .queryParamMap .pipe(map(params => params.get('session_id') || 'None')); // Capture the fragment if available this.token = this.route .fragment .pipe(map(fragment => fragment || 'None')); } }
Once the application loads the initial route, the CrisisCenterModule
is preloaded. Verify this by logging in to the Admin
feature area and noting that the crisis-center
is listed in the Preloaded Modules
. It also logs to the browser's console.
You've setup the routes for navigating around your application and used navigation imperatively and declaratively. But like any application, requirements change over time. You've setup links and navigation to /heroes
and /hero/:id
from the HeroListComponent
and HeroDetailComponent
components. If there were a requirement that links to heroes
become superheroes
, you would still want the previous URLs to navigate correctly. You also don't want to update every link in your application, so redirects makes refactoring routes trivial.
/heroes
to /superheroes
This section guides you through migrating the Hero
routes to new URLs. The Router
checks for redirects in your configuration before navigating, so each redirect is triggered when needed. To support this change, add redirects from the old routes to the new routes in the heroes-routing.module
.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list/hero-list.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', redirectTo: '/superheroes' }, { path: 'hero/:id', redirectTo: '/superhero/:id' }, { path: 'superheroes', component: HeroListComponent, data: { animation: 'heroes' } }, { path: 'superhero/:id', component: HeroDetailComponent, data: { animation: 'hero' } } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroesRoutingModule { }
Notice two different types of redirects. The first change is from /heroes
to /superheroes
without any parameters. The second change is from /hero/:id
to /superhero/:id
, which includes the :id
route parameter. Router redirects also use powerful pattern-matching, so the Router
inspects the URL and replaces route parameters in the path
with their appropriate destination. Previously, you navigated to a URL such as /hero/15
with a route parameter id
of 15
.
The
Router
also supports query parameters and the fragment when using redirects.
Currently, the empty path route redirects to /heroes
, which redirects to /superheroes
. This won't work because the Router
handles redirects once at each level of routing configuration. This prevents chaining of redirects, which can lead to endless redirect loops.
Instead, update the empty path route in app-routing.module.ts
to redirect to /superheroes
.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { AuthGuard } from './auth/auth.guard'; import { SelectivePreloadingStrategyService } from './selective-preloading-strategy.service'; const appRoutes: Routes = [ { path: 'compose', component: ComposeMessageComponent, outlet: 'popup' }, { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canLoad: [AuthGuard] }, { path: 'crisis-center', loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule), data: { preload: true } }, { path: '', redirectTo: '/superheroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: false, // <-- debugging purposes only preloadingStrategy: SelectivePreloadingStrategyService, } ) ], exports: [ RouterModule ] }) export class AppRoutingModule { }
A routerLink
isn't tied to route configuration, so update the associated router links to remain active when the new route is active. Update the app.component.ts
template for the /heroes
routerLink
.
<h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/superheroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a routerLink="/login" routerLinkActive="active">Login</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet>
Update the goToHeroes()
method in the hero-detail.component.ts
to navigate back to /superheroes
with the optional route parameters.
gotoHeroes(hero: Hero) { const 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(['/superheroes', { id: heroId, foo: 'foo' }]); }
With the redirects setup, all previous routes now point to their new destinations and both URLs still function as intended.
To determine if your routes are actually evaluated in the proper order, you can inspect the router's configuration.
Do this by injecting the router and logging to the console its config
property. For example, update the AppModule
as follows and look in the browser console window to see the finished route configuration.
export class AppModule { // Diagnostic only: inspect router configuration constructor(router: Router) { // Use a custom replacer to display function names in the route configs const replacer = (key, value) => (typeof value === 'function') ? value.name : value; console.log('Routes: ', JSON.stringify(router.config, replacer, 2)); } }
For the completed router app, see the for the final source code.
© 2010–2020 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v10.angular.io/guide/router-tutorial-toh