The Angular Router
enables navigation from one view to the next as users perform application tasks.
This guide covers the router's primary features, illustrating them through the evolution of a small application that you can run live in the browser.
To see the URL changes in the browser address bar of the live example, open it again in the Plunker editor by clicking the icon in the upper right, then pop out the preview window by clicking the blue 'X' button in the upper right corner. |
|
The browser is a familiar model of application navigation:
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. It can pass optional parameters along to the supporting view component that help it decide what specific content to present. You can bind the router to links on a page and it will navigate to the appropriate application view when the user clicks a link. You 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.
CanActivate
: requiring authenticationCanActivateChild
: guarding child routesCanDeactivate
: handling unsaved changesResolve
: pre-fetching component dataThis guide proceeds in phases, marked by milestones, starting from a simple two-pager and building toward a modular, multi-view design with child routes.
An introduction to a few core router concepts will help orient you to the details that follow.
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 the sample application, set the href
value exactly as shown here.
<base href="/">
The Angular Router is an optional service that presents a particular component view for a given URL. It is not part of the Angular core. It is in its own library package, @angular/router
. Import what you need from it as you would from any other Angular package.
import { RouterModule, Routes } from '@angular/router';
You'll learn about more options in the details below.
A routed Angular application has one singleton instance of the Router
service. When the browser's URL changes, that router looks for a corresponding Route
from which it can determine the component to display.
A router has no routes until you configure it. The following example creates four route definitions, configures the router via the RouterModule.forRoot
method, and adds the result to the AppModule
's imports
array.
const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'hero/:id', component: HeroDetailComponent }, { path: 'heroes', component: HeroListComponent, data: { title: 'Heroes List' } }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) // other imports here ], ... }) export class AppModule { }
The appRoutes
array of routes describes how to navigate. Pass it to the RouterModule.forRoot
method in the module imports
to configure the router.
Each Route
maps a URL path
to a component. There are no leading slashes in the path. The router parses and builds the final URL for you, allowing you to use both 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. You'll learn more about route parameters later in this guide.
The data
property in the third route is a place to store arbitrary data associated with this specific route. The data property is accessible within each activated route. Use it to store items such as page titles, breadcrumb text, and other read-only, static data. You'll use the resolve guard to retrieve dynamic data later in the guide.
The empty path
in the fourth route represents the default path for the application, the place to go when the path in the URL is empty, as it typically is at the start. This default route redirects to the route for the /heroes
URL and, therefore, will display the HeroesListComponent
.
The **
path in the last route is a wildcard. The router will select this route if the requested URL doesn't match any paths for routes defined earlier in the configuration. This is useful for displaying a "404 - Not Found" 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 the configuration above, routes with a static path are listed first, followed by an empty path route, that matches the default route. The wildcard route comes last because it matches every URL and should be selected only if no other routes are matched first.
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
after a RouterOutlet
that you've placed in the host view's HTML.
<router-outlet></router-outlet> <!-- Routed views go here -->
Now you have routes configured and a place to render them, but how do you navigate? The URL could arrive directly from the browser address bar. But most of the time you navigate as a result of some user action such as the click of an anchor tag.
Consider the following 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> `
The RouterLink
directives on the anchor tags give the router control over those elements. The navigation paths are fixed, so you can assign a string to the routerLink
(a "one-time" binding).
Had the navigation path been more dynamic, you could have bound to a template expression that returned an array of route link parameters (the link parameters array). The router resolves that array into a complete URL.
The RouterLinkActive
directive on each anchor tag helps visually distinguish the anchor for the currently selected "active" route. The router adds the active
CSS class to the element when the associated RouterLink becomes active. You can add this directive to the anchor or to its parent element.
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. You can access the current RouterState
from anywhere in the application using the Router
service and the routerState
property.
Each ActivatedRoute
in the RouterState
provides methods to traverse up and down the route tree to get information from parent, child and sibling routes.
The application has a configured router. The shell 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 displays a view. |
RouterLink |
The directive for binding a clickable HTML element to a route. Clicking an element 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 together with convenience methods for traversing the route tree. |
Link parameters array | An array that the router interprets as a routing instruction. You can bind that array to a RouterLink 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. |
This guide describes development of a multi-page routed sample application. Along the way, it highlights design decisions and describes key features of the router such as:
id
while routing to the "Hero Detail").CanActivate
guard (checking route access).CanActivateChild
guard (checking child route access).CanDeactivate
guard (ask permission to discard unsaved changes).Resolve
guard (pre-fetching route data).CanLoad
guard (check before loading feature module assets).The guide proceeds as a sequence of milestones as if you were building the app step-by-step. But, it is not a tutorial and it glosses over details of Angular application construction that are more thoroughly covered elsewhere in the documentation.
The full source for the final version of the app can be seen and downloaded from the live example.
Imagine an application that helps the Hero Employment Agency run its business. Heroes need work and the agency finds crises for them to solve.
The application has three main feature areas:
Try it by clicking on this live example link.
Once the app warms up, you'll see a row of navigation buttons and the Heroes view with its list of heroes.
Select one hero and the app takes you to a hero editing screen.
Alter the name. Click the "Back" button and the app returns to the heroes list which displays the changed hero name. Notice that the name change took effect immediately.
Had you clicked the browser's back button instead of the "Back" button, the app would have returned you to the heroes list as well. Angular app navigation updates the browser history as normal web navigation does.
Now click the Crisis Center link for a list of ongoing crises.
Select a crisis and the application takes you to a crisis editing screen. The Crisis Detail appears in a child view on the same page, beneath the list.
Alter the name of a crisis. Notice that the corresponding name in the crisis list does not change.
Unlike Hero Detail, which updates as you type, Crisis Detail changes are temporary until you either save or discard them by pressing the "Save" or "Cancel" buttons. Both buttons navigate back to the Crisis Center and its list of crises.
Do not click either button yet. Click the browser back button or the "Heroes" link instead.
Up pops a dialog box.
You can say "OK" and lose your changes or click "Cancel" and continue editing.
Behind this behavior is the router's CanDeactivate
guard. The guard gives you a chance to clean-up or ask the user's permission before navigating away from the current view.
The Admin
and Login
buttons illustrate other router capabilities to be covered later in the guide. This short introduction will do for now.
Proceed to the first application milestone.
Begin with a simple version of the app that navigates between two empty views.
The router uses the browser's history.pushState for navigation. Thanks to pushState
, you can make in-app URL paths look the way you want them to look, e.g. localhost:3000/crisis-center
. The in-app URLs can be indistinguishable from server URLs.
Modern HTML5 browsers were the first to support pushState
which is why many people refer to these URLs as "HTML5 style" URLs.
HTML5 style navigation is the router default. In the LocationStrategy and browser URL styles Appendix, learn why HTML5 style is preferred, how to adjust its behavior, and how to switch to the older hash (#) style, if necessary.
You must add a <base href> element to the app's index.html
for pushState
routing to work. The browser uses the <base href>
value to prefix relative URLs when referencing 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 this application, set the href
value in index.html
exactly as shown here.
<base href="/">
A live coding environment like Plunker sets the application base address dynamically so you can't specify a fixed address. That's why the example code replaces the <base href...>
with a script that writes the <base>
tag on the fly.
<script>document.write('<base href="' + document.location + '" />');</script>
You only need this trick for the live example, not production code.
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 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.
You teach the router how to navigate by configuring it with routes.
A router must be configured with a list of route definitions.
The first configuration defines an array of two routes with simple paths leading to the CrisisListComponent
and HeroListComponent
.
Each definition translates to a Route object which has two things: a path
, the URL path segment for this route; and a component
, the component associated with this route.
The router draws upon its registry of definitions when the browser URL changes or when application code tells the router to navigate along a route path.
In simpler terms, you might say this of the first route:
When the browser's location URL changes to match the path segment /crisis-center
, then the router activates an instance of the CrisisListComponent
and displays its view.
When the application requests navigation to the path /crisis-center
, the router activates an instance of CrisisListComponent
, displays its view, and updates the browser's address location and history with the URL for that path.
Here is the first configuration. Pass the array of routes, appRoutes
, to the RouterModule.forRoot
method. It returns a module, containing the configured Router
service provider, plus other providers that the routing library requires. Once the application is bootstrapped, the Router
performs the initial navigation based on the current browser URL.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot(appRoutes) ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, ], bootstrap: [ AppComponent ] }) export class AppModule { }
Adding the configured RouterModule
to the AppModule
is sufficient for simple route configurations. As the application grows, you'll want to refactor the routing configuration into a separate file and create a Routing Module, a special type of Service Module
dedicated to the purpose of routing in feature modules.
Providing the RouterModule
in the AppModule
makes the Router available everywhere in the application.
The root AppComponent
is the application shell. It has a title, a navigation bar with two links, and a router outlet where the router swaps views on and off the page. Here's what you get:
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> `
The RouterOutlet
is a directive from the router library that marks the spot in the template where the router should display the views for that outlet.
The router adds the <router-outlet>
element to the DOM and subsequently inserts the navigated view element immediately after the <router-outlet>
.
Above the outlet, within the anchor tags, you see attribute bindings to the RouterLink
directive that look like routerLink="..."
.
The links in this example each have a string path, the path of a route that you configured earlier. There are no route parameters yet.
You can also add more contextual information to the RouterLink
by providing query string parameters or a URL fragment for jumping to different areas on the 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 you can also use the link parameters array in the appendix below.
On each anchor tag, you also see property bindings to the RouterLinkActive
directive that look like routerLinkActive="..."
.
The template expression to the right of the equals (=) contains a space-delimited string of CSS classes that the Router will add when this link is active (and remove when the link is inactive). You can also set the RouterLinkActive
directive to a string of classes such as [routerLinkActive]="active fluffy"
or bind it to a component property that returns such a string.
The RouterLinkActive
directive toggles css classes for active RouterLink
s based on the current RouterState
. This cascades down through each level of the route tree, so parent and child router links can be active at the same time. To override this behavior, you 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.
RouterLink
, RouterLinkActive
and RouterOutlet
are directives provided by the Angular RouterModule
package. They are readily available for you to use in the template.
The current state of app.component.ts
looks like this:
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 { }
You've created two routes in the app so far, one to /crisis-center
and the other to /heroes
. Any other URL causes the router to throw an error and crash the app.
Add a wildcard route to intercept invalid URLs and handle them gracefully. A wildcard route has a path consisting of two asterisks. It matches every URL. The router will select this route if it can't match a route earlier in the configuration. A wildcard route can navigate to a custom "404 Not Found" component or redirect to an existing route.
The router selects the route with a first match wins strategy. Wildcard routes are the least specific routes in the route configuration. Be sure it is the last route in the configuration.
To test this feature, add a button with a RouterLink
to the HeroListComponent
template and set the link to "/sidekicks"
.
import { Component } from '@angular/core'; @Component({ template: ` <h2>HEROES</h2> <p>Get your heroes here</p> <button routerLink="/sidekicks">Go to sidekicks</button> ` }) export class HeroListComponent { }
The application will fail if the user clicks that button because you haven't defined a "/sidekicks"
route yet.
Instead of adding the "/sidekicks"
route, define a wildcard
route instead and have it navigate to a simple PageNotFoundComponent
.
{ path: '**', component: PageNotFoundComponent }
Create the PageNotFoundComponent
to display when users visit invalid URLs.
import { Component } from '@angular/core'; @Component({ template: '<h2>Page not found</h2>' }) export class PageNotFoundComponent {}
As with the other components, add the PageNotFoundComponent
to the AppModule
declarations.
Now when the user visits /sidekicks
, or any other invalid URL, the browser displays "Page not found". The browser address bar continues to point to the invalid URL.
When the application launches, the initial URL in the browser bar is something like:
localhost:3000
That doesn't match any of the configured routes which means that the application won't display any component when it's launched. The user must click one of the links to trigger a navigation and display a component.
It would be nicer if the application had a default route that displayed the list of heroes immediately, just as it will when the user clicks the "Heroes" link or pastes localhost:3000/heroes
into the address bar.
The preferred solution is to add a redirect
route that translates the initial relative URL (''
) to the desired default path (/heroes
). The browser address bar shows .../heroes
as if you'd navigated there directly.
Add the default route somewhere above the wildcard route. It's just above the wildcard route in the following excerpt showing the complete appRoutes
for this milestone.
const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
A redirect route requires a pathMatch
property to tell the router how to match a URL to the path of a route. The router throws an error if you don't. In this app, the router should select the route to the HeroListComponent
only when the entire URL matches ''
, so set the pathMatch
value to 'full'
.
Technically, pathMatch = 'full'
results in a route hit when the remaining, unmatched segments of the URL match ''
. In this example, the redirect is in a top level route so the remaining URL and the entire URL are the same thing.
The other possible pathMatch
value is 'prefix'
which tells the router to match the redirect route when the remaining URL begins with the redirect route's prefix path.
Don't do that here. If the pathMatch
value were 'prefix'
, every URL would match ''
.
Try setting it to 'prefix'
then click the Go to sidekicks
button. Remember that's a bad URL and you should see the "Page not found" page. Instead, you're still on the "Heroes" page. Enter a bad URL in the browser address bar. You're instantly re-routed to /heroes
. Every URL, good or bad, that falls through to this route definition will be a match.
The default route should redirect to the HeroListComponent
only when the entire url is ''
. Remember to restore the redirect to pathMatch = 'full'
.
Learn more in Victor Savkin's post on redirects.
You've got a very basic navigating app, one that can switch between two views when the user clicks a link.
You've learned how to do the following:
routerLink
and routerLinkActive
directives.router-outlet
to the shell template where views will be displayed.RouterModule.forRoot
.wildcard
route.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.
The 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 { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; import { PageNotFoundComponent } from './not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot(appRoutes) ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
import { Component } from '@angular/core'; @Component({ template: ` <h2>HEROES</h2> <p>Get your heroes here</p> <button routerLink="/sidekicks">Go to sidekicks</button> ` }) export class HeroListComponent { }
import { Component } from '@angular/core'; @Component({ template: ` <h2>CRISIS CENTER</h2> <p>Get your crisis here</p>` }) export class CrisisListComponent { }
import { Component } from '@angular/core'; @Component({ template: '<h2>Page not found</h2>' }) export class PageNotFoundComponent {}
<html> <head> <!-- Set the base href --> <base href="/"> <title>Angular Router</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="styles.css"> <!-- Polyfills --> <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/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('main.js') .catch(function(err){ console.error(err); }); </script> </head> <body> <my-app>loading...</my-app> </body> </html>
In the initial route configuration, you provided a simple setup with two routes used to configure the application for routing. This is perfectly fine for simple routing. As the application grows and you make use of more Router
features, such as guards, resolvers, and child routing, you'll naturally want to refactor the routing configuration into its own file. We recommend moving the routing information into a special-purpose module called a Routing Module.
The Routing Module has several characteristics:
Create a file named app-routing.module.ts
in the /app
folder to contain the routing module.
Import the CrisisListComponent
and the HeroListComponent
components just like you did in the app.module.ts
. Then move the Router
imports and routing configuration, including RouterModule.forRoot
, into this routing module.
Following convention, add a class name AppRoutingModule
and export it so you can import it later in AppModule
.
Finally, re-export the Angular RouterModule
by adding it to the module exports
array. By re-exporting the RouterModule
here and importing AppRoutingModule
in AppModule
, the components declared in AppModule
will have access to router directives such as RouterLink
and RouterOutlet
.
After these steps, the file should look like this.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; import { PageNotFoundComponent } from './not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
Next, update the app.module.ts
file, first importing the newly created AppRoutingModule
from app-routing.module.ts
, then replacing RouterModule.forRoot
in the imports
array with the AppRoutingModule
.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { CrisisListComponent } from './crisis-list.component'; import { HeroListComponent } from './hero-list.component'; import { PageNotFoundComponent } from './not-found.component'; @NgModule({ imports: [ BrowserModule, FormsModule, AppRoutingModule ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
Later in this guide you will create multiple routing modules and discover that you must import those routing modules in the correct order.
The application continues to work just the same, and you can use AppRoutingModule
as the central place to maintain future routing configuration.
The Routing Module replaces the routing configuration in the root or feature module. Either configure routes in the Routing Module or within the module itself but not in both.
The Routing Module is a design choice whose value is most obvious when the configuration is complex and includes specialized guard and resolver services. It can seem like overkill when the actual configuration is dead simple.
Some developers skip the Routing Module (for example, AppRoutingModule
) when the configuration is simple and merge the routing configuration directly into the companion module (for example, AppModule
).
Choose one pattern or the other and follow that pattern consistently.
Most developers should always implement a Routing Module for the sake of consistency. It keeps the code clean when configuration becomes complex. It makes testing the feature module easier. Its existence calls attention to the fact that a module is routed. It is where developers expect to find and expand routing configuration.
You've seen how to navigate using the RouterLink
directive. Now you'll learn the following:
This example recreates the heroes feature in the "Services" episode of the Tour of Heroes tutorial, and you'll be copying much of the code from the .
Here's how the user will experience this version of the app:
A typical application has multiple feature areas, each dedicated to a particular business purpose.
While you could continue to add files to the src/app/
folder, that is unrealistic and ultimately not maintainable. Most developers prefer to put each feature area in its own folder.
You are about to break up the app into different feature modules, each with its own concerns. Then you'll import into the main module and navigate among them.
Follow these steps:
src/app/heroes
folder; you'll be adding files implementing hero management there.hero-list.component.ts
that's in the app
folder.hero-list.component.ts
under src/app/heroes
.app.component.ts
from the "Services" tutorial.selector
(routed components don't need them).<h1>
.<h2>
to <h2>HEROES</h2>
.<hero-detail>
at the bottom of the template.AppComponent
class to HeroListComponent
.hero-detail.component.ts
and the hero.service.ts
files into the heroes
subfolder.heroes.module.ts
in the heroes folder that looks like this:import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ CommonModule, FormsModule, ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] }) export class HeroesModule {}
When you're done, you'll have these hero management files:
The heroes feature has two interacting components, the hero list and the hero detail. The list view is self-sufficient; you navigate to it, it gets a list of heroes and displays them.
The detail view is different. It displays a particular hero. It can't know which hero to show on its own. That information must come from outside.
When the user selects a hero from the list, the app should navigate to the detail view and show that hero. You tell the detail view which hero to display by including the selected hero's id in the route URL.
Create a new heroes-routing.module.ts
in the heroes
folder using the same techniques you learned while creating the AppRoutingModule
.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent }, { path: 'hero/:id', component: HeroDetailComponent } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroRoutingModule { }
Put the routing module file in the same folder as its companion module file. Here both heroes-routing.module.ts
and heroes.module.ts
are in the same src/app/heroes
folder.
Consider giving each feature module its own route configuration file. It may seem like overkill early when the feature routes are simple. But routes have a tendency to grow more complex and consistency in patterns pays off over time.
Import the hero components from their new locations in the src/app/heroes/
folder, define the two hero routes, and export the HeroRoutingModule
class.
Now that you have routes for the Heroes
module, register them with the Router
via the RouterModule
almost as you did in the AppRoutingModule
.
There is a small but critical difference. In the AppRoutingModule
, you used the static RouterModule.forRoot
method to register the routes and application level service providers. In a feature module you use the static forChild
method.
Only call RouterModule.forRoot
in the root AppRoutingModule
(or the AppModule
if that's where you register top level application routes). In any other module, you must call the RouterModule.forChild
method to register additional routes.
Add the HeroRoutingModule
to the HeroModule
just as you added AppRoutingModule
to the AppModule
.
Open heroes.module.ts
. Import the HeroRoutingModule
token from heroes-routing.module.ts
and add it to the imports
array of the HeroesModule
. The finished HeroesModule
looks like this:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; import { HeroRoutingModule } from './heroes-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroRoutingModule ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] }) export class HeroesModule {}
The hero routes are currently defined in two places: in the HeroesRoutingModule
, by way of the HeroesModule
, and in the AppRoutingModule
.
Routes provided by feature modules are combined together into their imported module's routes by the router. This allows you to continue defining the feature module routes without modifying the main route configuration.
But you don't want to define the same routes twice. Remove the HeroListComponent
import and the /heroes
route from the app-routing.module.ts
.
Leave the default and the wildcard routes! These are concerns at the top level of the application itself.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisListComponent } from './crisis-list.component'; // import { HeroListComponent } from './hero-list.component'; // <-- delete this line import { PageNotFoundComponent } from './not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, // { path: 'heroes', component: HeroListComponent }, // <-- delete this line { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
The heroes feature module is ready, but the application doesn't know about the HeroesModule
yet. Open app.module.ts
and revise it as follows.
Import the HeroesModule
and add it to the imports
array in the @NgModule
metadata of the AppModule
.
Remove the HeroListComponent
from the AppModule
's declarations
because it's now provided by the HeroesModule
. This is important. There can be only one owner for a declared component. In this case, the Heroes
module is the owner of the Heroes
components and is making them available to components in the AppModule
via the HeroesModule
.
As a result, the AppModule
no longer has specific knowledge of the hero feature, its components, or its route details. You can evolve the hero feature with more components and different routes. That's a key benefit of creating a separate module for each feature area.
After these steps, the AppModule
should look like this:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisListComponent } from './crisis-list.component'; import { PageNotFoundComponent } from './not-found.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, AppRoutingModule ], declarations: [ AppComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
Look at the module imports
array. Notice that the AppRoutingModule
is last. Most importantly, it comes after the HeroesModule
.
imports: [ BrowserModule, FormsModule, HeroesModule, AppRoutingModule ],
The order of route configuration matters. The router accepts the first route that matches a navigation request path.
When all routes were in one AppRoutingModule
, you put the default and wildcard routes last, after the /heroes
route, so that the router had a chance to match a URL to the /heroes
route before hitting the wildcard route and navigating to "Page not found".
The routes are no longer in one file. They are distributed across two modules, AppRoutingModule
and HeroesRoutingModule
.
Each routing module augments the route configuration in the order of import. If you list AppRoutingModule
first, the wildcard route will be registered before the hero routes. The wildcard route — which matches every URL — will intercept the attempt to navigate to a hero route.
Reverse the routing modules and see for yourself that a click of the heroes link results in "Page not found". Learn about inspecting the runtime router configuration below.
Return to the HeroesRoutingModule
and look at the route definitions again. The route to HeroDetailComponent
has a twist.
{ path: 'hero/:id', component: HeroDetailComponent }
Notice the :id
token in the path. That creates a slot in the path for a Route Parameter. In this case, the router will insert the id
of a hero into that slot.
If you tell the router to navigate to the detail component and display "Magneta", you expect a hero id to appear in the browser URL like this:
localhost:3000/hero/15
If a user enters that URL into the browser address bar, the router should recognize the pattern and go to the same "Magneta" detail view.
Embedding the route parameter token, :id
, in the route definition path is a good choice for this scenario because the id
is required by the HeroDetailComponent
and because the value 15
in the path clearly distinguishes the route to "Magneta" from a route for some other hero.
Users will not navigate to the detail component by clicking a link so you won't add a new RouterLink
anchor tag to the shell.
Instead, when the user clicks a hero in the list, you'll ask the router to navigate to the hero detail view for the selected hero.
Start in the HeroListComponent
. Revise its constructor so that it acquires the Router
and the HeroService
by dependency injection:
constructor( private router: Router, private service: HeroService ) {}
Make the following few changes to the component's template:
template: ` <h2>HEROES</h2> <ul class="items"> <li *ngFor="let hero of heroes | async" (click)="onSelect(hero)"> <span class="badge">{{ hero.id }}</span> {{ hero.name }} </li> </ul> <button routerLink="/sidekicks">Go to sidekicks</button> `
The template defines an *ngFor
repeater such as you've seen before. There's a (click)
event binding to the component's onSelect
method which you implement as follows:
onSelect(hero: Hero) { this.router.navigate(['/hero', hero.id]); }
The component's onSelect
calls the router's navigate
method with a link parameters array. You can use this same syntax in a RouterLink
if you decide later to navigate in HTML template rather than in component code.
After navigating to the HeroDetailComponent
, you expect to see the details of the selected hero. You need two pieces of information: the routing path to the component and the hero's id
.
Accordingly, the link parameters array has two items: the routing path and a route parameter that specifies the id
of the selected hero.
['/hero', hero.id] // { 15 }
The router composes the destination URL from the array like this: localhost:3000/hero/15
.
How does the target HeroDetailComponent
learn about that id
? Don't analyze the URL. Let the router do it.
The router extracts the route parameter (id:15
) from the URL and supplies it to the HeroDetailComponent
via the ActivatedRoute
service.
The route path and parameters are available through an injected router service called the ActivatedRoute. It has a great deal of useful information including:
url
: An Observable
of the route path(s), represented 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.
Import the Router
, ActivatedRoute
, and Params
tokens from the router package.
import { Router, ActivatedRoute, Params } from '@angular/router';
Import the switchMap
operator because you need it later to process the Observable
route parameters.
import 'rxjs/add/operator/switchMap';
As usual, you write a constructor that asks Angular to inject services that the component requires and reference them as private variables.
constructor( private route: ActivatedRoute, private router: Router, private service: HeroService ) {}
Later, in the ngOnInit
method, you use the ActivatedRoute
service to retrieve the parameters for the route, pull the hero id
from the parameters and retrieve the hero to display.
Put this data access logic in the ngOnInit
method rather than inside the constructor to improve the component's testability. Angular calls the ngOnInit
method shortly after creating an instance of the HeroDetailComponent
so the hero will be retrieved in time to use it.
Learn more about the ngOnInit
method and other component lifecycle hooks in the Lifecycle Hooks guide.
ngOnInit() { this.route.params // (+) converts string 'id' to a number .switchMap((params: Params) => this.service.getHero(+params['id'])) .subscribe((hero: Hero) => this.hero = hero); }
Since the parameters are provided as an Observable
, you use the switchMap
operator to provide them for the id
parameter by name and tell the HeroService
to fetch the hero with that id
.
The switchMap
operator allows you to perform an action with the current value of the Observable
, and map it to a new Observable
. As with many rxjs
operators, switchMap
handles an Observable
as well as a Promise
to retrieve the value they emit.
The switchMap
operator will also cancel any in-flight requests if the user re-navigates to the route while still retrieving a hero.
Use the subscribe
method to detect id
changes and to (re)set the retrieved Hero
.
In this example, you 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 re-uses a component instance when it re-navigates to the same component type without visiting a different component first. The route parameters could change each time.
Suppose a parent component navigation bar had "forward" and "back" buttons that scrolled through the list of heroes. Each click navigated imperatively to the HeroDetailComponent
with the next or previous id
.
You don't want the router to remove the current HeroDetailComponent
instance from the DOM only to re-create it for the next id
. That could be visibly jarring. Better to simply re-use the same component instance and update the parameter.
Unfortunately, ngOnInit
is only called once per component instantiation. You need a way to detect when the route parameters change from within the same instance. The observable params
property handles that beautifully.
When subscribing to an observable in a component, you almost always arrange to unsubscribe when the component is destroyed.
There are a few exceptional observables where this is not necessary. The ActivatedRoute
observables are among the exceptions.
The ActivatedRoute
and its observables are insulated from the Router
itself. The Router
destroys a routed component when it is no longer needed and the injected ActivatedRoute
dies with it.
Feel free to unsubscribe anyway. It is harmless and never a bad practice.
This application won't re-use the HeroDetailComponent
. The user always returns to the hero list to select another hero to view. There's no way to navigate from one hero detail to another hero detail without visiting the list component in between. Therefore, the router creates a new HeroDetailComponent
instance every time.
When you know for certain that a HeroDetailComponent
instance will never, never, ever be re-used, you can simplify the code with the snapshot.
The route.snapshot
provides the initial value of the route parameters. You can access the parameters directly without subscribing or adding observable operators. It's much simpler to write and read:
ngOnInit() { // (+) converts string 'id' to a number let id = +this.route.snapshot.params['id']; this.service.getHero(id) .then((hero: Hero) => this.hero = hero); }
Remember: you only get the initial value of the parameters with this technique. Stick with the observable params
approach if there's even a chance that the router could re-use the component. This sample stays with the observable params
strategy just in case.
The HeroDetailComponent
has a "Back" button wired to its gotoHeroes
method that navigates imperatively back to the HeroListComponent
.
The router navigate
method takes the same one-item link parameters array that you can bind to a [routerLink]
directive. It holds the path to the HeroListComponent
:
gotoHeroes() { this.router.navigate(['/heroes']); }
Use route parameters to specify a required parameter value within the route URL as you do when navigating to the HeroDetailComponent
in order to view the hero with id 15:
localhost:3000/hero/15
You can also add optional information to a route request. For example, when returning to the heroes list from the hero detail view, it would be nice if the viewed hero was preselected in the list.
You'll implement this feature in a moment by including the viewed hero's id
in the URL as an optional parameter when returning from the HeroDetailComponent
.
Optional information takes other forms. Search criteria are often loosely structured, e.g., name='wind*'
. Multiple values are common—after='12/31/2015' & before='1/1/2017'
—in no particular order—before='1/1/2017' & after='12/31/2015'
— in a variety of formats—during='currentYear'
.
These kinds of parameters don't fit easily in a URL path. Even if you could define a suitable URL token scheme, doing so greatly complicates the pattern matching required to translate an incoming URL to a named route.
Optional parameters are the ideal vehicle for conveying arbitrarily complex information during navigation. Optional parameters aren't involved in pattern matching and afford flexibility of expression.
The router supports navigation with optional parameters as well as required route parameters. Define optional parameters in a separate object after you define the required route parameters.
In general, prefer a required route parameter when the value is mandatory (for example, if necessary to distinguish one route path from another); prefer an optional parameter when the value is optional, complex, and/or multivariate.
When navigating to the HeroDetailComponent
you specified the required id
of the hero-to-edit in the route parameter and made it the second item of the link parameters array.
['/hero', hero.id] // { 15 }
The router embedded the id
value in the navigation URL because you had defined it as a route parameter with an :id
placeholder token in the route path
:
{ path: 'hero/:id', component: HeroDetailComponent }
When the user clicks the back button, the HeroDetailComponent
constructs another link parameters array which it uses to navigate back to the HeroListComponent
.
gotoHeroes() { this.router.navigate(['/heroes']); }
This array lacks a route parameter because you had no reason to send information to the HeroListComponent
.
Now you have a reason. You'd like to send the id of the current hero with the navigation request so that the HeroListComponent
can highlight that hero in its list. This is a nice-to-have feature; the list will display perfectly well without it.
Send the id
with an object that contains an optional id
parameter. For demonstration purposes, there's an extra junk parameter (foo
) in the object that the HeroListComponent
should ignore. Here's the revised navigation statement:
gotoHeroes() { let heroId = this.hero ? this.hero.id : null; // Pass along the hero id if available // so that the HeroList component can select that hero. // Include a junk 'foo' property for fun. this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]); }
The application still works. Clicking "back" returns to the hero list view.
Look at the browser address bar.
To see the URL changes in the browser address bar of the live example, open it again in the Plunker editor by clicking the icon in the upper right, then 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 you may not have seen before.
Matrix URL notation is an idea first introduced in a 1996 proposal by the founder of the web, Tim Berners-Lee.
Although matrix notation never made it into the HTML standard, it is legal and it became popular among browser routing systems as a way to isolate parameters belonging to parent and child routes. The Router is such a system and provides support for the matrix notation across browsers.
The syntax may seem strange to you but users are unlikely to notice or care as long as the URL can be emailed and pasted into a browser address bar as this one can.
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 you're about to cover. At the moment this guide is describing the state of affairs prior to those steps.
The HeroListComponent
isn't expecting any parameters at all and wouldn't know what to do with them. You can change that.
Previously, when navigating from the HeroListComponent
to the HeroDetailComponent
, you subscribed to the route params Observable
and made it available to the HeroDetailComponent
in the ActivatedRoute
service. You injected that service in the constructor of the HeroDetailComponent
.
This time you'll be navigating in the opposite direction, from the HeroDetailComponent
to the HeroListComponent
.
First you extend the router import statement to include the ActivatedRoute
service symbol:
import { Router, ActivatedRoute, Params } from '@angular/router';
Import the switchMap
operator to perform an operation on the Observable
of route parameters.
import 'rxjs/add/operator/switchMap'; import { Observable } from 'rxjs/Observable';
Then you inject the ActivatedRoute
in the HeroListComponent
constructor.
export class HeroListComponent implements OnInit { heroes: Observable<Hero[]>; private selectedId: number; constructor( private service: HeroService, private route: ActivatedRoute, private router: Router ) {} ngOnInit() { this.heroes = this.route.params .switchMap((params: Params) => { this.selectedId = +params['id']; return this.service.getHeroes(); }); } }
The ActivatedRoute.params
property is an Observable
of route parameters. The params
emits new id
values when the user navigates to the component. In ngOnInit
you subscribe to those values, set the selectedId
, and get the 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.
Add an isSelected
method that returns true
when a hero's id
matches the selected id
.
isSelected(hero: Hero) { return hero.id === this.selectedId; }
Finally, update the 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:
template: ` <h2>HEROES</h2> <ul class="items"> <li *ngFor="let hero of heroes | async" [class.selected]="isSelected(hero)" (click)="onSelect(hero)"> <span class="badge">{{ hero.id }}</span> {{ hero.name }} </li> </ul> <button routerLink="/sidekicks">Go to sidekicks</button> `
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.
The heroes feature module is almost complete, but what is a feature without some smooth transitions?
This section shows you how to add some animations to the HeroDetailComponent
.
Create an animations.ts
file in the root src/app/
folder. The contents look like this:
import { animate, AnimationEntryMetadata, state, style, transition, trigger } from '@angular/core'; // Component transition animations export const slideInDownAnimation: AnimationEntryMetadata = trigger('routeAnimation', [ state('*', style({ opacity: 1, transform: 'translateX(0)' }) ), transition(':enter', [ style({ opacity: 0, transform: 'translateX(-100%)' }), animate('0.2s ease-in') ]), transition(':leave', [ animate('0.5s ease-out', style({ opacity: 0, transform: 'translateY(100%)' })) ]) ]);
This file does the following:
Imports the animation symbols that build the animation triggers, control state, and manage transitions between states.
Exports a constant named slideInDownAnimation
set to an animation trigger named routeAnimation
; animated components will refer to this name.
Specifies the wildcard state , *
, that matches any animation state that the route component is in.
Defines two transitions, one to ease the component in from the left of the screen as it enters the application view (:enter
), the other to animate the component down as it leaves the application view (:leave
).
You could create more triggers with different transitions for other route components. This trigger is sufficient for the current milestone.
Back in the HeroDetailComponent
, import the slideInDownAnimation
from './animations.ts
. Add the HostBinding
decorator to the imports from @angular/core
; you'll need it in a moment.
Add an animations
array to the @Component
metadata's that contains the slideInDownAnimation
.
Then add three @HostBinding
properties to the class to set the animation and styles for the route component's element.
@HostBinding('@routeAnimation') routeAnimation = true; @HostBinding('style.display') display = 'block'; @HostBinding('style.position') position = 'absolute';
The '@routeAnimation'
passed to the first @HostBinding
matches the name of the slideInDownAnimation
trigger, routeAnimation
. Set the routeAnimation
property to true
because you only care about the :enter
and :leave
states.
The other two @HostBinding
properties style the display and position of the component.
The HeroDetailComponent
will ease in from the left when routed to and will slide down when navigating away.
Applying route animations to individual components works for a simple demo, but in a real life app, it is better to animate routes based on route paths.
You've learned how to do the following:
AppModule
.After these changes, the folder structure looks like this:
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 { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisListComponent } from './crisis-list.component'; import { PageNotFoundComponent } from './not-found.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, AppRoutingModule ], declarations: [ AppComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisListComponent } from './crisis-list.component'; // import { HeroListComponent } from './hero-list.component'; // <-- delete this line import { PageNotFoundComponent } from './not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, // { path: 'heroes', component: HeroListComponent }, // <-- delete this line { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
// TODO SOMEDAY: Feature Componetized like CrisisCenter import 'rxjs/add/operator/switchMap'; import { Observable } from 'rxjs/Observable'; 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 | async" [class.selected]="isSelected(hero)" (click)="onSelect(hero)"> <span class="badge">{{ hero.id }}</span> {{ hero.name }} </li> </ul> <button routerLink="/sidekicks">Go to sidekicks</button> ` }) export class HeroListComponent implements OnInit { heroes: Observable<Hero[]>; private selectedId: number; constructor( private service: HeroService, private route: ActivatedRoute, private router: Router ) {} ngOnInit() { this.heroes = this.route.params .switchMap((params: Params) => { this.selectedId = +params['id']; return this.service.getHeroes(); }); } isSelected(hero: Hero) { return hero.id === this.selectedId; } onSelect(hero: Hero) { this.router.navigate(['/hero', hero.id]); } }
import 'rxjs/add/operator/switchMap'; import { Component, OnInit, HostBinding } from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { slideInDownAnimation } from '../animations'; 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: [ slideInDownAnimation ] }) export class HeroDetailComponent implements OnInit { @HostBinding('@routeAnimation') routeAnimation = true; @HostBinding('style.display') display = 'block'; @HostBinding('style.position') position = 'absolute'; hero: Hero; constructor( private route: ActivatedRoute, private router: Router, private service: HeroService ) {} ngOnInit() { this.route.params // (+) converts string 'id' to a number .switchMap((params: Params) => this.service.getHero(+params['id'])) .subscribe((hero: 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. // Include a junk 'foo' property for fun. 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 { HeroRoutingModule } from './heroes-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroRoutingModule ], declarations: [ HeroListComponent, HeroDetailComponent ], providers: [ HeroService ] }) export class HeroesModule {}
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list.component'; import { HeroDetailComponent } from './hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', component: HeroListComponent }, { path: 'hero/:id', component: HeroDetailComponent } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroRoutingModule { }
It's time to add real features to the app's current placeholder crisis center.
Begin by imitating the heroes feature:
app/crisis-center
folder.app/heroes
into the new crisis center folder.You'll turn the CrisisService
into a purveyor of mock crises instead of mock heroes:
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'), ];
The resulting crisis center is a foundation for introducing a new concept—child routing. You can leave Heroes in its current state as a contrast with the Crisis Center and decide later if the differences are worthwhile.
In keeping with the Separation of Concerns principle, changes to the Crisis Center won't affect the AppModule
or any other feature's component.
This section shows you how to organize the crisis center to conform to the following recommended pattern for Angular applications:
If your app had many feature areas, the app component trees might look like this:
Add the following crisis-center.component.ts
to the crisis-center
folder:
@Component({ template: ` <h2>CRISIS CENTER</h2> <router-outlet></router-outlet> ` }) export class CrisisCenterComponent { }
The CrisisCenterComponent
has the following in common with the AppComponent
:
AppComponent
is the root of the entire application.AppComponent
is a shell to manage the high-level workflow.Like most shells, the CrisisCenterComponent
class is very simple, simpler even than AppComponent
: it has no business logic, and its template has no links, just a title and <router-outlet>
for the crisis center child views.
Unlike AppComponent
, and most other components, it lacks a selector. It doesn't need one since you don't embed this component in a parent template, instead you use the router to navigate to it.
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.
@Component({ template: ` <p>Welcome to the Crisis Center</p> ` }) export class CrisisCenterHomeComponent { }
Create a crisis-center-routing.module.ts
file as you did the heroes-routing.module.ts
file. This time, you define child routes within the parent crisis-center
route.
const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
Notice that the parent crisis-center
route has a children
property with a single route containing the CrisisListComponent
. The CrisisListComponent
route also has a children
array with two routes.
These two routes navigate to the crisis center child components, CrisisCenterHomeComponent
and CrisisDetailComponent
, respectively.
There are important differences in the way the router treats these child routes.
The router displays the components of these routes in the RouterOutlet
of the CrisisCenterComponent
, not in the RouterOutlet
of the AppComponent
shell.
The CrisisListComponent
contains the crisis list and a RouterOutlet
to display the Crisis Center Home
and Crisis Detail
route components.
The Crisis Detail
route is a child of the Crisis List
. Since the router reuses components by default, the Crisis Detail
component will be re-used as you select different crises. In contrast, back in the Hero Detail
route, the component was recreated each time you selected a different hero.
At the top level, paths that begin with /
refer to the root of the application. But child routes extend the path of the parent route. With each step down the route tree, you add a slash followed by the route path, unless the path is empty.
Apply that logic to navigation within the crisis center for which the parent path is /crisis-center
.
To navigate to the CrisisCenterHomeComponent
, the full URL is /crisis-center
(/crisis-center
+ ''
+ ''
).
To navigate to the CrisisDetailComponent
for a crisis with id=2
, the full URL is /crisis-center/2
(/crisis-center
+ ''
+ '/2'
).
The absolute URL for the latter example, including the localhost
origin, is
localhost:3000/crisis-center/2
Here's the complete crisis-center-routing.module.ts
file with its imports.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home.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 } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
As with the HeroesModule
, you must add the CrisisCenterModule
to the imports
array of the AppModule
before the AppRoutingModule
:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './not-found.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesModule, CrisisCenterModule, AppRoutingModule ], declarations: [ AppComponent, PageNotFoundComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }
Remove the initial crisis center route from the app-routing.module.ts
. The feature routes are now provided by the HeroesModule
and the CrisisCenter
modules.
The app-routing.module.ts
file retains the top-level application routes such as the default and wildcard routes.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { ComposeMessageComponent } from './compose-message.component'; import { PageNotFoundComponent } from './not-found.component'; const appRoutes: Routes = [ { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
While building out the crisis center feature, you navigated to the crisis detail route using an absolute path that begins with a slash.
The router matches such absolute paths to routes starting from the top of the route configuration.
You could continue to use absolute paths like this to navigate inside the Crisis Center feature, but that pins the links to the parent routing structure. If you changed the parent /crisis-center
path, you would have to change the link parameters array.
You can free the links from this dependency by defining paths that are relative to the current URL segment. Navigation within the feature area remains intact even if you change the parent route path to the feature.
Here's an example:
The router supports directory-like syntax in a link parameters list to help guide route name lookup:
./
or no leading slash
is relative to the current level.
../
to go up one level in the route path.
You can combine relative navigation syntax with an ancestor path. If you must navigate to a sibling route, you could use the ../<sibling>
convention to go up one level, then over and down the sibling route path.
To navigate a relative path with the Router.navigate
method, you must supply the ActivatedRoute
to give the router knowledge of where you are in the current route tree.
After the link parameters array, add an object with a relativeTo
property set to the ActivatedRoute
. The router then calculates the target URL based on the active route's location.
Always specify the complete absolute path when calling router's navigateByUrl
method.
Update the Crisis List onSelect
method to use relative navigation so you don't have to start from the top of the route configuration.
You've already injected the ActivatedRoute
that you need to compose the relative navigation path.
constructor( private service: CrisisService, private route: ActivatedRoute, private router: Router ) {}
When you visit the Crisis Center, the ancestor path is /crisis-center
, so you only need to add the id
of the Crisis Center to the existing path.
onSelect(crisis: Crisis) { this.selectedId = crisis.id; // Navigate with relative link this.router.navigate([crisis.id], { relativeTo: this.route }); }
If you were using a RouterLink
to navigate instead of the Router
service, you'd use the same link parameters array, but you wouldn't provide the object with the relativeTo
property. The ActivatedRoute
is implicit in a RouterLink
directive.
template: ` <ul class="items"> <li *ngFor="let crisis of crises | async"> <a [routerLink]="[crisis.id]" [class.selected]="isSelected(crisis)"> <span class="badge">{{ crisis.id }}</span> {{ crisis.name }} </a> </li> </ul>`
Update the gotoCrises
method of the CrisisDetailComponent
to navigate back to the Crisis Center list using relative path navigation.
// Relative navigation back to the crises this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
Notice that the path goes up a level using the ../
syntax. If the current crisis id
is 3
, the resulting path back to the crisis list is /crisis-center/;id=3;foo=foo
.
You decide to give users a way to contact the crisis center. When a user clicks a "Contact" button, you want to display a message in a popup view.
The popup should stay open, even when switching between pages in the application, until the user closes it by sending the message or canceling. Clearly you can't put the popup in the same outlet as the other pages.
Until now, you've defined a single outlet and you've nested child routes under that outlet to group routes together. The router only supports one primary unnamed outlet per template.
A template can also have any number of named outlets. Each named outlet has its own set of routes with their own components. Multiple outlets can be displaying different content, determined by different routes, all at the same time.
Add an outlet named "popup" in the AppComponent
, directly below the unnamed outlet.
<router-outlet></router-outlet> <router-outlet name="popup"></router-outlet>
That's where a popup will go, once you learn how to route a popup component to it.
Named outlets are the targets of secondary routes.
Secondary routes look like primary routes and you configure them the same way. They differ in a few key respects.
Create a new component named ComposeMessageComponent
in src/app/compose-message.component.ts
. It displays a simple form with a header, an input box for the message, and two buttons, "Send" and "Cancel".
Here's the component and its template:
import { Component, HostBinding } from '@angular/core'; import { Router } from '@angular/router'; import { slideInDownAnimation } from './animations'; @Component({ templateUrl: './compose-message.component.html', styles: [ ':host { position: relative; bottom: 10%; }' ], animations: [ slideInDownAnimation ] }) export class ComposeMessageComponent { @HostBinding('@routeAnimation') routeAnimation = true; @HostBinding('style.display') display = 'block'; @HostBinding('style.position') position = 'absolute'; details: string; sending: boolean = false; constructor(private router: Router) {} send() { this.sending = true; this.details = 'Sending Message...'; setTimeout(() => { this.sending = false; this.closePopup(); }, 1000); } cancel() { this.closePopup(); } closePopup() { // Providing a `null` value to the named outlet // clears the contents of the named outlet this.router.navigate([{ outlets: { popup: null }}]); } }
<h3>Contact Crisis Center</h3> <div *ngIf="details"> {{ details }} </div> <div> <div> <label>Message: </label> </div> <div> <textarea [(ngModel)]="message" rows="10" cols="35" [disabled]="sending"></textarea> </div> </div> <p *ngIf="!sending"> <button (click)="send()">Send</button> <button (click)="cancel()">Cancel</button> </p>
It looks about the same as any other component you've seen in this guide. There are two noteworthy differences.
Note that the send()
method simulates latency by waiting a second before "sending" the message and closing the popup.
The closePopup()
method closes the popup view by navigating to the popup outlet with a null
. That's a peculiarity covered below.
As with other application components, you add the ComposeMessageComponent
to the declarations
of an NgModule
. Do so in the AppModule
.
Open the AppRoutingModule
and add a new compose
route to the appRoutes
.
{ path: 'compose', component: ComposeMessageComponent, outlet: 'popup' },
The path
and component
properties should be familiar. There's a new property, outlet
, set to 'popup'
. This route now targets the popup outlet and the ComposeMessageComponent
will display there.
The user needs a way to open the popup. Open the AppComponent
and add a "Contact" link.
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
Although the compose
route is pinned to the "popup" outlet, that's not sufficient for wiring the route to a RouterLink
directive. You have to specify the named outlet in a link parameters array and bind it to the RouterLink
with a property binding.
The link parameters array contains an object with a single outlets
property whose value is another object keyed by one (or more) outlet names. In this case there is only the "popup" outlet property and its value is another link parameters array that specifies the compose
route.
You are in effect saying, when the user clicks this link, display the component associated with the compose
route in the popup
outlet.
This outlets
object within an outer object was completely unnecessary when there was only one route and one unnamed outlet to think about.
The router assumed that your route specification targeted the unnamed primary outlet and created these objects for you.
Routing to a named outlet has revealed a previously hidden router truth: you can target multiple outlets with multiple routes in the same RouterLink
directive.
You're not actually doing that here. But to target a named outlet, you must use the richer, more verbose syntax.
Navigate to the Crisis Center and click "Contact". you should see something like the following URL in the browser address bar.
http://.../crisis-center(popup:compose)
The interesting part of the URL follows the ...
:
crisis-center
is the primary navigation. popup
), a colon
separator, and the secondary route path (compose
).Click the Heroes link and look at the URL again.
http://.../heroes(popup:compose)
The primary navigation part has changed; the secondary route is the same.
The router is keeping track of two separate branches in a navigation tree and generating a representation of that tree in the URL.
You can add many more outlets and routes, at the top level and in nested levels, creating a navigation tree with many branches. The router will generate the URL to go with it.
You can tell the router to navigate an entire tree at once by filling out the outlets
object mentioned above. Then pass that object inside a link parameters array to the router.navigate
method.
Experiment with these possibilities at your leisure.
As you've learned, a component in an outlet persists until you navigate away to a new component. Secondary outlets are no different in this regard.
Each secondary outlet has its own navigation, independent of the navigation driving the primary outlet. Changing a current route that displays in the primary outlet has no effect on the popup outlet. That's why the popup stays visible as you navigate among the crises and heroes.
Clicking the "send" or "cancel" buttons does clear the popup view. To see how, look at the closePopup()
method again:
closePopup() { // Providing a `null` value to the named outlet // clears the contents of the named outlet this.router.navigate([{ outlets: { popup: null }}]); }
It navigates imperatively with the Router.navigate()
method, passing in a link parameters array.
Like the array bound to the Contact RouterLink
in the AppComponent
, this one includes an object with an outlets
property. The outlets
property value is another object with outlet names for keys. The only named outlet is 'popup'
.
This time, the value of 'popup'
is null
. That's not a route, but it is a legitimate value. Setting the popup RouterOutlet
to null
clears the outlet and removes the secondary popup route from the current URL.
At the moment, any user can navigate anywhere in the application anytime. That's not always the right thing to do.
You can add guards to the route configuration to handle these scenarios.
A guard's return value controls the router's behavior:
true
, the navigation process continues.false
, the navigation process stops and the user stays put.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.
You can have multiple guards at every level of a routing hierarchy. The router checks the CanDeactivate()
and CanActivateChild()
guards first, from the deepest child route to the top. Then it checks the CanActivate()
guards from the top down to the deepest child route. If the feature module is loaded asynchronously, the CanLoad()
guard is checked before the module is loaded. If any guard returns false, pending guards that have not completed will be canceled, and the entire navigation is canceled.
There are several examples over the next few sections.
Applications often restrict access to a feature area based on who the user is. You could permit access only to authenticated users or to users with a specific role. You might block or limit access until the user's account is activated.
The CanActivate
guard is the tool to manage these navigation business rules.
In this next section, you'll extend the crisis center with some new administrative features. Those features aren't defined yet. But you can start by adding a new feature module named AdminModule
.
Create an admin
folder with a feature module file, a routing configuration file, and supporting components.
The admin feature file structure looks like this:
The admin feature module contains the AdminComponent
used for routing within the feature module, a dashboard route and two unfinished components to manage crises and heroes.
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 { AdminRoutingModule } from './admin-routing.module'; @NgModule({ imports: [ CommonModule, AdminRoutingModule ], 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 the admin dashboard RouterLink
is an empty path route in the AdminComponent
, it is considered a match to any route within the admin feature area. You only want the Dashboard
link to be active when the user visits that route. Adding an additional binding to the Dashboard
routerLink, [routerLinkActiveOptions]="{ exact: true }"
, marks the ./
link as active when the user navigates to the /admin
URL and not when navigating to any of the child routes.
The initial admin routing configuration:
const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
Looking at the child route under the AdminComponent
, there is a path
and a children
property but it's not using a component
. You haven't made a mistake in the configuration. You've defined a component-less route.
The goal is to group the Crisis Center
management routes under the admin
path. You don't need a component to do it. A component-less route makes it easier to guard child routes.
Next, import the AdminModule
into app.module.ts
and add it to the imports
array to register the admin routes.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './not-found.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { AdminModule } from './admin/admin.module'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesModule, CrisisCenterModule, AdminModule, AppRoutingModule ], declarations: [ AppComponent, PageNotFoundComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { }
Add an "Admin" link to the AppComponent
shell so that users can get to this feature.
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]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <router-outlet></router-outlet> <router-outlet name="popup"></router-outlet> `
Currently every route within the Crisis Center is open to everyone. The new admin feature should be accessible only to authenticated users.
You could hide the link until the user logs in. But that's tricky and difficult to maintain.
Instead you'll write a CanActivate()
guard to redirect anonymous users to the login page when they try to enter the admin area.
This is a general purpose guard—you can imagine other features that require authenticated users—so you create an auth-guard.service.ts
in the application root folder.
At the moment you're interested in seeing how guards work so the first version does nothing useful. It simply logs to console and returns
true immediately, allowing navigation to proceed:
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, open admin-routing.module.ts
, import the AuthGuard
class, and update the admin route with a CanActivate()
guard property that references it:
import { AuthGuard } from '../auth-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 } ], } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
The admin feature is now protected by the guard, albeit protected poorly.
Make the 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
:
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 you need for this discussion. It has an isLoggedIn
flag to tell you whether the user is authenticated. Its login
method simulates an API call to an external service by returning an Observable that resolves successfully after a short pause. The redirectUrl
property will store the attempted URL so you can navigate to it after authenticating.
Revise the AuthGuard
to call it.
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 you inject the AuthService
and the Router
in the constructor. You haven't provided the AuthService
yet but it's good to know that you can inject helpful services into routing guards.
This guard returns a synchronous boolean result. If the user is logged in, it returns true and the navigation continues.
The ActivatedRouteSnapshot
contains the future route that will be activated and the RouterStateSnapshot
contains the future RouterState
of the application, should you pass through the guard check.
If the user is not logged in, you store the attempted URL the user came from using the RouterStateSnapshot.url
and tell the router to navigate to a login page—a page you haven't created yet. This secondary navigation automatically cancels the current navigation; checkLogin()
returns false
just to be clear about that.
You need a LoginComponent
for the user to log in to the app. After logging in, you'll redirect to the stored URL if available, or use the default URL. There is nothing new about this component or the way you wire it into the router configuration.
Register a /login
route in the login-routing.module.ts
and add the necessary providers to the providers
array. In app.module.ts
, import the LoginComponent
and add it to the AppModule
declarations
. Import and add the LoginRoutingModule
to the AppModule
imports as well.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { ComposeMessageComponent } from './compose-message.component'; import { LoginRoutingModule } from './login-routing.module'; import { LoginComponent } from './login.component'; import { PageNotFoundComponent } from './not-found.component'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, LoginRoutingModule, AppRoutingModule, BrowserAnimationsModule ], declarations: [ AppComponent, ComposeMessageComponent, LoginComponent, PageNotFoundComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { // Diagnostic only: inspect router configuration constructor(router: Router) { console.log('Routes: ', JSON.stringify(router.config, undefined, 2)); } }
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 { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from './auth-guard.service'; import { AuthService } from './auth.service'; import { LoginComponent } from './login.component'; const loginRoutes: Routes = [ { path: 'login', component: LoginComponent } ]; @NgModule({ imports: [ RouterModule.forChild(loginRoutes) ], exports: [ RouterModule ], providers: [ AuthGuard, AuthService ] }) export class LoginRoutingModule {}
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.
You can also protect child routes with the CanActivateChild
guard. The CanActivateChild
guard is similar to the CanActivate
guard. The key difference is that it runs before any child route is activated.
You protected the admin feature module from unauthorized access. You should also protect child routes within the feature module.
Extend the AuthGuard
to protect when navigating between the admin
routes. Open auth-guard.service.ts
and add the CanActivateChild
interface to the imported tokens from the router package.
Next, implement the CanActivateChild
method which takes the same arguments as the CanActivate
method: an ActivatedRouteSnapshot
and RouterStateSnapshot
. The CanActivateChild
method can return an Observable<boolean>
or Promise<boolean>
for async checks and a boolean
for sync checks. This one returns a boolean
:
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); } /* . . . */ }
Add the same AuthGuard
to the component-less
admin route to protect all other child routes at one time instead of adding the AuthGuard
to each route individually.
const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', canActivateChild: [AuthGuard], children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
Back in the "Heroes" workflow, the app accepts every change to a hero immediately without hesitation or validation.
In the real world, you might have to accumulate the users changes. You might have to validate across fields. You might have to validate on the server. You 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 you do about unapproved, unsaved changes when the user navigates away? You can't just leave and risk losing the user's changes; that would be a terrible experience.
It's better to pause and let the user decide what to do. If the user cancels, you'll stay put and allow more changes. If the user approves, the app can save.
You still might delay navigation until the save succeeds. If you let the user move to the next screen immediately and the save were to fail (perhaps the data are ruled invalid), you would lose the context of the error.
You can't block while waiting for the server—that's not possible in a browser. You need to stop the navigation while you wait, asynchronously, for the server to return with its answer.
You need the CanDeactivate
guard.
The sample application doesn't talk to a server. Fortunately, you 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. Instead, the app updates the entity when the user presses the Save button and discards the changes when the user presses the Cancel button.
Both buttons navigate back to the crisis list after save or cancel.
cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); }
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?
This demo does neither. Instead, it asks the user to make that choice explicitly in a confirmation dialog box that waits asynchronously for the user's answer.
You could wait for the user's answer with synchronous, blocking code. The 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
).
Create a guard that checks for the presence of a canDeactivate
method in a component—any component. The CrisisDetailComponent
will have this method. But the guard doesn't have to know that. The guard shouldn't know the details of any component's deactivation method. It need only detect that the component has a canDeactivate()
method and call it. This approach makes the guard reusable.
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, you could make a component-specific CanDeactivate
guard for the CrisisDetailComponent
. The canDeactivate()
method provides you with the current instance of the component
, the current ActivatedRoute
, and RouterStateSnapshot
in case you needed to access some external information. This would be useful if you only wanted to use this guard for this component and needed to get the component's properties or confirm whether the router should allow navigation away from it.
import { Injectable } from '@angular/core'; import { 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 the CrisisDetailComponent
, it implements the confirmation workflow for unsaved changes.
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 falsy (stay put).
Add the Guard
to the crisis detail route in crisis-center-routing.module.ts
using the canDeactivate
array.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } 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 } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
Add the Guard
to the main AppRoutingModule
providers
array so the Router
can inject it during the navigation process.
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { ComposeMessageComponent } from './compose-message.component'; import { CanDeactivateGuard } from './can-deactivate-guard.service'; import { PageNotFoundComponent } from './not-found.component'; const appRoutes: Routes = [ { path: 'compose', component: ComposeMessageComponent, outlet: 'popup' }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ], providers: [ CanDeactivateGuard ] }) export class AppRoutingModule {}
Now you have given the user a safeguard against unsaved changes.
In the Hero Detail
and Crisis Detail
, the app waited until the route was activated to fetch the respective hero or crisis.
This worked well, but there's a better way. If you were using a real world API, there might be some delay before the data to display is returned from the server. You don't want to display a blank component while waiting for the data.
It's preferable to pre-fetch data from the server so it's ready the moment the route is activated. This also allows you to handle errors before routing to the component. There's no point in navigating to a crisis detail for an id
that doesn't have a record. It'd be better to send the user back to the Crisis List
that shows only valid crisis centers.
In summary, you want to delay rendering the routed component until all necessary data have been fetched.
You need a resolver.
At the moment, the CrisisDetailComponent
retrieves the selected crisis. If the crisis is not found, it navigates back to the crisis list view.
The experience might be better if all of this were handled first, before the route is activated. A CrisisDetailResolver
service could retrieve a Crisis
or navigate away if the Crisis
does not exist before activating the route and creating the CrisisDetailComponent
.
Create the crisis-detail-resolver.service.ts
file within the Crisis Center
feature area.
import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; @Injectable() export class CrisisDetailResolver implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Crisis> { 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 null; } }); } }
Take the relevant parts of the crisis retrieval logic in CrisisDetailComponent.ngOnInit
and move them into the CrisisDetailResolver
. Import the Crisis
model, CrisisService
, and the Router
so you can navigate elsewhere if you can't fetch the crisis.
Be explicit. Implement the Resolve
interface with a type of Crisis
.
Inject the CrisisService
and Router
and implement the resolve()
method. That method could return a Promise
, an Observable
, or a synchronous return value.
The CrisisService.getCrisis
method returns a promise. Return that promise to prevent the route from loading until the data is fetched. If it doesn't return a valid Crisis
, navigate the user back to the CrisisListComponent
, canceling the previous in-flight navigation to the CrisisDetailComponent
.
Import this resolver in the crisis-center-routing.module.ts
and add a resolve
object to the CrisisDetailComponent
route configuration.
Remember to add the CrisisDetailResolver
service to the CrisisCenterRoutingModule
's providers
array.
import { CrisisDetailResolver } from './crisis-detail-resolver.service'; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ], providers: [ CrisisDetailResolver ] }) export class CrisisCenterRoutingModule { }
The CrisisDetailComponent
should no longer fetch the crisis. Update the CrisisDetailComponent
to get the crisis from the ActivatedRoute.data.crisis
property instead; that's where you said it should be when you re-configured the route. It will be there when the CrisisDetailComponent
ask for it.
ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); }
Two critical points
The router's Resolve
interface is optional. The CrisisDetailResolver
doesn't inherit from a base class. The router looks for that method and calls it if found.
Rely on the router to call the resolver. Don't worry about all the ways that the user could navigate away. That's the router's job. Write this class and let the router take it from there.
The relevant Crisis Center code for this milestone follows.
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> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <router-outlet></router-outlet> <router-outlet name="popup"></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 { NgModule } from '@angular/core'; import { RouterModule, Routes } 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 { CrisisDetailResolver } from './crisis-detail-resolver.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: CrisisDetailResolver } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ], providers: [ CrisisDetailResolver ] }) export class CrisisCenterRoutingModule { }
import 'rxjs/add/operator/switchMap'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router, Params } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Crisis, CrisisService } from './crisis.service'; @Component({ template: ` <ul class="items"> <li *ngFor="let crisis of crises | async" (click)="onSelect(crisis)" [class.selected]="isSelected(crisis)"> <span class="badge">{{ crisis.id }}</span> {{ crisis.name }} </li> </ul> <router-outlet></router-outlet> ` }) export class CrisisListComponent implements OnInit { crises: Observable<Crisis[]>; selectedId: number; constructor( private service: CrisisService, private route: ActivatedRoute, private router: Router ) {} isSelected(crisis: Crisis) { return crisis.id === this.selectedId; } ngOnInit() { this.crises = this.route.params .switchMap((params: Params) => { this.selectedId = +params['id']; return this.service.getCrises(); }); } onSelect(crisis: Crisis) { this.selectedId = crisis.id; // Navigate with relative link this.router.navigate([crisis.id], { relativeTo: this.route }); } }
import { Component, OnInit, HostBinding } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { slideInDownAnimation } from '../animations'; 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: [ slideInDownAnimation ] }) export class CrisisDetailComponent implements OnInit { @HostBinding('@routeAnimation') routeAnimation = true; @HostBinding('style.display') display = 'block'; @HostBinding('style.position') position = 'absolute'; crisis: Crisis; editName: string; constructor( private route: ActivatedRoute, private router: Router, public dialogService: DialogService ) {} ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); } cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); } canDeactivate(): 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, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { Crisis, CrisisService } from './crisis.service'; @Injectable() export class CrisisDetailResolver implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Crisis> { 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 null; } }); } }
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; } }
In the route parameters example, you only dealt with parameters specific to the route, but what if you wanted optional parameters available to all routes? This is where query parameters come into play.
Fragments refer to certain elements on the page identified with an id
attribute.
Update the AuthGuard
to provide a session_id
query that will remain after navigating to another route.
Add an anchor
element so you can jump to a certain point on the page.
Add the NavigationExtras
object to the router.navigate
method that navigates you to the /login
route.
import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, NavigationExtras } 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; } }
You can also preserve query parameters and fragments across navigations without having to provide them again when navigating. In the LoginComponent
, you'll add an object as the second argument in the router.navigate
function and provide the preserveQueryParams
and preserveFragment
to pass along the current query parameters and fragment to the next route.
// Set our navigation extras object // that passes on our global query params and fragment let navigationExtras: NavigationExtras = { preserveQueryParams: true, preserveFragment: true }; // Redirect the user this.router.navigate([redirect], navigationExtras);
Since you'll be navigating to the Admin Dashboard route after logging in, you'll update it to handle the query parameters and fragment.
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. Just like route parameters, the query parameters and fragments are provided as an Observable
. The updated Crisis Admin component feeds the Observable
directly into the template using the AsyncPipe
.
To see the URL changes in the browser address bar of the live example, open it again in the Plunker editor by clicking the icon in the upper right, then pop out the preview window by clicking the blue 'X' button in the upper right corner. |
|
Now, you can click on the Admin button, which takes you to the Login page with the provided query params
and fragment
. After you click the login button, notice that you have been redirected to the Admin Dashboard
page with the query params
and fragment
still intact.
You can use these persistent bits of information for things that need to be provided across pages like authentication tokens or session ids.
The query params
and fragment
can also be preserved using a RouterLink
with the preserveQueryParams
and preserveFragment
bindings respectively.
As you've worked through the milestones, the application has naturally gotten larger. As you continue to build out feature areas, the overall application size will continue to grow. At some point you'll reach a tipping point where the application takes long time to load.
How do you combat this problem? With asynchronous routing, which loads feature modules lazily, on request. Lazy loading has multiple benefits.
You're already made part way there.
By organizing the application into modules—AppModule
, HeroesModule
, AdminModule
and CrisisCenterModule
—you have natural candidates for lazy loading.
Some modules, like AppModule
, must be loaded from the start. But others can and should be lazy loaded. The AdminModule
, for example, is needed by a few authorized users, so you should only load it when requested by the right people.
Change the admin
path in the admin-routing.module.ts
from 'admin'
to an empty string, ''
, the empty path.
The Router
supports empty path routes; use them to group routes together without adding any additional path segments to the URL. Users will still visit /admin
and the AdminComponent
still serves as the Routing Component containing child routes.
Open the AppRoutingModule
and add a new admin
route to its appRoutes
array.
Give it a loadChildren
property (not a children
property!), set to the address of the AdminModule
. The address is the AdminModule
file location (relative to the app root), followed by a #
separator, followed by the name of the exported module class, AdminModule
.
{ path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', },
When the router navigates to this route, it uses the loadChildren
string to dynamically load the AdminModule
. Then it adds the AdminModule
routes to its current route configuration. Finally, it loads the requested route to the destination admin component.
The lazy loading and re-configuration happen just once, when the route is first requested; the module and routes are available immediately for subsequent requests.
Angular provides a built-in module loader that supports SystemJS to load modules asynchronously. If you were using another bundling tool, such as Webpack, you would use the Webpack mechanism for asynchronously loading modules.
Take the final step and detach the admin feature set from the main application. The root AppModule
must neither load nor reference the AdminModule
or its files.
In app.module.ts
, remove the AdminModule
import statement from the top of the file and remove the AdminModule
from the Angular module's imports
array.
You're already protecting the AdminModule
with a CanActivate
guard that prevents unauthorized users from accessing the admin feature area. It redirects to the login page if the user is not authorized.
But the router is still loading the AdminModule
even if the user can't visit any of its components. Ideally, you'd only load the AdminModule
if the user is logged in.
Add a CanLoad
guard that only loads the AdminModule
once the user is logged in and attempts to access the admin feature area.
The existing AuthGuard
already has the essential logic in its checkLogin()
method to support the CanLoad
guard.
Open auth-guard.service.ts
. Import the CanLoad
interface from @angular/router
. Add it to the AuthGuard
class's implements
list. Then implement canLoad
as follows:
canLoad(route: Route): boolean { let url = `/${route.path}`; return this.checkLogin(url); }
The router sets the canLoad()
method's route
parameter to the intended destination URL. The checkLogin()
method redirects to that URL once the user has logged in.
Now import the AuthGuard
into the AppRoutingModule
and add the AuthGuard
to the canLoad
array for the admin
route. The completed admin route looks like this:
{ path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [AuthGuard] },
You've learned how to load modules on-demand. You can also load modules asynchronously with preloading.
This may seem like what the app has been doing all along. Not quite. The AppModule
is loaded when the application starts; that's eager loading. Now the AdminModule
loads only when the user clicks on a link; that's lazy loading.
Preloading is something in between. Consider the Crisis Center. It isn't the first view that a user sees.
By default, the Heroes are the first view. For the smallest initial payload and fastest launch time, you should eagerly load the AppModule
and the HeroesModule
.
You could lazy load the Crisis Center. But you're almost certain that the user will visit the Crisis Center within minutes of launching the app. Ideally, the app would launch with just the AppModule
and the HeroesModule
loaded and then, almost immediately, load the CrisisCenterModule
in the background. By the time the user navigates to the Crisis Center, its module will have been loaded and ready to go.
That's preloading.
After each successful navigation, the router looks in its configuration for an unloaded module that it can preload. Whether it preloads a module, and which modules it preloads, depends upon the preload strategy.
The Router
offers two preloading strategies out of the box:
Out of the box, the router either never preloads, or preloads every lazy load module. The Router
also supports custom preloading strategies for fine control over which modules to preload and when.
In this next section, you'll update the CrisisCenterModule
to load lazily by default and use the PreloadAllModules
strategy to load it (and all other lazy loaded modules) as soon as possible.
Update the route configuration to lazy load the CrisisCenterModule
. Take the same steps you used to configure AdminModule
for lazy load.
Change the crisis-center
path in the CrisisCenterRoutingModule
to an empty string.
Add a crisis-center
route to the AppRoutingModule
.
Set the loadChildren
string to load the CrisisCenterModule
.
Remove all mention of the CrisisCenterModule
from app.module.ts
.
Here are the updated modules before enabling preload:
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { ComposeMessageComponent } from './compose-message.component'; import { LoginRoutingModule } from './login-routing.module'; import { LoginComponent } from './login.component'; import { PageNotFoundComponent } from './not-found.component'; import { DialogService } from './dialog.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HeroesModule, LoginRoutingModule, AppRoutingModule, BrowserAnimationsModule ], declarations: [ AppComponent, ComposeMessageComponent, LoginComponent, PageNotFoundComponent ], providers: [ DialogService ], bootstrap: [ AppComponent ] }) export class AppModule { // Diagnostic only: inspect router configuration constructor(router: Router) { console.log('Routes: ', JSON.stringify(router.config, undefined, 2)); } }
import { NgModule } from '@angular/core'; import { RouterModule, Routes, } from '@angular/router'; import { ComposeMessageComponent } from './compose-message.component'; import { PageNotFoundComponent } from './not-found.component'; import { CanDeactivateGuard } from './can-deactivate-guard.service'; import { AuthGuard } from './auth-guard.service'; const appRoutes: Routes = [ { path: 'compose', component: ComposeMessageComponent, outlet: 'popup' }, { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [AuthGuard] }, { path: 'crisis-center', loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule' }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes ) ], exports: [ RouterModule ], providers: [ CanDeactivateGuard ] }) export class AppRoutingModule {}
import { NgModule } from '@angular/core'; import { RouterModule, Routes } 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 { CrisisDetailResolver } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: '', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolver } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ], providers: [ CrisisDetailResolver ] }) export class CrisisCenterRoutingModule { }
You could try this now and confirm that the CrisisCenterModule
loads after you click the "Crisis Center" button.
To enable preloading of all lazy loaded modules, import the PreloadAllModules
token from the Angular router package.
The second argument in the RouterModule.forRoot
method takes an object for additional configuration options. The preloadingStrategy
is one of those options. Add the PreloadAllModules
token to the forRoot
call:
RouterModule.forRoot( appRoutes , { preloadingStrategy: PreloadAllModules } )
This tells the Router
preloader to immediately load all lazy loaded routes (routes with a loadChildren
property).
When you visit http://localhost:3000
, the /heroes
route loads immediately upon launch and the router starts loading the CrisisCenterModule
right after the HeroesModule
loads.
Surprisingly, the AdminModule
does not preload. Something is blocking it.
The PreloadAllModules
strategy does not load feature areas protected by a CanLoad guard. This is by design.
You added a CanLoad
guard to the route in the AdminModule
a few steps back to block loading of that module until the user is authorized. That CanLoad
guard takes precedence over the preload strategy.
If you want to preload a module and guard against unauthorized access, drop the canLoad
guard and rely on the CanActivate guard alone.
Preloading every lazy loaded modules works well in many situations, but it isn't always the right choice, especially on mobile devices and over low bandwidth connections. You may choose to preload only certain feature modules, based on user metrics and other business and technical factors.
You can control what and how the router preloads with a custom preloading strategy.
In this section, you'll add a custom strategy that only preloads routes whose data.preload
flag is set to true
. Recall that you can add anything to the data
property of a route.
Set the data.preload
flag in the crisis-center
route in the AppRoutingModule
.
{ path: 'crisis-center', loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule', data: { preload: true } },
Add a new file to the project called selective-preloading-strategy.ts
and define a SelectivePreloadingStrategy
service class as follows:
import 'rxjs/add/observable/of'; import { Injectable } from '@angular/core'; import { PreloadingStrategy, Route } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @Injectable() export class SelectivePreloadingStrategy implements PreloadingStrategy { preloadedModules: string[] = []; preload(route: Route, load: () => Observable<any>): Observable<any> { if (route.data && route.data['preload']) { // add the route path to the preloaded module array this.preloadedModules.push(route.path); // log the route path to the console console.log('Preloaded: ' + route.path); return load(); } else { return Observable.of(null); } } }
SelectivePreloadingStrategy
implements the PreloadingStrategy
, which has one method, preload
.
The router calls the preload
method with two arguments:
An implementation of preload
must return an Observable
. If the route should preload, it returns the observable returned by calling the loader function. If the route should not preload, it returns an Observable
of null
.
In this sample, the preload
method loads the route if the route's data.preload
flag is truthy.
It also has a side-effect. SelectivePreloadingStrategy
logs the path
of a selected route in its public preloadedModules
array.
Shortly, you'll extend the AdminDashboardComponent
to inject this service and display its preloadedModules
array.
But first, make a few changes to the AppRoutingModule
.
SelectivePreloadingStrategy
into AppRoutingModule
.PreloadAllModules
strategy in the call to forRoot
with this SelectivePreloadingStrategy
. SelectivePreloadingStrategy
strategy to the AppRoutingModule
providers array so it can be injected elsewhere in the app.Now edit the AdminDashboardComponent
to display the log of preloaded routes.
SelectivePreloadingStrategy
(it's a service).preloadedModules
array.When you're done it looks like this.
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { SelectivePreloadingStrategy } from '../selective-preloading-strategy'; 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> Preloaded Modules <ul> <li *ngFor="let module of modules">{{ module }}</li> </ul> ` }) export class AdminDashboardComponent implements OnInit { sessionId: Observable<string>; token: Observable<string>; modules: string[]; constructor( private route: ActivatedRoute, private preloadStrategy: SelectivePreloadingStrategy ) { this.modules = preloadStrategy.preloadedModules; } 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'); } }
Once the application loads the initial route, the CrisisCenterModule
is preloaded. Verify this by logging in to the Admin
feature area and noting that the crisis-center
is listed in the Preloaded Modules
. It's also logged to the browser's console.
You put a lot of effort into configuring the router in several routing module files and were careful to list them in the proper order. Are routes actually evaluated as you planned? How is the router really configured?
You can inspect the router's current configuration any time by injecting it and examining its config
property. For example, update the AppModule
as follows and look in the browser console window to see the finished route configuration.
import { Router } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; export class AppModule { // Diagnostic only: inspect router configuration constructor(router: Router) { console.log('Routes: ', JSON.stringify(router.config, undefined, 2)); } }
You've covered a lot of ground in this guide and the application is too big to reprint here. Please visit the where you can download the final source code.
The balance of this guide is a set of appendices that elaborate some of the points you covered quickly above.
The appendix material isn't essential. Continued reading is for the curious.
A link parameters array holds the following ingredients for router navigation:
You can bind the RouterLink
directive to such an array like this:
<a [routerLink]="['/heroes']">Heroes</a>
You've written a two element array when specifying a route parameter like this:
this.router.navigate(['/hero', hero.id]);
You can provide optional route parameters in an object like this:
<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>
These three examples cover the need for an app with one level routing. The moment you add a child router, such as the crisis center, you create new link array possibilities.
Recall that you specified a default child route for the crisis center so this simple RouterLink
is fine.
<a [routerLink]="['/crisis-center']">Crisis Center</a>
Parse it out.
/crisis-center
).CrisisListComponent
, whose route path is /
, but you don't need to explicitly add the slash.['/crisis-center']
.Take it a step further. Consider the following router link that navigates from the root of the application down to the Dragon Crisis:
<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>
/crisis-center
)./:id
).id
route parameter.id
of the Dragon Crisis as the second item in the array (1
)./crisis-center/1
.If you wanted to, you could redefine the 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, you 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.
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 HTML5 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 "HTML5 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 "HTML5 pushState" style.HashLocationStrategy
—the "hash URL" style.The RouterModule.forRoot
function sets the LocationStrategy
to the PathLocationStrategy
, making it the default strategy. You can switch to the HashLocationStrategy
with an override during the bootstrapping process if you prefer it.
Learn about providers and the bootstrap process in the Dependency Injection guide.
You must choose a strategy and you 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 projects should use the default HTML5 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.
While the router uses the HTML5 pushState style by default, you 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.
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 HTML5 URLs by taking two remedial steps:
You can go old-school with the HashLocationStrategy
by providing the useHash: true
in an object as the second argument of the RouterModule.forRoot
in the AppModule
.
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'; import { PageNotFoundComponent } from './not-found.component'; const routes: Routes = [ ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot(routes, { useHash: true }) // .../#/crisis-center/ ], declarations: [ AppComponent, PageNotFoundComponent ], providers: [ ], bootstrap: [ AppComponent ] }) export class AppModule { }
© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://angular.io/docs/ts/latest/guide/router.html