The Angular Router enables navigation from one view to the next as users perform application tasks.
We cover the router's primary features in this chapter, illustrating them through the evolution of a small application that we can run live.
To see the URL changes in the browser address bar, pop out the preview window by clicking the blue 'X' button in the upper right corner.
Overview
The browser is a familiar model of application navigation. We enter a URL in the address bar and the browser navigates to a corresponding page. We click links on the page and the browser navigates to a new page. We click the browser's back and forward buttons and the browser navigates backward and forward through the history of pages we've seen.
The Angular Router ("the router") borrows from this model. It can interpret a browser URL as an instruction to navigate to a client-generated view and pass optional parameters along to the supporting view component to help it decide what specific content to present. We can bind the router to links on a page and it will navigate to the appropriate application view when the user clicks a link. We can navigate imperatively when the user clicks a button, selects from a drop box, or in response to some other stimulus from any source. And the router logs activity in the browser's history journal so the back and forward buttons work as well.
We'll learn many router details in this chapter which covers
- Setting the base href
- Importing from the router library
- configuring the router
- the link parameters array that propels router navigation
- navigating when the user clicks a data-bound RouterLink
- navigating under program control
- retrieving information from the route
- animating transitions for route components
- navigating relative to our current URL
- toggling css classes for the active router link
- embedding critical information in the URL with route parameters
- providing non-critical information in optional route parameters
- add child routes under a feature section
- grouping child routes without a component
- redirecting from one route to another
- confirming or canceling navigation with guards
- CanActivate to prevent navigation to a route
- CanActivateChild to prevent navigation to a child route
- CanDeactivate to prevent navigation away from the current route
- Resolve to pre-fetch data before activating a route
- CanLoad to prevent asynchronous routing
- providing optional information across routes with query parameters
- jumping to anchor elements using a fragment
- loading feature areas asynchronously
- choosing the "HTML5" or "hash" URL style
We proceed in phases marked by milestones building from a simple two-pager with placeholder views up to a modular, multi-view design with child routes.
But first, an overview of router basics.
The Basics
Let's begin with a few core concepts of the Router. Then we can explore the details through a sequence of examples.
<base href>
Most routing applications should add a <base>
element to the index.html
as the first child in the <head>
tag to tell the router how to compose navigation URLs.
If the app
folder is the application root, as it is for our sample application, set the href
value exactly as shown here.
index.html (base-href)
<base href="/">
Router imports
The Angular Router is an optional service that presents a particular component view for a given URL. It is not part of the Angular 2 core. It is in its own library package, @angular/router
. We import what we need from it as we would from any other Angular package.
app/app.routing.ts (import)
import { Routes, RouterModule } from '@angular/router';
We cover other options in the details below.
Configuration
The application will have one router
. When the browser's URL changes, the router looks for a corresponding Route
from which it can determine the component to display.
A router has no routes until we configure it. We bootstrap our application with an array of routes that we'll provide to our RouterModule.forRoot
function.
In the following example, we configure our application with four route definitions.
app/app.routing.ts (excerpt)
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const appRoutes: Routes = [ { path: 'hero/:id', component: HeroDetailComponent }, { path: 'crisis-center', component: CrisisCenterComponent }, { path: 'heroes', component: HeroListComponent, data: { title: 'Heroes List' } }, { path: '', component: HomeComponent }, { path: '**', component: PageNotFoundComponent } ]; export const appRoutingProviders: any[] = [ ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
The Routes
is an array of routes that describe how to navigate. Each Route maps a URL path
to a component.
There are no leading slashes in our path. The router parses and builds the URL for us, allowing us to use relative and absolute paths when navigating between application views.
The :id
in the first route is a token for a route parameter. In a URL such as /hero/42
, "42" is the value of the id
parameter. The corresponding HeroDetailComponent
will use that value to find and present the hero whose id
is 42. We'll learn more about route parameters later in this chapter.
The data
property in the third route is a place to store arbitrary data associated with each specific route. This data is accessible within each activated route and can be used to store items such as page titles, breadcrumb text and other read-only data. We'll use the resolve guard to retrieve additional data later in the chapter.
The empty path
in the fourth route matches as the default path for each level of routing. It also allows for adding routes without extending the URL path.
The **
in the last route denotes a wildcard path for our route. The router will match this route if the URL requested doesn't match any paths for routes defined in our configuration. This is useful for displaying a 404 page or redirecting to another route.
The order of the routes in the configuration matters and this is by design. The router uses a first-match wins strategy when matching routes, so more specific routes should be placed above less specific routes. In our configuration above, the routes with a static path are listed first, followed by an empty path route, that matches as the default route. The wildcard route is listed last as it's the most generic route and should be matched only if no other routes are matched first.
We export the routing
constant so we can import it into our app.module.ts
file where we'll add a configured Router module to our AppModule
imports.
Next we open app.module.ts
where we must register our routing, routing providers, and declare our two route components.
app/app.module.ts (basic setup)
import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroListComponent } from './hero-list.component'; import { CrisisListComponent } from './crisis-list.component'; @NgModule({ imports: [ BrowserModule, FormsModule, routing ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent ], providers: [ appRoutingProviders ], bootstrap: [ AppComponent ] })
Router Outlet
Given this configuration, when the browser URL for this application becomes /heroes
, the router matches that URL to the Route
path /heroes
and displays the HeroListComponent
in a RouterOutlet
that we've placed in the host view's HTML.
<!-- Routed views go here --> <router-outlet></router-outlet>
Router Links
Now we have routes configured and a place to render them, but how do we navigate? The URL could arrive directly from the browser address bar. But most of the time we navigate as a result of some user action such as the click of an anchor tag.
We add a RouterLink
directive to the anchor tag. Since we know our link doesn't contain any dynamic information, we can use a one-time binding to our route path.
If our RouterLink
needed to be more dynamic we could bind to a template expression that returns an array of route link parameters (the link parameters array). The router ultimately resolves that array into a URL and a component view.
We also add a RouterLinkActive
directive to each anchor tag to add or remove CSS classes to the element when the associated RouterLink becomes active. The directive can be added directly on the element or on its parent element.
We see such bindings in the following AppComponent
template:
template: ` <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> `
We're adding two anchor tags with RouterLink
and RouterLinkActive
directives. We bind each RouterLink
to a string containing the path of a route. '/crisis-center' and '/heroes' are the paths of the Routes
we configured above.
We'll learn to write link expressions — and why they are arrays — later in the chapter.
We define active
as the CSS class we want toggled to each RouterLink
when they become the current route using the RouterLinkActive
directive. We could add multiple classes to the RouterLink
if we so desired.
Router State
After the end of each successful navigation lifecycle, the router builds a tree of ActivatedRoute
objects that make up the current state of the router. We can access the current RouterState
from anywhere in our application using the Router
service and the routerState
property.
The router state provides us with methods to traverse up and down the route tree from any activated route to get information we may need from parent, child and sibling routes.
Let's summarize
The application is provided with a configured router. The component has a RouterOutlet
where it can display views produced by the router. It has RouterLink
s that users can click to navigate via the router.
Here are the key Router terms and their meanings:
Router Part | Meaning |
---|---|
Router |
Displays the application component for the active URL. Manages navigation from one component to the next. |
RouterModule |
A separate Angular module that provides the necessary service providers and directives for navigating through application views. |
Routes |
Defines an array of Routes, each mapping a URL path to a component. |
Route |
Defines how the router should navigate to a component based on a URL pattern. Most routes consist of a path and a component type. |
RouterOutlet |
The directive (<router-outlet> ) that marks where the router should display a view. |
RouterLink |
The directive for binding a clickable HTML element to a route. Clicking an anchor tag with a routerLink directive that is bound to a string or a Link Parameters Array triggers a navigation. |
RouterLinkActive |
The directive for adding/removing classes from an HTML element when an associated routerLink contained on or inside the element becomes active/inactive. |
ActivatedRoute |
A service that is provided to each route component that contains route specific information such as route parameters, static data, resolve data, global query params and the global fragment. |
RouterState |
The current state of the router including a tree of the currently activated routes in our application along convenience methods for traversing the route tree. |
Link Parameters Array |
An array that the router interprets into a routing instruction. We can bind a RouterLink to that array or pass the array as an argument to the Router.navigate method. |
Routing Component |
An Angular component with a RouterOutlet that displays views based on router navigations. |
We've barely touched the surface of the router and its capabilities.
The following detail sections describe a sample routing application as it evolves over a sequence of milestones. We strongly recommend taking the time to read and understand this story.
The Sample Application
We have an application in mind as we move from milestone to milestone.
While we make incremental progress toward the ultimate sample application, this chapter is not a tutorial. We discuss code and design decisions pertinent to routing and application design. We gloss over everything in between.
The full source is available in the live example.
Our client is the Hero Employment Agency. Heroes need work and The Agency finds Crises for them to solve.
The application has three main feature areas:
- A Crisis Center where we maintain the list of crises for assignment to heroes.
- A Heroes area where we maintain the list of heroes employed by The Agency.
- An Admin area where we manage the list of crises and heroes displayed.
Run the live example. It opens in the Crisis Center. We'll come back to that.
Click the Heroes link. We're presented with a list of Heroes.
We select one and the application takes us to a hero editing screen.
Our changes take effect immediately. We click the "Back" button and the app returns us to the Heroes list.
We could have clicked the browser's back button instead. That would have returned us to the Heroes List as well. Angular app navigation updates the browser history as normal web navigation does.
Now click the Crisis Center link. We go to the Crisis Center and its list of ongoing crises.
We select one and the application takes us to a crisis editing screen.
This is a bit different from the Hero Detail. Hero Detail saves the changes as we type. In Crisis Detail our changes are temporary until we 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.
Suppose we click a crisis, make a change, but do not click either button. Maybe we click the browser back button instead. Maybe we click the "Heroes" link.
Do either. Up pops a dialog box.
We can say "OK" and lose our changes or click "Cancel" and continue editing.
The router supports a CanDeactivate
guard that gives us a chance to clean-up or ask the user's permission before navigating away from the current view.
Here we see an entire user session that touches all of these features.
Here's a diagram of all application routing options:
This app illustrates the router features we'll cover in this chapter
- organizing the application features into modules
- navigating to a component (Heroes link to "Heroes List")
- including a route parameter (passing the Hero
id
while routing to the "Hero Detail") - child routes (the Crisis Center has its own routes)
- the
CanActivate
guard (checking route access) - the
CanActivateChild
guard (checking child route access) - the
CanDeactivate
guard (ask permission to discard unsaved changes) - the
Resolve
guard (pre-fetching route data) - lazy loading feature modules
- the
CanLoad
guard (check before loading feature module assets)
Milestone #1: Getting Started with the Router
Let's begin with a simple version of the app that navigates between two empty views.
Set the <base href>
The Router uses the browser's history.pushState for navigation. Thanks to pushState
, we can make our in-app URL paths look the way we want them to look, e.g. localhost:3000/crisis-center
. Our in-app URLs can be indistinguishable from server URLs.
Modern HTML 5 browsers were the first to support pushState
which is why many people refer to these URLs as "HTML 5 style" URLs.
We must add a <base href> element tag to the index.html
to make pushState
routing work. The browser also needs the base href
value to prefix relative URLs when downloading and linking to css files, scripts, and images.
Add the base element just after the <head>
tag. If the app
folder is the application root, as it is for our application, set the href
value in index.html
exactly as shown here.
index.html (base-href)
<base href="/">
HTML 5 style navigation is the Router default. Learn why "HTML 5" style is preferred, how to adjust its behavior, and how to switch to the older hash (#) style if necessary in the Browser URL Styles appendix below.
Live example note
We have to get tricky when we run the live example because the host service sets the application base address dynamically. That's why we replace the <base href...>
with a script that writes a <base>
tag on the fly to match.
<script>document.write('<base href="' + document.location + '" />');</script>
We should only need this trick for the live example, not production code.
Configure the routes for the Router
We begin by importing some symbols from the router library.
The Router is in its own @angular/router
package. It's not part of the Angular 2 core. The router is an optional service because not all applications need routing and, depending on your requirements, you may need a different routing library.
We teach our router how to navigate by configuring it with routes. We recommend creating a separate app.routing.ts
file dedicated to this purpose.
Here is our first configuration. We pass the array of routes to the RouterModule.forRoot
method which returns a module containing the configured Router
service provider ... and some other, unseen providers that the routing library requires. We export this as the routing
token.
app/app.routing.ts (excerpt)
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent } ]; export const appRoutingProviders: any[] = [ ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
We also export an empty appRoutingProviders
array so we can simplify registration of router dependencies later in app.module.ts
. We don't have any providers to register right now. But we will.
Define routes
A router must be configured with a list of route definitions.
Our first configuration defines an array of two routes with simple paths leading to the CrisisListComponent
and HeroListComponent
components.
Each definition translates to a Route object which has 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 such route definitions when the browser URL changes or when our code tells the router to navigate along a route path.
In plain English, we might say of the first route:
-
When the browser's location URL changes to match the path segment
/crisis-center
, create or retrieve an instance of theCrisisListComponent
and display its view. -
When the application requests navigation to the path
/crisis-center
, create or retrieve an instance of theCrisisListComponent
, display its view, and update the browser's address location and history with the URL for that path.
Learn about providers in the Dependency Injection chapter.
Register routing in the AppModule
Our app launches from the app.module.ts
file in the /app
folder.
We import the routing
token we exported from the app.routing.ts
file and add it to the imports
array.
We import our CrisisListComponent
and HeroListComponent
components and add them to our declarations so they will be registered within our AppModule
.
We also import the appRoutingProviders
array and add it to the providers
array.
app/app.module.ts (excerpt)
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroListComponent } from './hero-list.component'; import { CrisisListComponent } from './crisis-list.component'; @NgModule({ imports: [ BrowserModule, FormsModule, routing ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent ], providers: [ appRoutingProviders ], bootstrap: [ AppComponent ] }) export class AppModule { }
Providing the router module in our AppModule
makes the Router available everywhere in our application.
The AppComponent shell
The root AppComponent
is the application shell. It has a title at the top, a navigation bar with two links, and a Router Outlet at the bottom where the router swaps views on and off the page. Here's what we mean:
The corresponding component template looks like this:
template: ` <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> `
RouterOutlet
RouterOutlet
is a component from the router library. The router displays views within the bounds of the <router-outlet>
tags.
A template may hold exactly one unnamed <router-outlet>
. The router supports multiple named outlets, a feature we'll cover in future.
RouterLink binding
Above the outlet, within the anchor tags, we see Property Bindings to the RouterLink
directive that look like routerLink="..."
. We use the RouterLink
from the router library.
The links in this example each have a string path, the path of a route that we configured earlier. We don't have route parameters yet.
We can also add more contextual information to our RouterLink
by providing query string parameters or a URL fragment for jumping to different areas on our page. Query string parameters are provided through the [queryParams]
binding which takes an object (e.g. { name: 'value' }
), while the URL fragment takes a single value bound to the [fragment]
input binding.
Learn about the how we can also use the link parameters array in the appendix below.
RouterLinkActive binding
On each anchor tag, we also see Property Bindings to the RouterLinkActive
directive that look like routerLinkActive="..."
.
The template expression to the right of the equals (=) contains our space-delimited string of CSS classes. We can also bind to the RouterLinkActive
directive using an array of classes such as [routerLinkActive]="['...']"
.
The RouterLinkActive
directive toggles css classes for active RouterLink
s based on the current RouterState
. This cascades down through each level in our route tree, so parent and child router links can be active at the same time. To override this behavior, we can bind to the [routerLinkActiveOptions]
input binding with the { exact: true }
expression. By using { exact: true }
, a given RouterLink
will only be active if its URL is an exact match to the current URL.
Router Directives
RouterLink
, RouterLinkActive
and RouterOutlet
are directives provided by the Angular RouterModule
package. They are readily available for us to use in our template.
The current state of app.component.ts
looks like this:
app/app.component.ts (excerpt)
import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <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> ` }) export class AppComponent { }
"Getting Started" wrap-up
We've got a very basic, navigating app, one that can switch between two views when the user clicks a link.
We've learned how to
- load the router library
- add a nav bar to the shell template with anchor tags,
routerLink
androuterLinkActive
directives - add a
router-outlet
to the shell template where views will be displayed - configure the router module with
RouterModule.forRoot
- set the router to compose "HTML 5" browser URLs.
The rest of the starter app is mundane, with little interest from a router perspective. Here are the details for readers inclined to build the sample through to this milestone.
Our starter app's structure looks like this:
Here are the files discussed in this milestone
import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <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> ` }) export class AppComponent { }
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroListComponent } from './hero-list.component'; import { CrisisListComponent } from './crisis-list.component'; @NgModule({ imports: [ BrowserModule, FormsModule, routing ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent ], providers: [ appRoutingProviders ], bootstrap: [ AppComponent ] }) export class AppModule { }
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent } ]; export const appRoutingProviders: any[] = [ ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
import { Component } from '@angular/core'; @Component({ template: ` <h2>HEROES</h2> <p>Get your heroes here</p>` }) export class HeroListComponent { }
import { Component } from '@angular/core'; @Component({ template: ` <h2>CRISIS CENTER</h2> <p>Get your crisis here</p>` }) export class CrisisListComponent { }
<html> <head> <!-- Set the base href --> <base href="/"> <title>Router Sample</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="styles.css"> <!-- Polyfill(s) for older browsers --> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.js"></script> <script src="node_modules/reflect-metadata/Reflect.js"></script> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('app') .catch(function(err){ console.error(err); }); </script> </head> <body> <my-app>loading...</my-app> </body> </html>
Milestone #2: The Heroes Feature
We've seen how to navigate using the RouterLink
directive.
Now we'll learn some new tricks such as how to
- organize our app and routes into feature areas using modules
- navigate imperatively from one component to another
- pass required and optional information in route parameters
To demonstrate, we'll build out the Heroes feature.
The Heroes "feature area"
A typical application has multiple feature areas, each an island of functionality with its own workflow(s), dedicated to a particular business purpose.
We could continue to add files to the app/
folder. That's unrealistic and ultimately not maintainable. We think it's better to put each feature area in its own folder.
Our first step is to create a separate app/heroes/
folder and add Hero Management feature files there.
We won't be creative about it. Our example is pretty much a copy of the code and capabilities in the "Tutorial: Tour of Heroes".
Here's how the user will experience this version of the app
Add Heroes functionality
We want to break our app out into different feature modules that we then import into our main module so it can make use of them. First, we'll create a heroes.module.ts
in our heroes folder.
We delete the placeholder hero-list.component.ts
that's in the app/
folder.
We create a new hero-list.component.ts
in the app/heroes/
folder and copy over the contents of the final heroes.component.ts
from the tutorial. We copy the hero-detail.component.ts
and the hero.service.ts
files into the heroes/
folder.
We provide the HeroService
in the providers
array of our Heroes
module so its available to all components within our module.
Our Heroes
module is ready for routing.
app/heroes/heroes.module.ts (excerpt)
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ CommonModule, FormsModule ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] }) export class HeroesModule {}
When we're done organizing, we have four Hero Management files:
Now it's time for some surgery to bring these files and the rest of the app into alignment with our application router.
Hero feature routing requirements
The new Heroes feature has two interacting components, the list and the detail. The list view is self-sufficient; we navigate to it, it gets a list of heroes and displays them. It doesn't need any outside information.
The detail view is different. It displays a particular hero. It can't know which hero on its own. That information must come from outside.
In our example, when the user selects a hero from the list, we navigate to the detail view to show that hero. We'll tell the detail view which hero to display by including the selected hero's id in the route URL.
Hero feature route configuration
We recommend giving each feature area its own route configuration file.
Create a new heroes.routing.ts
in the heroes
folder like this:
app/heroes/heroes.routing.ts (excerpt)
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent }, { path: 'hero/:id', component: HeroDetailComponent } ]; export const heroesRouting: ModuleWithProviders = RouterModule.forChild(heroesRoutes);
We use the same techniques we learned for app.routing.ts
.
We import the two components from their new locations in the app/heroes/
folder, define the two hero routes. and add export our heroesRouting
that returns configured RouterModule
for our feature module.
Now that we have routes for our Heroes
module, we'll need to register them with the Router. We'll import the RouterModule like we did in the app.routing.ts
, but there is a slight difference here. In our app.routing.ts
, we used the static forRoot method to register our routes and application level service providers. In a feature module we use static forChild method.
The RouterModule.forRoot should only be provided for the AppModule
. Since we are in a feature module, we'll use RouterModule.forChild method to only register additional routes.
We import our heroesRouting
token from heroes.routing.ts
into our Heroes
module and register the routing.
app/heroes/heroes.module.ts (heroes routing)
import { heroesRouting } from './heroes.routing'; @NgModule({ imports: [ CommonModule, FormsModule, heroesRouting ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] })
Route definition with a parameter
The route to HeroDetailComponent
has a twist.
app/heroes/heroes.routing.ts (excerpt)
{ path: 'hero/:id', component: HeroDetailComponent }
Notice the :id
token in the path. That creates a slot in the path for a Route Parameter. In this case, we're expecting the router to insert the id
of a hero into that slot.
If we tell the router to navigate to the detail component and display "Magneta", we expect hero id
(15) to appear in the browser URL like this:
localhost:3000/hero/15
If a user enters that URL into the browser address bar, the router should recognize the pattern and go to the same "Magneta" detail view.
Route parameter: Required or optional?
Embedding the route parameter token, :id
, in the route definition path is a good choice for our 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.
An optional-route-parameter might be a better choice if we were passing an optional value to HeroDetailComponent
.
Navigate to hero detail imperatively
We won't navigate to the detail component by clicking a link so we won't be adding a new RouterLink
anchor tag to the shell.
Instead, when the user clicks a hero in the list, we'll command the router to navigate to the hero detail view for the selected hero.
We'll adjust the HeroListComponent
to implement these tasks, beginning with its constructor which acquires the router service and the HeroService
by dependency injection:
app/heroes/hero-list.component.ts (constructor)
constructor( private router: Router, private service: HeroService) { }
We make a few changes to the template:
template: ` <h2>HEROES</h2> <ul class="items"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> `
The template defines an *ngFor
repeater such as we've seen before. There's a (click)
EventBinding to the component's onSelect
method which we implement as follows:
app/heroes/hero-list.component.ts (select)
onSelect(hero: Hero) { this.router.navigate(['/hero', hero.id]); }
It calls the router's navigate
method with a Link Parameters Array. We can use this same syntax with a RouterLink
if we want to use it in HTML rather than code.
Setting the route parameters in the list view
We're navigating to the HeroDetailComponent
where we expect to see the details of the selected hero. We'll need two pieces of information: the destination and the hero's id
.
Accordingly, the link parameters array has two items: the path of the destination route and a route parameter that specifies the id
of the selected hero.
app/heroes/hero-list.component.ts (link-parameters-array)
['/hero', hero.id] // { 15 }
The router composes the appropriate two-part destination URL from this array:
localhost:3000/hero/15
Getting the route parameter in the details view
How does the target HeroDetailComponent
learn about that id
? Certainly not by analyzing the URL! That's the router's job.
The router extracts the route parameter (id:15
) from the URL and supplies it to the HeroDetailComponent
via the ActivatedRoute service.
ActivatedRoute: the one-stop-shop for route information
Each route contains information about its path, data parameters, URL segment and much more. All of this information is available in an injected service provided by the router called the ActivatedRoute.
The ActivatedRoute
contains all the information you need from the current route component as well as ways to get information about other activated routes in the RouterState
.
url
: An Observable
of the route path(s). The value is provided as an array of strings for each part of the route path.
data
: An Observable
that contains the data
object provided for the route. Also contains any resolved values from the resolve guard.
params
: An Observable
that contains the required and optional parameters specific to the route.
queryParams
: An Observable
that contains the query parameters available to all routes.
fragment
: An Observable
of the URL fragment available to all routes.
outlet
: The name of the RouterOutlet
used to render the route. For an unnamed outlet, the outlet name is primary.
routeConfig
: The route configuration used for the route that contains the origin path.
parent
: an ActivatedRoute
that contains the information from the parent route when using child routes.
firstChild
: contains the first ActivatedRoute
in the list of child routes.
children
: contains all the child routes activated under the current route.
We import the Router
, ActivatedRoute
, and Params
tokens from the router package.
app/heroes/hero-detail.component.ts (activated route)
import { Router, ActivatedRoute, Params } from '@angular/router';
As usual, we write a constructor that asks Angular to inject services that the component requires and reference them as private variables.
app/heroes/hero-detail.component.ts (constructor)
constructor( private route: ActivatedRoute, private router: Router, private service: HeroService) {}
Later, in the ngOnInit
method, we use the ActivatedRoute
service to retrieve the parameters for our route. Since our parameters are provided as an Observable
, we use the forEach method to retrieve them for the id
parameter by name and tell the HeroService
to fetch the hero with that id
.
app/heroes/hero-detail.component.ts (ngOnInit)
ngOnInit() { this.route.params.forEach((params: Params) => { let id = +params['id']; // (+) converts string 'id' to a number this.service.getHero(id).then(hero => this.hero = hero); }); }
Angular calls the ngOnInit
method shortly after creating an instance of the HeroDetailComponent
.
We put the data access logic in the ngOnInit
method rather than inside the constructor to improve the component's testability. We explore this point in greater detail in the OnInit appendix below.
Learn about the ngOnInit
method in the Lifecycle Hooks chapter.
Observable params and component re-use
In this example, we retrieve the route params from an Observable
. That implies that the route params can change during the lifetime of this component.
They might. By default, the router reuses a component instance when it re-navigates to the same component type without visiting a different component first. The parameters can change between each re-use.
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
.
We don't want the router to remove the current HeroDetailComponent
instance from the DOM only to re-create it for the next id
. That could be visibly jarring. Better to simply re-use the same component instance and update the parameter.
But ngOnInit
is only called once per instantiation. We need a way to detect when the route parameters change from within the same instance. The observable params
property handles that beautifully.
Snapshot: the no-observable alternative
This application won't reuse the HeroDetailComponent
. We always return to the hero list to select another hero to view. There's no way to navigate from hero detail to hero detail without visiting the list component in between. That means we get a new HeroDetailComponent
instance every time.
Suppose we know for certain that HeroDetailComponent
will never, never, ever be re-used. We'll always re-create the component each time we navigate to it.
The router offers a Snapshot alternative that gives us the initial value of the route parameters. We don't need to subscribe or unsubscribe. It's much simpler to write and read:
app/heroes/hero-detail.component.ts (ngOnInit snapshot)
ngOnInit() { // (+) converts string 'id' to a number let id = +this.route.snapshot.params['id']; this.service.getHero(id).then(hero => this.hero = hero); }
Remember: we only get the initial value of the parameters with this technique. Stick with the observable params
approach if there's even a chance that we might navigate to this component multiple times in a row. We are leaving the observable params
strategy in place just in case.
Navigating back to the list component
The HeroDetailComponent
has a "Back" button wired to its gotoHeroes
method that navigates imperatively back to the HeroListComponent
.
The router navigate
method takes the same one-item link parameters array that we can bind to a [routerLink]
directive. It holds the path to the HeroListComponent
:
app/heroes/hero-detail.component.ts (excerpt)
gotoHeroes() { this.router.navigate(['/heroes']); }
Route Parameters
We use route parameters to specify a required parameter value within the route URL as we do when navigating to the HeroDetailComponent
in order to view-and-edit the hero with id:15.
localhost:3000/hero/15
Sometimes we wish to add optional information to a route request. For example, the HeroListComponent
doesn't need help to display a list of heroes. But it might be nice if the previously-viewed hero were pre-selected when returning from the HeroDetailComponent
.
That becomes possible if we can include hero Magneta's id
in the URL when we return from the HeroDetailComponent
, a scenario we'll pursue in a moment.
Optional information takes other forms. Search criteria are often loosely structured, e.g., name='wind*'
. Multiple values are common — after='12/31/2015' & before='1/1/2017'
— in no particular order — before='1/1/2017' & after='12/31/2015'
— in a variety of formats — during='currentYear'
.
These kinds of parameters don't fit easily in a URL path. Even if we could define a suitable URL token scheme, doing so greatly complicates the pattern matching required to translate an incoming URL to a named route.
Optional parameters are the ideal vehicle for conveying arbitrarily complex information during navigation. Optional parameters aren't involved in pattern matching and affords enormous flexibility of expression.
The Router supports navigation with optional parameters as well as required route parameters. We define optional parameters in an object after we define our required route parameters.
Route Parameters: Required or Optional?
There is no hard-and-fast rule. In general,
prefer a required route parameter when
- the value is required.
- the value is necessary to distinguish one route path from another.
prefer an optional parameter when
- the value is optional, complex, and/or multi-variate.
Route parameter
When navigating to the HeroDetailComponent
we specified the required id
of the hero-to-edit in the route parameter and made it the second item of the link parameters array.
app/heroes/hero-list.component.ts (link-parameters-array)
['/hero', hero.id] // { 15 }
The router embedded the id
value in the navigation URL because we had defined it as a route parameter with an :id
placeholder token in the route path
:
app/heroes/heroes.routing.ts (hero-detail-route)
{ 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
.
app/heroes/hero-detail.component.ts (gotoHeroes)
gotoHeroes() { this.router.navigate(['/heroes']); }
This array lacks a route parameter because we had no reason to send information to the HeroListComponent
.
Now we have a reason. We'd like to send the id of the current hero with the navigation request so that the HeroListComponent
can highlight that hero in its list. This is a nice-to-have feature; the list will display perfectly well without it.
We do that with an object that contains an optional id
parameter. For demonstration purposes, we also defined a junk parameter (foo
) that the HeroListComponent
should ignore. Here's the revised navigation statement:
app/heroes/hero-detail.component.ts (go to heroes)
gotoHeroes() { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. 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.
When running in plunker, pop out the preview window by clicking the blue 'X' button in the upper right corner.
It should look something like this, depending on where you run it:
localhost:3000/heroes;id=15;foo=foo
The id
value appears in the URL as (;id=15;foo=foo
), not in the URL path. The path for the "Heroes" route doesn't have an :id
token.
The optional route parameters are not separated by "?" and "&" as they would be in the URL query string. They are separated by semicolons ";" This is matrix URL notation — something we may not have seen before.
Matrix URL notation is an idea first floated in a 1996 proposal by the founder of the web, Tim Berners-Lee.
Although matrix notation never made it into the HTML standard, it is legal and it became popular among browser routing systems as a way to isolate parameters belonging to parent and child routes. The Router is such a system and provides support for the matrix notation across browsers.
The syntax may seem strange to us but users are unlikely to notice or care as long as the URL can be emailed and pasted into a browser address bar as this one can.
Route parameters in the ActivatedRoute service
The list of heroes is unchanged. No hero row is highlighted.
The live example does highlight the selected row because it demonstrates the final state of the application which includes the steps we're about to cover. At the moment we're describing the state of affairs prior to those steps.
The HeroListComponent
isn't expecting any parameters at all and wouldn't know what to do with them. Let's change that.
Previously, when navigating from the HeroListComponent
to the HeroDetailComponent
, we subscribed to the route params Observable
and made it available to the HeroDetailComponent
in the ActivatedRoute
service. We injected that service in the constructor of the HeroDetailComponent
.
This time we'll be navigating in the opposite direction, from the HeroDetailComponent
to the HeroListComponent
.
First we extend the router import statement to include the ActivatedRoute
service symbol;
app/heroes/hero-list.component.ts (import)
import { Router, ActivatedRoute, Params } from '@angular/router';
Then we use the ActivatedRoute
to access the params
Observable so we can subscribe and extract the id
parameter as the selectedId
:
app/heroes/hero-list.component.ts (constructor)
private selectedId: number; constructor( private service: HeroService, private route: ActivatedRoute, private router: Router ) {} ngOnInit() { this.route.params.forEach((params: Params) => { this.selectedId = +params['id']; this.service.getHeroes() .then(heroes => this.heroes = heroes); }); }
All route/query parameters are strings. The (+) in front of the params['id']
expression is a JavaScript trick to convert the string to an integer.
We add an isSelected
method that returns true when a hero's id matches the selected id.
app/heroes/hero-list.component.ts (isSelected)
isSelected(hero: Hero) { return hero.id === this.selectedId; }
Finally, we update our template with a Class Binding to that isSelected
method. The binding adds the selected
CSS class when the method returns true
and removes it when false
. Look for it within the repeated <li>
tag as shown here:
app/heroes/hero-list.component.ts (template)
template: ` <h2>HEROES</h2> <ul class="items"> <li *ngFor="let hero of heroes" [class.selected]="isSelected(hero)" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> `
When the user navigates from the heroes list to the "Magneta" hero and back, "Magneta" appears selected:
The optional foo
route parameter is harmless and continues to be ignored.
Adding animations to the route component
Our heroes feature module is almost complete, but what is a feature without some smooth transitions? We already know that Angular supports animations and we want to take advantage of them by adding some animation to our Hero Detail component.
First, we'll start by importing our animation functions that build our animation triggers, control state and manage transitions between states. We'll use these functions to add transitions to our route component as it moves between states our application view. We'll also import the HostBinding
decorator for binding to our route component.
app/heroes/hero-detail.component.ts (animation imports)
import { Component, OnInit, HostBinding, trigger, transition, animate, style, state } from '@angular/core';
Next, we'll use a host binding for route animations named @routeAnimation. There is nothing special about the choice of the binding name, but since we are controlling route animation, we'll go with routeAnimation
. The binding value is set to true
because we only care about the *
and void
states which are entering and leaving animation states.
We'll also add some display and positioning bindings for styling.
app/heroes/hero-detail.component.ts (route animation binding)
export class HeroDetailComponent implements OnInit { @HostBinding('@routeAnimation') get routeAnimation() { return true; } @HostBinding('style.display') get display() { return 'block'; } @HostBinding('style.position') get position() { return 'absolute'; } hero: Hero; constructor( private route: ActivatedRoute, private router: Router, private service: HeroService) {} ngOnInit() { this.route.params.forEach((params: Params) => { let id = +params['id']; // (+) converts string 'id' to a number this.service.getHero(id).then(hero => this.hero = hero); }); } gotoHeroes() { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]); } }
Now we can build our animation trigger, which we'll call routeAnimation to match the binding we previously setup. We'll use the wildcard state that matches any animation state our route component is in, along with two transitions. One transition animates the component as it enters the application view (void => *
), while the other animates the component as it leaves the application view (* => void
).
We could add different transitions to different route components depending on our needs. We'll just animate our HeroDetailComponent
for this milestone.
Using route animations on individual components is something we don't want to do throughout our entire application. It would be better to animate routes based on route paths, a topic to cover in a future update to this chapter.
Our route component animation looks as such:
app/heroes/hero-detail.component.ts (route animation)
@Component({ template: ` <h2>HEROES</h2> <div *ngIf="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()">Back</button> </p> </div> `, animations: [ trigger('routeAnimation', [ state('*', style({ opacity: 1, transform: 'translateX(0)' }) ), transition('void => *', [ style({ opacity: 0, transform: 'translateX(-100%)' }), animate('0.2s ease-in') ]), transition('* => void', [ animate('0.5s ease-out', style({ opacity: 0, transform: 'translateY(100%)' })) ]) ]) ] }) export class HeroDetailComponent implements OnInit {
Simply stated, our HeroDetailComponent
will ease in from the left when routed to and will slide down when navigating away. We could add more complex animations here, but we'll leave our HeroDetailComponent
as is for now.
Import hero module into AppModule
Our heroes feature module is ready, but application doesn't know about our heroes module yet. We'll need to import it into the AppModule
we defined in app.module.ts
.
Update app.module.ts
as follows:
app/app.module.ts (heroes module import)
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisListComponent } from './crisis-list.component'; @NgModule({ imports: [ BrowserModule, FormsModule, routing, HeroesModule ], declarations: [ AppComponent, CrisisListComponent ], providers: [ appRoutingProviders ], bootstrap: [ AppComponent ] })
We imported the HeroesModule
and added it to our AppModule
's imports
.
We removed the HeroListComponent
from the AppModule
's declarations
because its being provided by the HeroesModule
now. This is important because their can be only one owner for a declared component. In our case, the Heroes
module is the owner of the Heroes
components and is making them available to the AppModule
.
Routes provided by feature modules will be combined together into their imported module's routes by the router. This allows us to continue defining our feature module routes without modifying our main route configuration.
As a result, the AppModule
no longer has specific knowledge of the hero feature, its components, or its route details. We can evolve the hero feature with more components and different routes. That's a key benefit of creating a separate module for each feature area.
Since our Heroes
routes are defined within our feature module, we can also remove our initial heroes
route from the app.routing.ts
.
app/app.routing.ts (v2)
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent } ]; export const appRoutingProviders: any[] = [ ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
Heroes App Wrap-up
We've reached the second milestone in our router education.
We've learned how to
- organize our app into feature areas
- navigate imperatively from one component to another
- pass information along in route parameters and subscribe to them in our component
- import our feature area NgModule into our
AppModule
- apply animations to our route component
After these changes, the folder structure looks like this:
The Heroes App code
Here are the relevant files for this version of the sample application.
import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <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> ` }) export class AppComponent { }
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisListComponent } from './crisis-list.component'; @NgModule({ imports: [ BrowserModule, FormsModule, routing, HeroesModule ], declarations: [ AppComponent, CrisisListComponent ], providers: [ appRoutingProviders ], bootstrap: [ AppComponent ] }) export class AppModule { }
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent } ]; export const appRoutingProviders: any[] = [ ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
// TODO SOMEDAY: Feature Componetized like CrisisCenter import { Component, OnInit } from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { Hero, HeroService } from './hero.service'; @Component({ template: ` <h2>HEROES</h2> <ul class="items"> <li *ngFor="let hero of heroes" [class.selected]="isSelected(hero)" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> ` }) export class HeroListComponent implements OnInit { heroes: Hero[]; private selectedId: number; constructor( private service: HeroService, private route: ActivatedRoute, private router: Router ) {} ngOnInit() { this.route.params.forEach((params: Params) => { this.selectedId = +params['id']; this.service.getHeroes() .then(heroes => this.heroes = heroes); }); } isSelected(hero: Hero) { return hero.id === this.selectedId; } onSelect(hero: Hero) { this.router.navigate(['/hero', hero.id]); } }
import { Component, OnInit, HostBinding, trigger, transition, animate, style, state } from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { Hero, HeroService } from './hero.service'; @Component({ template: ` <h2>HEROES</h2> <div *ngIf="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()">Back</button> </p> </div> `, animations: [ trigger('routeAnimation', [ state('*', style({ opacity: 1, transform: 'translateX(0)' }) ), transition('void => *', [ style({ opacity: 0, transform: 'translateX(-100%)' }), animate('0.2s ease-in') ]), transition('* => void', [ animate('0.5s ease-out', style({ opacity: 0, transform: 'translateY(100%)' })) ]) ]) ] }) export class HeroDetailComponent implements OnInit { @HostBinding('@routeAnimation') get routeAnimation() { return true; } @HostBinding('style.display') get display() { return 'block'; } @HostBinding('style.position') get position() { return 'absolute'; } hero: Hero; constructor( private route: ActivatedRoute, private router: Router, private service: HeroService) {} ngOnInit() { this.route.params.forEach((params: Params) => { let id = +params['id']; // (+) converts string 'id' to a number this.service.getHero(id).then(hero => this.hero = hero); }); } gotoHeroes() { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]); } }
import { Injectable } from '@angular/core'; export class Hero { constructor(public id: number, public name: string) { } } let HEROES = [ new Hero(11, 'Mr. Nice'), new Hero(12, 'Narco'), new Hero(13, 'Bombasto'), new Hero(14, 'Celeritas'), new Hero(15, 'Magneta'), new Hero(16, 'RubberMan') ]; let heroesPromise = Promise.resolve(HEROES); @Injectable() export class HeroService { getHeroes() { return heroesPromise; } getHero(id: number | string) { return heroesPromise .then(heroes => 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.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; import { heroesRouting } from './heroes.routing'; @NgModule({ imports: [ CommonModule, FormsModule, heroesRouting ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] }) export class HeroesModule {}
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent }, { path: 'hero/:id', component: HeroDetailComponent } ]; export const heroesRouting: ModuleWithProviders = RouterModule.forChild(heroesRoutes);
Milestone #3: The Crisis Center
The Crisis Center is a fake view at the moment. Time to make it useful.
The new Crisis Center begins as a virtual copy of the Heroes module. We create a new app/crisis-center
folder, copy the Hero files, and change every mention of "hero" to "crisis".
A Crisis
has an id
and name
, just like a Hero
The new CrisisListComponent
displays lists of crises. When the user selects a crisis, the app navigates to the CrisisDetailComponent
for display and editing of the crisis name.
Voilà, another feature module!
There's no point to this exercise unless we can learn something. We do have new ideas and techniques in mind:
-
We'd like our route URLs to branch in to child route trees that reflect the component trees in our feature areas.
-
The application should navigate to the Crisis Center by default.
-
The router should prevent navigation away from the detail view while there are pending changes.
-
The user should be able to cancel unwanted changes.
-
The router should block access to certain features until the user logs-in.
-
Our
CrisisService
is only needed within the Crisis Center module. We should limit access to it to that module. -
Changes to a feature module such as Crisis Center shouldn't provoke changes to the
AppModule
or any other feature's component. We need to separate our concerns.
We'll address all of these issues in the Crisis Center starting with the introduction of child routes
We'll leave Heroes in its less-than-perfect state to serve as a contrast with what we believe to be a superior Crisis Center design.
A Crisis Center with child routes
We'll organize the Crisis Center to conform to the following recommended pattern for Angular applications.
- each feature area in its own folder within a defined module
- each area with its own area root component
- each area root component with its own router-outlet and child routes
- area routes rarely (if ever) cross
If we had many feature areas, their component trees might look like this:
Child Routing Component
Add the following crisis-center.component.ts
to the crisis-center
folder:
app/crisis-center/crisis-center.component.ts (minus imports)
@Component({ template: ` <h2>CRISIS CENTER</h2> <router-outlet></router-outlet> ` }) export class CrisisCenterComponent { }
The CrisisCenterComponent
is much like the AppComponent
shell.
-
It is the root of the Crisis Center area just as
AppComponent
is the root of the entire application. -
It is a shell for the crisis management feature area just as the
AppComponent
is a shell to manage the high-level workflow. -
It is dead simple — simpler even than the
AppComponent
template. It has no content, no links, just a<router-outlet>
for the Crisis Center child views.
Unlike AppComponent
(and most other components), it lacks a selector. It doesn't need one. We don't embed this component in a parent template. We navigate to it from the outside, via the router.
We can give it a selector. There's no harm in it. Our point is that we don't need one because we only navigate to it.
Service isolation
The CrisisService
is neither needed nor wanted outside the Crisis Center domain. Instead of registering it with the AppModule
's providers — which makes it visible everywhere — we register the CrisisService
in the CrisisCenterModule
providers array.
providers: [ CrisisService ]
This limits the scope of the CrisisService
to the Crisis Center routes. No module outside of the Crisis Center can access it.
There's a practical benefit to restricting its scope in this way.
First we can evolve the service independently of the rest of the application without fear of breaking what should be unrelated modules.
Second, we can delay loading this service into memory until we need it. We can remove it from the application launch bundle, reducing the size of the initial payload and improving performance. We can load it optionally, asynchronously with the other Crisis Center components if and when the user begins that workflow.
Child Route Configuration
The CrisisCenterComponent
is a Routing Component like the AppComponent
. It has its own RouterOutlet
and its own child routes.
Add the following crisis-center-home.component.ts
to the crisis-center
folder.
app/crisis-center/crisis-center-home.component.ts (minus imports)
@Component({ template: ` <p>Welcome to the Crisis Center</p> ` }) export class CrisisCenterHomeComponent { }
We create a crisis-center.routing.ts
file as we did the heroes.routing.ts
file. But this time we define child routes within the parent crisis-center
route.
app/crisis-center/crisis-center.routing.ts (Routes)
const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);
Notice that the parent crisis-center
route has a children
property with a single route containing our CrisisListComponent
. The CrisisListComponent
route also has a children
array with two routes.
These two routes navigate to the two Crisis Center child components, CrisisCenterHomeComponent
and CrisisDetailComponent
.
There are some important differences in the treatment of these 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
. Since the router reuses components by default, the Crisis Detail
component will be re-used as we select different crises.
In contrast, back in the Hero Detail
route, the component was recreated each time we selected a different hero.
At the top level, paths that begin with /
refer to the root of the application. But these are child routes. They extend the path of the parent route. With each step down the route tree, we add a slash followed by the route path (unless the route path is empty).
For example, the parent path to the CrisisCenterComponent
is /crisis-center
The router appends these child paths to the parent path to the
CrisisCenterComponent(
/crisis-center).
-
to navigate to the
CrisisCenterHomeComponent, the full URL is
/crisis-center(/crisis-center
+''
+''
). -
to navigate to the
CrisisDetailComponent
for a crisis withid=2
, the full URL is/crisis-center/2
(/crisis-center+
''+
'/2'`).
The absolute URL for the latter example, including the origin, is
localhost:3000/crisis-center/2
Here's the complete crisis-center.routing.ts
file with its imports.
app/crisis-center/crisis-center.routing.ts (excerpt)
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);
Import crisis center module into the AppModule routes
As with the Heroes
module, we must import the Crisis Center
module into the AppModule
:
app/app.module.ts (Crisis Center Module)
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ CommonModule, FormsModule, routing, HeroesModule, CrisisCenterModule, ], declarations: [ AppComponent ], providers: [ appRoutingProviders, DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }
We also remove the initial crisis center route from our app.routing.ts
. Our routes are now being provided by our HeroesModule
and our CrisisCenter
feature modules. We'll keep our app.routing.ts
file for general routes which we'll cover later in the chapter.
app/app.routing.ts (v3)
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const appRoutes: Routes = [ ]; export const appRoutingProviders: any[] = [ ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
Redirecting routes
When the application launches, the initial URL in the browser bar is something like:
localhost:3000
That doesn't match any of our configured routes which means that our application won't display any component when it's launched. The user must click one of the navigation links to trigger a navigation and display something.
We prefer that the application display the list of crises as it would if the user clicked the "Crisis Center" link or pasted localhost:3000/crisis-center/
into the address bar. This is our intended default route.
The preferred solution is to add a redirect
route that transparently translates from the initial relative URL (''
) to the desired default path (/crisis-center
):
{ path: '', redirectTo: '/crisis-center', pathMatch: 'full' },
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 CrisisListComponent
when the entire URL matches ''
, so we set the pathMatch
value to 'full'
.
Technically, pathMatch = 'full'
results in a route hit when the remaining, unmatched segments of the URL match ''
. In our example, the redirect is at the top level of the route configuration tree 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.
That's not what we want to do here. If the pathMatch
value were 'prefix'
, every URL would match ''
. We could never navigate to /crisis-center/1
because the redirect route would match first and send us to the CrisisListComponent
.
We should redirect to the CrisisListComponent
only when the entire (remaining) url is ''
.
Learn more in Victor Savkin's blog post on redirects.
We'll discuss redirects in more detail in a future update to this chapter.
The updated route definitions look like this:
app/crisis-center/crisis-center.routing.ts (routes v2)
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; const crisisCenterRoutes: Routes = [ { path: '', redirectTo: '/crisis-center', pathMatch: 'full' }, { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);
Relative Navigation
While building out our Crisis Center feature, we've navigated to the Crisis Detail route using an absolute path that begins with a slash. This navigation starts from the top of our route configuration to find the matching path to our route.
We could continue to use absolute paths to navigate inside our Crisis Center feature, but that makes our links very rigid. If we changed our parent /crisis-center
path, we would have to change our link parameters array.
We can make our links more flexible by using relative navigation with the router.
- The full path to the route is not required.
- Navigation within our feature area remains intact if the parent route path is changed.
- The link parameters array only contains navigation relative to the current URL.
The link parameters array supports a directory-like syntax for relative navigation.
./
or no leading slash
is relative to the current level.
../
to go up one level in the route path.
The relative navigation syntax can be used in combination with a path. If we wanted to navigate from one route path to another sibling route path we could use ../path
convention to go up one level and down to the sibling route path.
In order to navigate relatively using the Router
service, we use the ActivatedRoute
to give the router knowledge of where we are in the RouterState, which is our tree of activated routes. We do this by adding an object as the second argument in our router.navigate
method after the link parameters array specifying the relativeTo property. We set the relativeTo
property to our ActivatedRoute
and the router will merge our navigation information into to the current URL.
When using router's navigateByUrl
method, the navigation is always absolute.
Navigate to Crisis Detail relatively
Let's update our Crisis List onSelect
method to use relative navigation so we don't have to start from the top of our route configuration. We've already injected the ActivatedRoute
into our constructor that we'll need for the relative navigation.
app/crisis-center/crisis-list.component.ts (constructor)
constructor( private service: CrisisService, private route: ActivatedRoute, private router: Router) {}
When we visit the Crisis Center, our path is /crisis-center
, so we just want to add the id
of the Crisis Center to our existing path. When the router navigates, it will use the current path /crisis-center
, adding on our id
. If our id
were 1
, the resulting path would be /crisis-center/1
.
app/crisis-center/crisis-list.component.ts (relative navigation)
onSelect(crisis: Crisis) { this.selectedId = crisis.id; // Navigate with relative link this.router.navigate([crisis.id], { relativeTo: this.route }); }
We'll also update the Crisis Detail component to navigate back to our Crisis Center list. We want to go back up a level in the path, so we use to the ../
syntax. If our current id
is 1
, the resulting path coming from /crisis-center/1
would be /crisis-center
.
app/crisis-center/crisis-detail.component.ts (relative navigation)
gotoCrises() { let 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 }); }
If we are using a RouterLink
to navigate instead of the Router
service, we can use the same link parameters array, but we don't have to provide the object with the relativeTo
property. The ActivatedRoute
is implicit in the RouterLink
directive.
app/crisis-center/crisis-list.component.ts (relative routerLink)
<a [routerLink]="[crisis.id]">{{ crisis.name }}</a>
Route Guards
Milestone #4: Route Guards
At the moment, any user can navigate anywhere in the application anytime.
That's not always the right thing to do.
- Perhaps the user is not authorized to navigate to the target component.
- Maybe the user must login (authenticate) first.
- Maybe we should fetch some data before we display the target component.
- We might want to save pending changes before leaving a component.
- We might ask the user if it's OK to discard pending changes rather than save them.
We can add guards to our route configuration to handle these scenarios.
A guard's return value controls the router's behavior:
- if it returns
true
, the navigation process continues - if it returns
false
, the navigation process stops and the user stays put
The guard can also tell the router to navigate elsewhere, effectively canceling the current navigation.
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
.
The router supports multiple kinds of guards:
-
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.
We can have multiple guards at every level of a routing hierarchy. The router checks the CanDeactivate
and CanActivateChild
guards first, from 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.
Let's look at some examples.
CanActivate: requiring authentication
Applications often restrict access to a feature area based on who the user is. We could permit access only to authenticated users or to users with a specific role. We might block or limit access until the user's account is activated.
The CanActivate
guard is the tool to manage these navigation business rules.
Add an admin feature module
We intend to extend the Crisis Center with some new administrative features. Those features aren't defined yet. So we add a new feature module named AdminModule
. We'll follow our same convention by creating an admin
folder with a feature module file, route file and supporting components.
Our admin feature module file structure looks like this:
Our admin feature module contains our AdminComponent
used for routing within our feature module, a dashboard route and two unfinished components to manage crises and heroes.
import { Component } from '@angular/core'; @Component({ template: ` <p>Dashboard</p> ` }) export class AdminDashboardComponent { }
import { Component } from '@angular/core'; @Component({ template: ` <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> ` }) export class AdminComponent { }
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AdminComponent } from './admin.component'; import { AdminDashboardComponent } from './admin-dashboard.component'; import { ManageCrisesComponent } from './manage-crises.component'; import { ManageHeroesComponent } from './manage-heroes.component'; import { adminRouting } from './admin.routing'; @NgModule({ imports: [ CommonModule, adminRouting ], declarations: [ AdminComponent, AdminDashboardComponent, ManageCrisesComponent, ManageHeroesComponent ] }) export class AdminModule {}
import { Component } from '@angular/core'; @Component({ template: ` <p>Manage your crises here</p> ` }) export class ManageCrisesComponent { }
import { Component } from '@angular/core'; @Component({ template: ` <p>Manage your heroes here</p> ` }) export class ManageHeroesComponent { }
Since our admin dashboard RouterLink
is an empty path route in our AdminModule
, it is considered a match to any route within our admin feature area. We only want the Dashboard
link to be active when we visit that route. We've added an additional binding to our Dashboard
routerLink, [routerLinkActiveOptions]="{ exact: true }"
which will only mark the ./
link as active when we navigate the to /admin
URL and not when we navigate to one the other child routes.
Our initial admin routing configuration:
app/admin/admin.routing.ts (admin routing)
const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; export const adminRouting: ModuleWithProviders = RouterModule.forChild(adminRoutes);
Component-Less Route: grouping routes without a component
Looking at our child route under the AdminComponent
, we have a route with a path and a children property but it's not using a component. We haven't made a mistake in our configuration, because we can use a component-less route.
We want to group our Crisis Center
management routes under the admin
path, but we don't need a component just to group those routes under an additional RouterOutlet
. This also allows us to guard child routes.
Next, we'll import the AdminModule
into our app.module.ts
and add it to the imports
array to register our admin routes.
app/app.module.ts (admin module)
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { AdminModule } from './admin/admin.module'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ CommonModule, FormsModule, routing, HeroesModule, CrisisCenterModule, AdminModule ], declarations: [ AppComponent ], providers: [ appRoutingProviders, DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }
And we add a link to the AppComponent
shell that users can click to get to this feature.
app/app.component.ts (template)
template: ` <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> </nav> <router-outlet></router-outlet> `
Guard the admin feature
Currently every route within our Crisis Center is open to everyone. The new admin feature should be accessible only to authenticated users.
We could hide the link until the user logs in. But that's tricky and difficult to maintain.
Instead we'll write a CanActivate
guard to redirect anonymous users to the login page when they try to reach the admin component.
This is a general purpose guard — we can imagine other features that require authenticated users — so we create an auth-guard.service.ts
in the application root folder.
At the moment we're interested in seeing how guards work so our first version does nothing useful. It simply logs to console and returns
true immediately, allowing navigation to proceed:
app/auth-guard.service.ts (excerpt)
import { Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { canActivate() { console.log('AuthGuard#canActivate called'); return true; } }
Next we open admin.routing.ts
, import the AuthGuard
class, and update the admin route with a CanActivate
guard property that references it:
app/admin/admin.routing.ts (guarded admin route)
import { AuthGuard } from '../auth-guard.service'; const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ], } ] } ]; export const adminRouting: ModuleWithProviders = RouterModule.forChild(adminRoutes);
Our admin feature is now protected by the guard, albeit protected poorly.
Teach AuthGuard to authenticate
Let's make our AuthGuard
at least pretend to authenticate.
The AuthGuard
should call an application service that can login a user and retain information about the current user. Here's a demo AuthService
:
app/auth.service.ts (excerpt)
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/delay'; @Injectable() export class AuthService { isLoggedIn: boolean = false; // store the URL so we can redirect after logging in redirectUrl: string; login(): Observable<boolean> { return Observable.of(true).delay(1000).do(val => this.isLoggedIn = true); } logout(): void { this.isLoggedIn = false; } }
Although it doesn't actually log in, it has what we need for this discussion. It has an isLoggedIn
flag to tell us 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 will store our attempted URL so we can navigate to it after authenticating.
Let's revise our AuthGuard
to call it.
app/auth-guard.service.ts (v2)
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Navigate to the login page with extras this.router.navigate(['/login']); return false; } }
Notice that we inject the AuthService
and the Router
in the constructor. We haven't provided the AuthService
yet but it's good to know that we can inject helpful services into our 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 our application, should we pass through our guard check.
If the user is not logged in, we store the attempted URL the user came from using the RouterStateSnapshot.url
and tell the router to navigate to a login page — a page we haven't created yet. This secondary navigation automatically cancels the current navigation; we return false
just to be clear about that.
Add the LoginComponent
We need a LoginComponent
for the user to log in to the app. After logging in, we'll redirect to our stored URL if available, or use the default URL. There is nothing new about this component or the way we wire it into the router configuration.
We'll register a /login
route in our app.routing.ts
and add the necessary providers to the appRoutingProviders
array we created earlier. In our app.module.ts
, we'll import the LoginComponent
and add it to our AppModule
declarations
.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { LoginComponent } from './login.component'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ BrowserModule, FormsModule, routing, HeroesModule, CrisisCenterModule ], declarations: [ AppComponent, LoginComponent ], providers: [ appRoutingProviders, DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { loginRoutes, authProviders } from './login.routing'; import { CanDeactivateGuard } from './can-deactivate-guard.service'; const appRoutes: Routes = [ ...loginRoutes ]; export const appRoutingProviders: any[] = [ authProviders, CanDeactivateGuard ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ template: ` <h2>LOGIN</h2> <p>{{message}}</p> <p> <button (click)="login()" *ngIf="!authService.isLoggedIn">Login</button> <button (click)="logout()" *ngIf="authService.isLoggedIn">Logout</button> </p>` }) 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) { // Get the redirect URL from our auth service // If no redirect has been set, use the default let redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/crisis-center/admin'; // Redirect the user this.router.navigate([redirect]); } }); } logout() { this.authService.logout(); this.setMessage(); } }
import { Routes } from '@angular/router'; import { AuthGuard } from './auth-guard.service'; import { AuthService } from './auth.service'; import { LoginComponent } from './login.component'; export const loginRoutes: Routes = [ { path: 'login', component: LoginComponent } ]; export const authProviders = [ AuthGuard, AuthService ];
Guards and the service providers they require must be provided at the module-level. This allows the Router access to retrieve these services from the Injector
during the navigation process. The same rule applies for feature modules loaded asynchronously.
CanActivateChild: guarding child routes
As we learned about guarding routes with CanActivate
, we can also protect child routes with the CanActivateChild
guard. The CanActivateChild
guard works similarly to the CanActivate
guard, but the difference is its run before each child route is activated. We protected our admin feature module from unauthorized access, but we could also protect child routes within our feature module.
Let's extend our AuthGuard
to protect when navigating between our admin
routes. First we'll open our auth-guard.service.ts
and add CanActivateChild
interface to our imported tokens from the router package.
Next, we'll implement the canActivateChild
method with takes the same arguments as the canActivate
method, an ActivatedRouteSnapshot
and RouterStateSnapshot
. The canActivateChild
behaves the same way the other guards do, returning an Observable<boolean>
or Promise<boolean>
for async checks and boolean
for sync checks. We'll return a boolean
app/auth-guard.service.ts (excerpt)
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } }
We add the same AuthGuard
to our component-less
admin route to protect all other child routes at one time instead of adding the AuthGuard
to each route individually.
app/admin/admin.routing.ts (excerpt)
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 } ] } ] } ]; export const adminRouting: ModuleWithProviders = RouterModule.forChild(adminRoutes);
CanDeactivate: handling unsaved changes
Back in the "Heroes" workflow, the app accepts every change to a hero immediately without hesitation or validation.
In the real world, we might have to accumulate the users changes. We might have to validate across fields. We might have to validate on the server. We might have to hold changes in a pending state until the user confirms them as a group or cancels and reverts all changes.
What do we do about unapproved, unsaved changes when the user navigates away? We can't just leave and risk losing the user's changes; that would be a terrible experience.
We'd like to pause and let the user decide what to do. If the user cancels, we'll stay put and allow more changes. If the user approves, the app can save.
We still might delay navigation until the save succeeds. If we let the user move to the next screen immediately and the save failed (perhaps the data are ruled invalid), we would have lost the context of the error.
We can't block while waiting for the server — that's not possible in a browser. We need to stop the navigation while we wait, asynchronously, for the server to return with its answer.
We need the CanDeactivate
guard.
Cancel and Save
Our sample application doesn't talk to a server. Fortunately, we have another way to demonstrate an asynchronous router hook.
Users update crisis information in the CrisisDetailComponent
. Unlike the HeroDetailComponent
, the user changes do not update the crisis entity immediately. We update the entity when the user presses the Save button. We discard the changes if the user presses he Cancel button.
Both buttons navigate back to the crisis list after save or cancel.
app/crisis-center/crisis-detail.component.ts (excerpt)
export class CrisisDetailComponent implements OnInit { @HostBinding('@routeAnimation') get routeAnimation() { return true; } @HostBinding('style.display') get display() { return 'block'; } @HostBinding('style.position') get position() { return 'absolute'; } crisis: Crisis; editName: string; cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); } }
What if the user tries to navigate away without saving or canceling? The user could push the browser back button or click the heroes link. Both actions trigger a navigation. Should the app save or cancel automatically?
We'll do neither. Instead we'll ask the user to make that choice explicitly in a confirmation dialog box that waits asynchronously for the user's answer.
We could wait for the user's answer with synchronous, blocking code. Our app will be more responsive ... and can do other work ... by waiting for the user's answer asynchronously. Waiting for the user asynchronously is like waiting for the server asynchronously.
The DialogService
(provided in the AppModule
for app-wide use) does the asking.
It returns a promise 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
).
We create a Guard
that will check for the presence of a canDeactivate
function in our component, in this case being CrisisDetailComponent
. We don't need to know the details of how our CrisisDetailComponent
confirms deactivation. This makes our guard reusable, which is an easy win for us.
app/can-deactivate-guard.service.ts
import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs/Observable'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable() export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; } }
Alternatively, We could make a component-specific CanDeactivate
guard for our CrisisDetailComponent
. The canDeactivate
method provides us with the current instance of our component
, the current ActivatedRoute
and RouterStateSnapshot
in case we needed to access some external information. This would be useful if we only wanted to use this guard for this component and needed to ask the component's properties in or to confirm whether the router should allow navigation away from it.
app/can-deactivate-guard.service.ts (component-specific)
import { Injectable } from '@angular/core'; import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { CrisisDetailComponent } from './crisis-center/crisis-detail.component'; @Injectable() export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> { canDeactivate( component: CrisisDetailComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Promise<boolean> | boolean { // Get the Crisis Center ID console.log(route.params['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 // promise which resolves to true or false when the user decides return component.dialogService.confirm('Discard changes?'); } }
Looking back at our CrisisDetailComponent
, we have implemented our confirmation workflow for unsaved changes.
app/crisis-center/crisis-detail.component.ts (excerpt)
canDeactivate(): Promise<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 // promise 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 falsey (stay put).
We add the Guard
to our crisis detail route in crisis-center.routing.ts
using the canDeactivate
array.
app/crisis-center/crisis-center.routing.ts (can deactivate guard)
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate-guard.service'; const crisisCenterRoutes: Routes = [ { path: '', redirectTo: '/crisis-center', pathMatch: 'full' }, { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard] }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);
We also need to add the Guard
to our main appRoutingProviders
so the Router
can inject it during the navigation process.
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { loginRoutes, authProviders } from './login.routing'; import { CanDeactivateGuard } from './can-deactivate-guard.service'; const appRoutes: Routes = [ ...loginRoutes ]; export const appRoutingProviders: any[] = [ authProviders, CanDeactivateGuard ]; export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
Now we have given our user a safeguard against unsaved changes.
Resolve: pre-fetching component data
In our Hero Detail
and Crisis Detail
, we waited until the route was activated to fetch our respective hero or crisis.
This worked well for us, but we can always do better. If we were using a real world api, there may be some delay in when the data we want to display gets returned. We don't want to display a blank component until the data loads in this situation.
We'd like to pre-fetch data from the server so it's ready the moment our route is activated. We'd also like to handle the situation where our data fails to load or some other error condition occurs. This would help us in our Crisis Center
if we navigated to an id
that doesn't return a record. We could send the user back to the Crisis List
where we only show valid crisis centers. We want to delay rendering of our route component until all necessary data has been fetched or some action has occurred.
We need the Resolve
guard.
Preload route information
We'll update our Crisis Detail
route to resolve our Crisis before loading the route, or if the user happens to navigate to an invalid crisis center :id
, we'll navigate back to our list of existing crises.
Like the CanActivate
and CanDeactivate
guards, the Resolve
guard is an interface we can implement as a service to resolve route data synchronously or asynchronously. In CrisisDetailComponent
, we used the ngOnInit
to retrieve the Crisis
information. We also navigated the user away from the route if the Crisis
was not found. It would be more efficient to perform this action before the route is ever activated.
We'll create a CrisisDetailResolve
service that will handle retrieving the Crisis
and navigating the user away if the Crisis
does not exist. Then we can be assured that when we activate the CrisisDetailComponent
, the associated Crisis will already be available for display.
Let's create our crisis-detail-resolve.service.ts
file within our Crisis Center
feature area.
app/crisis-center/crisis-detail-resolve.service.ts
import { Injectable } from '@angular/core'; import { Router, Resolve, ActivatedRouteSnapshot } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; @Injectable() export class CrisisDetailResolve implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot): Promise<Crisis>|boolean { let id = +route.params['id']; return this.cs.getCrisis(id).then(crisis => { if (crisis) { return crisis; } else { // id not found this.router.navigate(['/crisis-center']); return false; } }); } }
We'll take the relevant parts of the ngOnInit
lifecycle hook in our CrisisDetailComponent
and moved them into our CrisisDetailResolve
guard. We import the Crisis
model and CrisisService
and also the Router
for navigation from our resolve implementation. We want to be explicit about the data we are resolving, so we implement the Resolve
interface with a type of Crisis
. This lets us know that what we will resolve will match our Crisis
model. We inject the CrisisService
and Router
and implement the resolve
method that supports a Promise
, Observable
or a synchronous return value.
We'll use our CrisisService.getCrisis
method that returns a promise to prevent our route from loading until the data is fetched. If we don't find a valid Crisis
, we navigate the user back to the CrisisList
, canceling the previous in-flight navigation to the crisis details.
Now that our guard is ready, we'll import it in our crisis-center.routing.ts
and use the resolve
object in our route configuration.
app/crisis-center/crisis-center.routing.ts (resolve)
import { CrisisDetailResolve } from './crisis-detail-resolve.service'; const crisisCenterRoutes: Routes = [ { path: '', redirectTo: '/crisis-center', pathMatch: 'full' }, { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolve } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);
We'll add the CrisisDetailResolve
service to our crisis center module's providers
, so its available to the Router
during the navigation process.
app/crisis-center/crisis-center.module.ts (crisis detail resolve provider)
import { CrisisDetailResolve } from './crisis-detail-resolve.service'; providers: [ CrisisService, CrisisDetailResolve ]
Now that we've added our Resolve
guard to fetch data before the route loads, we no longer need to do this once we get into our CrisisDetailComponent
. We'll update the CrisisDetailComponent
to use the ActivatedRoute
data, which is where our crisis
property from our Resolve
guard will be provided. Once activated, all we need to do is set our local crisis
and editName
properties from our resolved Crisis
information. The Crisis
is being provided at the time the route component is activated.
app/crisis-center/crisis-detail.component.ts (ngOnInit v2)
ngOnInit() { this.route.data.forEach((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); }
Two critical points
-
The router interface is optional. We don't inherit from a base class. We simply implement the interface method or not.
-
We rely on the router to call the guard. We don't worry about all the ways that the user could navigate away. That's the router's job. We simply write this class and let the router take it from there.
The relevant Crisis Center code for this milestone is
import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` <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="/login" routerLinkActive="active">Login</a> </nav> <router-outlet></router-outlet> ` }) export class AppComponent { }
// #docplaster import { Component } from '@angular/core'; @Component({ template: ` <p>Welcome to the Crisis Center</p> ` }) export class CrisisCenterHomeComponent { }
// #docplaster import { Component } from '@angular/core'; @Component({ template: ` <h2>CRISIS CENTER</h2> <router-outlet></router-outlet> ` }) export class CrisisCenterComponent { }
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.component'; import { CrisisListComponent } from './crisis-list.component'; import { CrisisCenterComponent } from './crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate-guard.service'; import { CrisisDetailResolve } from './crisis-detail-resolve.service'; const crisisCenterRoutes: Routes = [ { path: '', redirectTo: '/crisis-center', pathMatch: 'full' }, { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolve } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router, Params } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; @Component({ template: ` <ul class="items"> <li *ngFor="let crisis of crises" [class.selected]="isSelected(crisis)" (click)="onSelect(crisis)"> <span class="badge">{{crisis.id}}</span> {{crisis.name}} </li> </ul> <router-outlet></router-outlet> ` }) export class CrisisListComponent implements OnInit { crises: Crisis[]; public selectedId: number; constructor( private service: CrisisService, private route: ActivatedRoute, private router: Router ) { } isSelected(crisis: Crisis) { return crisis.id === this.selectedId; } ngOnInit() { this.route.params.forEach((params: Params) => { this.selectedId = params['id']; this.service.getCrises() .then(crises => this.crises = crises); }); } onSelect(crisis: Crisis) { this.selectedId = crisis.id; // Navigate with relative link this.router.navigate([crisis.id], { relativeTo: this.route }); } }
import { Component, OnInit, HostBinding, trigger, transition, animate, style, state } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { Crisis } from './crisis.service'; import { DialogService } from '../dialog.service'; @Component({ template: ` <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> `, styles: ['input {width: 20em}'], animations: [ trigger('routeAnimation', [ state('*', style({ opacity: 1, transform: 'translateX(0)' }) ), transition('void => *', [ style({ opacity: 0, transform: 'translateX(-100%)' }), animate('0.2s ease-in') ]), transition('* => void', [ animate('0.5s ease-out', style({ opacity: 0, transform: 'translateY(100%)' })) ]) ]) ] }) export class CrisisDetailComponent implements OnInit { @HostBinding('@routeAnimation') get routeAnimation() { return true; } @HostBinding('style.display') get display() { return 'block'; } @HostBinding('style.position') get position() { return 'absolute'; } crisis: Crisis; editName: string; constructor( private route: ActivatedRoute, private router: Router, public dialogService: DialogService ) { } ngOnInit() { this.route.data.forEach((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); } cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); } canDeactivate(): Promise<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 // promise which resolves to true or false when the user decides return this.dialogService.confirm('Discard changes?'); } gotoCrises() { let 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, ActivatedRouteSnapshot } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; @Injectable() export class CrisisDetailResolve implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot): Promise<Crisis>|boolean { let id = +route.params['id']; return this.cs.getCrisis(id).then(crisis => { if (crisis) { return crisis; } else { // id not found this.router.navigate(['/crisis-center']); return false; } }); } }
export class Crisis { constructor(public id: number, public name: string) { } } const CRISES = [ new Crisis(1, 'Dragon Burning Cities'), new Crisis(2, 'Sky Rains Great White Sharks'), new Crisis(3, 'Giant Asteroid Heading For Earth'), new Crisis(4, 'Procrastinators Meeting Delayed Again'), ]; let crisesPromise = Promise.resolve(CRISES); import { Injectable } from '@angular/core'; @Injectable() export class CrisisService { static nextCrisisId = 100; getCrises() { return crisesPromise; } getCrisis(id: number | string) { return crisesPromise .then(crises => crises.find(crisis => crisis.id === +id)); } }
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Navigate to the login page this.router.navigate(['/login']); return false; } }
import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs/Observable'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable() export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; } }
Query Parameters and Fragments
In our route parameters example, we only dealt with parameters specific to our route, but what if we wanted optional parameters available to all routes? This is where our query parameters come into play and serve a special purpose in our application.
Fragments refer to certain elements on the page identified with an id
attribute.
We'll update our AuthGuard
to provide a session_id
query that will remain after navigating to another route.
We'll also provide an arbitrary anchor
fragment, which we would use to jump to a certain point on our page.
We'll add the NavigationExtras
object to our router.navigate
method that navigates us to our /login
route.
app/auth-guard.service.ts (v3)
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, NavigationExtras } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Create a dummy session id let sessionId = 123456789; // Set our navigation extras object // that contains our global query params and fragment let navigationExtras: NavigationExtras = { queryParams: { 'session_id': sessionId }, fragment: 'anchor' }; // Navigate to the login page with extras this.router.navigate(['/login'], navigationExtras); return false; } }
We can also preserve query parameters and fragments across navigations without having to re-provide them when navigating. In our LoginComponent
, we'll add an object as the second argument in our router.navigate
function and provide the preserveQueryParams
and preserveFragment
to pass along the current query parameters and fragment to the next route.
app/login.component.ts (preserve)
// Set our navigation extras object // that passes on our global query params and fragment let navigationExtras: NavigationExtras = { preserveQueryParams: true, preserveFragment: true }; // Redirect the user this.router.navigate([redirect], navigationExtras);
Since we'll be navigating to our Admin Dashboard route after logging in, we'll update it to handle our query parameters and fragment.
app/admin/admin-dashboard.component.ts (v2)
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; @Component({ template: ` <p>Dashboard</p> <p>Session ID: {{ sessionId | async }}</p> <a id="anchor"></a> <p>Token: {{ token | async }}</p> ` }) 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 .queryParams .map(params => params['session_id'] || 'None'); // Capture the fragment if available this.token = this.route .fragment .map(fragment => fragment || 'None'); } }
Query Parameters and Fragments are also available through the ActivatedRoute
service available to route components. Just like our route parameters, query parameters and fragments are provided as an Observable
. For our updated Crisis Admin component we'll feed the Observable
directly into our template using the AsyncPipe
, which will handle unsubscribing from the Observable
for us when the component is destroyed.
When running in plunker, pop out the preview window by clicking the blue 'X' button in the upper right corner.
Following the steps in this process, we can click on the Admin button, that takes us to the Login page with our provided query params
and fragment
. After we click the login button, we notice that we have been redirected to the Admin Dashboard
page with our query params
and fragment
still intact. We can use these persistent bits of information for things that need to be provided with across pages interaction like authentication tokens or session ids.
The query params
and fragment
can also be preserved using a RouterLink
with the preserveQueryParams and preserveFragment bindings respectively.
Milestone #5: Asynchronous Routing
As we have completed our milestones, our application has naturally gotten larger. As we continue to build out feature areas our overall application size will get larger also. At some point we'll reach a tipping point in where our application takes a significant enough time to load. This is not a viable long term solution.
So how do we combat this problem? We introduce asynchronous routing into our application and take advantage of loading feature areas lazily. This buys us multiple things:
- We can continue building out feature areas without increasing our initial bundle.
- We can load feature areas only when requested by the user.
- We can speed up load time for users that only visit certain areas of our application.
These are all things we want to have in our application, so let's apply this to our current setup. We've already made great strides by organizing our application into four modules: AppModule
, HeroesModule
, AdminModule
and CrisisCenterModule
. Our AdminModule
is the area of our application that would be scoped to a small set of users, so we'll take advantage of asynchronous routing and only load the Admin
feature area when requested.
Lazy-Loading route configuration
We'll start by adding an admin
route to our app.routing.ts
file. We want to load our Admin
module asynchronously, so we'll use the loadChildren
property in our route config where previously we used the children
property to include our child routes.
We'll also change our admin
path in our admin.routing.ts
to an empty path. The Router
supports empty path routes, which we can use for grouping routes together without adding anything additional paths to the URL. Our users will still visit /admin
and our AdminComponent
still serves as our Routing Component which contains our child routes.
const adminRoutes: Routes = [ { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', } ]; const appRoutes: Routes = [ ...loginRoutes, ...adminRoutes ];
import { ModuleWithProviders } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AdminComponent } from './admin.component'; import { AdminDashboardComponent } from './admin-dashboard.component'; import { ManageCrisesComponent } from './manage-crises.component'; import { ManageHeroesComponent } from './manage-heroes.component'; import { AuthGuard } from '../auth-guard.service'; const adminRoutes: Routes = [ { path: '', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', canActivateChild: [AuthGuard], children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; export const adminRouting: ModuleWithProviders = RouterModule.forChild(adminRoutes);
We use the ES2015 spread
feature to flatten the route arrays of our adminRoutes
and loginRoutes
into our appRoutes
array to provide a simple array of routes.
The loadChildren
property is used by the Router
to map to our bundle we want to lazy-load, in this case being the AdminModule
.
If we look closer at the loadChildren
string, we can see that it maps directly to our admin.module.ts
file where we previously built out our Admin
feature area. After the path to the file we use a #
to denote where our file path ends and to tell the Router
the name of our AdminModule
. If we look in our admin.module.ts
file, we can see it matches name of our exported module class.
app/admin/admin.module.ts (export)
export class AdminModule {}
The loadChildren
property is used by the Router
to map to our bundle we want to lazy-load, in this case being the AdminModule
. The router will take our loadChildren string and dynamically load in our AdminModule
, add its routes to our configuration dynamically and then load the requested route. This will only happen when the route is first requested and the module will be immediately be available for subsequent requests.
Angular provides a built-in module loader that supports SystemJS
to load modules asynchronously. If we were using another bundling tool, such as Webpack, we would use the Webpack mechanism for asynchronously loading modules.
We've built our feature area, we've updated our route configuration to take advantage of lazy-loading, now we have to do the final step to break our AdminModule
into a completely separate module. In our app.module.ts
, we'll remove our AdminModule
from the imports
array since we'll be loading it on-demand an we'll remove the imported AdminModule
.
app/app.module.ts (async admin module)
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { routing, appRoutingProviders } from './app.routing'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { LoginComponent } from './login.component'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ BrowserModule, FormsModule, routing, HeroesModule, CrisisCenterModule ], declarations: [ AppComponent, LoginComponent ], providers: [ appRoutingProviders, DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }
CanLoad Guard: guarding against loading of feature modules
We're already protecting our AdminModule
with a CanActivate
guard that prevents the user from accessing the admin feature area unless authorized. We're currently loading the admin routing asynchronously when requested, checking the user access and redirecting to the login page if not authorized. Ideally, we only want to load the AdminModule
if the user is logged in and prevent the AdminModule
and its routing from being loaded until then.
The CanLoad guard covers this scenario.
We can use the CanLoad
guard to only load the AdminModule
once the user is logged in and attempts to access the admin feature area. We'll update our existing AuthGuard
to support the CanLoad
guard. We'll import the CanLoad
interface and the Route
the guard provides when called that contains the requested path.
We'll add the interface to our service, and then we'll implement the interface. Since our AuthGuard
already checks the user's logged in state, we can pass that access check to our canLoad
method. The Route
in the canLoad
method provides a path which comes from our route configuration.
app/auth-guard.service.ts (can load guard)
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, NavigationExtras, CanLoad, Route } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild, CanLoad { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } canLoad(route: Route): boolean { let url = `/${route.path}`; return this.checkLogin(url); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Create a dummy session id let sessionId = 123456789; // Set our navigation extras object // that contains our global query params and fragment let navigationExtras: NavigationExtras = { queryParams: { 'session_id': sessionId }, fragment: 'anchor' }; // Navigate to the login page with extras this.router.navigate(['/login'], navigationExtras); return false; } }
Next, we'll import the AuthGuard
into our app.routing.ts
and add the AuthGuard
to the canLoad
array for our admin
route. Now our admin
feature area is only loaded when the proper access has been granted.
app/app.routing.ts (can load guard)
import { AuthGuard } from './auth-guard.service'; const adminRoutes: Routes = [ { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [AuthGuard] } ];
Wrap Up
We've covered a lot of ground in this chapter and the application is too big to reprint here. Please visit the live example and where you can download the final source code.
Appendices
The balance of this chapter is a set of appendices that elaborate some of the points we covered quickly above.
The appendix material isn't essential. Continued reading is for the curious.
Appendix: Link Parameters Array
We've mentioned the Link Parameters Array several times. We've used it several times.
A link parameters array holds the ingredients for router navigation:
- the path of the route to the destination component
- required and optional route parameters that go into the route URL
We can bind the RouterLink
directive to such an array like this:
<a [routerLink]="['/heroes']">Heroes</a>
We've written a two element array when specifying a route parameter like this
this.router.navigate(['/hero', hero.id]);
We can provide optional route parameters in an object like this:
<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>
These three examples cover our needs for an app with one level routing. The moment we add a child router, such as the Crisis Center, we create new link array possibilities.
Recall that we specified a default child route for Crisis Center so this simple RouterLink
is fine.
<a [routerLink]="['/crisis-center']">Crisis Center</a>
Let's parse it out.
- The first item in the array identifies the parent route ('/crisis-center').
- There are no parameters for this parent route so we're done with it.
- There is no default for the child route so we need to pick one.
- We decide to go to the
CrisisListComponent
whose route path is '/' but we don't need to explicitly add it - Voila!
['/crisis-center']
.
Let's take it a step further. This time we'll build a link parameters array that navigates from the root of the application down to the "Dragon Crisis".
- The first item in the array identifies the parent route ('/crisis-center').
- There are no parameters for this parent route so we're done with it.
- The second item identifies the child route for details about a particular crisis ('/:id').
- The details child route requires an
id
route parameter - We add
id
of the Dragon Crisis as the second item in the array (1
)
It looks like this!
<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>
If we wanted to, we could redefine our AppComponent
template with Crisis Center routes exclusively:
template: ` <h1 class="title">Angular Router</h1> <nav> <a [routerLink]="['/crisis-center']">Crisis Center</a> <a [routerLink]="['/crisis-center/1', { foo: 'foo' }]">Dragon Crisis</a> <a [routerLink]="['/crisis-center/2']">Shark Crisis</a> </nav> <router-outlet></router-outlet> `
In sum, we can write applications with one, two or more levels of routing. The link parameters array affords the flexibility to represent any routing depth and any legal sequence of route paths, (required) router parameters and (optional) route parameter objects.
Appendix: Why use an ngOnInit method
We implemented an ngOnInit
method in many of our Component classes. We did so, for example, in the HeroDetailComponent. We might have put the ngOnInit
logic inside the constructor instead. We didn't for a reason. The reason is testability.
A constructor that has major side-effects can be difficult to test because it starts doing things as soon as we create a test instance. In this case, it might have made a request to a remote server, something it shouldn't do under test. It may even be impossible to reach the server in the test environment.
The better practice is to limit what the constructor can do. Mostly it should stash parameters in local variables and perform simple instance configuration.
Yet we want an instance of this class to get the hero data from the HeroService
soon after it is created. How do we ensure that happens if not in the constructor?
Angular detects when a component has certain lifecycle methods like ngOnInit and ngOnDestroy and calls them at the appropriate moment.
Angular will call ngOnInit
when we navigate to the HeroDetailComponent
, we'll get the id
from the ActivatedRoute
params and ask the server for the hero with that id
.
We too can call that ngOnInit
method in our tests if we wish ... after taking control of the injected HeroService
and (perhaps) mocking it.
Appendix: LocationStrategy and browser URL styles
When the router navigates to a new component view, it updates the browser's location and history with a URL for that view. This is a strictly local URL. The browser shouldn't send this URL to the server and should not reload the page.
Modern HTML 5 browsers support history.pushState, a technique that changes a browser's location and history without triggering a server page request. The router can compose a "natural" URL that is indistinguishable from one that would otherwise require a page load.
Here's the Crisis Center URL in this "HTML 5 pushState" style:
localhost:3002/crisis-center/
Older browsers send page requests to the server when the location URL changes ... unless the change occurs after a "#" (called the "hash"). Routers can take advantage of this exception by composing in-application route URLs with hashes. Here's a "hash URL" that routes to the Crisis Center
localhost:3002/src/#/crisis-center/
The Router supports both styles with two LocationStrategy
providers:
-
PathLocationStrategy
- the default "HTML 5 pushState" style. -
HashLocationStrategy
- the "hash URL" style.
The RouterModule.forRoot
function sets the LocationStrategy
to the PathLocationStrategy
, making it the default strategy. We can switch to the HashLocationStrategy
with an override during the bootstrapping process if we prefer it.
Learn about "providers" and the bootstrap process in the Dependency Injection chapter
Which Strategy is Best?
We must choose a strategy and we need to make the right call early in the project. It won't be easy to change later once the application is in production and there are lots of application URL references in the wild.
Almost all Angular 2 projects should use the default HTML 5 style. It produces URLs that are easier for users to understand. And it preserves the option to do server-side rendering later.
Rendering critical pages on the server is a technique that can greatly improve perceived responsiveness when the app first loads. An app that would otherwise take ten or more seconds to start could be rendered on the server and delivered to the user's device in less than a second.
This option is only available if application URLs look like normal web URLs without hashes (#) in the middle.
Stick with the default unless you have a compelling reason to resort to hash routes.
HTML 5 URLs and the <base href>
While the router uses the "HTML 5 pushState" style by default, we must configure that strategy with a base href
The preferred way to configure the strategy is to add a <base href> element tag in the <head>
of the index.html
.
<base href="/">
Without that tag, the browser may not be able to load resources (images, css, scripts) when "deep linking" into the app. Bad things could happen when someone pastes an application link into the browser's address bar or clicks such a link in an email link.
Some developers may not be able to add the <base>
element, perhaps because they don't have access to <head>
or the index.html
.
Those developers may still use HTML 5 URLs by taking two remedial steps:
- Provide the router with an appropriate
APP_BASE_HREF
value. - Use absolute URLs for all web resources: css, images, scripts, and template html files.
Learn about the APP_BASE_HREF in the API Guide.
HashLocationStrategy
We can go old-school with the HashLocationStrategy
by providing the useHash: true
in an object as the second argument of the RouterModule.forRoot
in our AppModule
.
app/app.module.ts (hash URL strategy)
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { Routes, RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; const routes: Routes = [ ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot(routes, { useHash: true }) // .../#/crisis-center/ ], declarations: [ AppComponent ], providers: [ ], bootstrap: [ AppComponent ] }) export class AppModule { }
Please login to continue.