Routing & Navigation

The Angular Router enables navigation from one view to the next as users perform application tasks.

We cover the router's primary features in this chapter, illustrating them through the evolution of a small application that we can run live.

pop out the window

To see the URL changes in the browser address bar, pop out the preview window by clicking the blue 'X' button in the upper right corner.

Overview

The browser is a familiar model of application navigation. We enter a URL in the address bar and the browser navigates to a corresponding page. We click links on the page and the browser navigates to a new page. We click the browser's back and forward buttons and the browser navigates backward and forward through the history of pages we've seen.

The Angular Router ("the router") borrows from this model. It can interpret a browser URL as an instruction to navigate to a client-generated view and pass optional parameters along to the supporting view component to help it decide what specific content to present. We can bind the router to links on a page and it will navigate to the appropriate application view when the user clicks a link. We can navigate imperatively when the user clicks a button, selects from a drop box, or in response to some other stimulus from any source. And the router logs activity in the browser's history journal so the back and forward buttons work as well.

We'll learn many router details in this chapter which covers

We proceed in phases marked by milestones building from a simple two-pager with placeholder views up to a modular, multi-view design with child routes.

But first, an overview of router basics.

The Basics

Let's begin with a few core concepts of the Router. Then we can explore the details through a sequence of examples.

<base href>

Most routing applications should add a <base> element to the index.html as the first child in the <head> tag to tell the router how to compose navigation URLs.

If the app folder is the application root, as it is for our sample application, set the href value exactly as shown here.

index.html (base-href)

<base href="/">

Router imports

The Angular Router is an optional service that presents a particular component view for a given URL. It is not part of the Angular 2 core. It is in its own library package, @angular/router. We import what we need from it as we would from any other Angular package.

app/app.routing.ts (import)

import { Routes, RouterModule } from '@angular/router';

We cover other options in the details below.

Configuration

The application will have one router. When the browser's URL changes, the router looks for a corresponding Route from which it can determine the component to display.

A router has no routes until we configure it. We bootstrap our application with an array of routes that we'll provide to our RouterModule.forRoot function.

In the following example, we configure our application with four route definitions.

app/app.routing.ts (excerpt)

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const appRoutes: Routes = [
  { path: 'hero/:id', component: HeroDetailComponent },
  { path: 'crisis-center', component: CrisisCenterComponent },
  {
    path: 'heroes',
    component: HeroListComponent,
    data: {
      title: 'Heroes List'
    }
  },
  { path: '', component: HomeComponent },
  { path: '**', component: PageNotFoundComponent }
];

export const appRoutingProviders: any[] = [

];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

The Routes is an array of routes that describe how to navigate. Each Route maps a URL path to a component.

There are no leading slashes in our path. The router parses and builds the URL for us, allowing us to use relative and absolute paths when navigating between application views.

The :id in the first route is a token for a route parameter. In a URL such as /hero/42, "42" is the value of the id parameter. The corresponding HeroDetailComponent will use that value to find and present the hero whose id is 42. We'll learn more about route parameters later in this chapter.

The data property in the third route is a place to store arbitrary data associated with each specific route. This data is accessible within each activated route and can be used to store items such as page titles, breadcrumb text and other read-only data. We'll use the resolve guard to retrieve additional data later in the chapter.

The empty path in the fourth route matches as the default path for each level of routing. It also allows for adding routes without extending the URL path.

The ** in the last route denotes a wildcard path for our route. The router will match this route if the URL requested doesn't match any paths for routes defined in our configuration. This is useful for displaying a 404 page or redirecting to another route.

The order of the routes in the configuration matters and this is by design. The router uses a first-match wins strategy when matching routes, so more specific routes should be placed above less specific routes. In our configuration above, the routes with a static path are listed first, followed by an empty path route, that matches as the default route. The wildcard route is listed last as it's the most generic route and should be matched only if no other routes are matched first.

We export the routing constant so we can import it into our app.module.ts file where we'll add a configured Router module to our AppModule imports.

Next we open app.module.ts where we must register our routing, routing providers, and declare our two route components.

app/app.module.ts (basic setup)

import { AppComponent }         from './app.component';
import { routing,
         appRoutingProviders }  from './app.routing';

import { HeroListComponent }    from './hero-list.component';
import { CrisisListComponent }  from './crisis-list.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent
  ],
  providers: [
    appRoutingProviders
  ],
  bootstrap: [ AppComponent ]
})

Router Outlet

Given this configuration, when the browser URL for this application becomes /heroes, the router matches that URL to the Route path /heroes and displays the HeroListComponent in a RouterOutlet that we've placed in the host view's HTML.

<!-- Routed views go here -->
<router-outlet></router-outlet>

Router Links

Now we have routes configured and a place to render them, but how do we navigate? The URL could arrive directly from the browser address bar. But most of the time we navigate as a result of some user action such as the click of an anchor tag.

We add a RouterLink directive to the anchor tag. Since we know our link doesn't contain any dynamic information, we can use a one-time binding to our route path.

If our RouterLink needed to be more dynamic we could bind to a template expression that returns an array of route link parameters (the link parameters array). The router ultimately resolves that array into a URL and a component view.

We also add a RouterLinkActive directive to each anchor tag to add or remove CSS classes to the element when the associated RouterLink becomes active. The directive can be added directly on the element or on its parent element.

We see such bindings in the following AppComponent template:

template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

We're adding two anchor tags with RouterLink and RouterLinkActive directives. We bind each RouterLink to a string containing the path of a route. '/crisis-center' and '/heroes' are the paths of the Routes we configured above.

We'll learn to write link expressions — and why they are arrays — later in the chapter.

We define active as the CSS class we want toggled to each RouterLink when they become the current route using the RouterLinkActive directive. We could add multiple classes to the RouterLink if we so desired.

Router State

After the end of each successful navigation lifecycle, the router builds a tree of ActivatedRoute objects that make up the current state of the router. We can access the current RouterState from anywhere in our application using the Router service and the routerState property.

The router state provides us with methods to traverse up and down the route tree from any activated route to get information we may need from parent, child and sibling routes.

Let's summarize

The application is provided with a configured router. The component has a RouterOutlet where it can display views produced by the router. It has RouterLinks that users can click to navigate via the router.

Here are the key Router terms and their meanings:

Router Part Meaning
Router Displays the application component for the active URL. Manages navigation from one component to the next.
RouterModule A separate Angular module that provides the necessary service providers and directives for navigating through application views.
Routes Defines an array of Routes, each mapping a URL path to a component.
Route Defines how the router should navigate to a component based on a URL pattern. Most routes consist of a path and a component type.
RouterOutlet The directive (<router-outlet>) that marks where the router should display a view.
RouterLink The directive for binding a clickable HTML element to a route. Clicking an anchor tag with a routerLink directive that is bound to a string or a Link Parameters Array triggers a navigation.
RouterLinkActive The directive for adding/removing classes from an HTML element when an associated routerLink contained on or inside the element becomes active/inactive.
ActivatedRoute A service that is provided to each route component that contains route specific information such as route parameters, static data, resolve data, global query params and the global fragment.
RouterState The current state of the router including a tree of the currently activated routes in our application along convenience methods for traversing the route tree.
Link Parameters Array An array that the router interprets into a routing instruction. We can bind a RouterLink to that array or pass the array as an argument to the Router.navigate method.
Routing Component An Angular component with a RouterOutlet that displays views based on router navigations.

We've barely touched the surface of the router and its capabilities.

The following detail sections describe a sample routing application as it evolves over a sequence of milestones. We strongly recommend taking the time to read and understand this story.

The Sample Application

We have an application in mind as we move from milestone to milestone.

While we make incremental progress toward the ultimate sample application, this chapter is not a tutorial. We discuss code and design decisions pertinent to routing and application design. We gloss over everything in between.

The full source is available in the live example.

Our client is the Hero Employment Agency. Heroes need work and The Agency finds Crises for them to solve.

The application has three main feature areas:

  1. A Crisis Center where we maintain the list of crises for assignment to heroes.
  2. A Heroes area where we maintain the list of heroes employed by The Agency.
  3. An Admin area where we manage the list of crises and heroes displayed.

Run the live example. It opens in the Crisis Center. We'll come back to that.

Click the Heroes link. We're presented with a list of Heroes.

Hero List

We select one and the application takes us to a hero editing screen.

Crisis Center Detail

Our changes take effect immediately. We click the "Back" button and the app returns us to the Heroes list.

We could have clicked the browser's back button instead. That would have returned us to the Heroes List as well. Angular app navigation updates the browser history as normal web navigation does.

Now click the Crisis Center link. We go to the Crisis Center and its list of ongoing crises.

Crisis Center List

We select one and the application takes us to a crisis editing screen.

Crisis Center Detail

This is a bit different from the Hero Detail. Hero Detail saves the changes as we type. In Crisis Detail our changes are temporary until we either save or discard them by pressing the "Save" or "Cancel" buttons. Both buttons navigate back to the Crisis Center and its list of crises.

Suppose we click a crisis, make a change, but do not click either button. Maybe we click the browser back button instead. Maybe we click the "Heroes" link.

Do either. Up pops a dialog box.

Confirm Dialog

We can say "OK" and lose our changes or click "Cancel" and continue editing.

The router supports a CanDeactivate guard that gives us a chance to clean-up or ask the user's permission before navigating away from the current view.

Here we see an entire user session that touches all of these features.

App in action

Here's a diagram of all application routing options:

Navigation diagram

This app illustrates the router features we'll cover in this chapter

  • organizing the application features into modules
  • navigating to a component (Heroes link to "Heroes List")
  • including a route parameter (passing the Hero id while routing to the "Hero Detail")
  • child routes (the Crisis Center has its own routes)
  • the CanActivate guard (checking route access)
  • the CanActivateChild guard (checking child route access)
  • the CanDeactivate guard (ask permission to discard unsaved changes)
  • the Resolve guard (pre-fetching route data)
  • lazy loading feature modules
  • the CanLoad guard (check before loading feature module assets)

Milestone #1: Getting Started with the Router

Let's begin with a simple version of the app that navigates between two empty views.

App in action

Set the <base href>

The Router uses the browser's history.pushState for navigation. Thanks to pushState, we can make our in-app URL paths look the way we want them to look, e.g. localhost:3000/crisis-center. Our in-app URLs can be indistinguishable from server URLs.

Modern HTML 5 browsers were the first to support pushState which is why many people refer to these URLs as "HTML 5 style" URLs.

We must add a <base href> element tag to the index.html to make pushState routing work. The browser also needs the base href value to prefix relative URLs when downloading and linking to css files, scripts, and images.

Add the base element just after the <head> tag. If the app folder is the application root, as it is for our application, set the href value in index.html exactly as shown here.

index.html (base-href)

<base href="/">

HTML 5 style navigation is the Router default. Learn why "HTML 5" style is preferred, how to adjust its behavior, and how to switch to the older hash (#) style if necessary in the Browser URL Styles appendix below.

Live example note

We have to get tricky when we run the live example because the host service sets the application base address dynamically. That's why we replace the <base href...> with a script that writes a <base> tag on the fly to match.

<script>document.write('<base href="' + document.location + '" />');</script>

We should only need this trick for the live example, not production code.

Configure the routes for the Router

We begin by importing some symbols from the router library.

The Router is in its own @angular/router package. It's not part of the Angular 2 core. The router is an optional service because not all applications need routing and, depending on your requirements, you may need a different routing library.

We teach our router how to navigate by configuring it with routes. We recommend creating a separate app.routing.ts file dedicated to this purpose.

Here is our first configuration. We pass the array of routes to the RouterModule.forRoot method which returns a module containing the configured Router service provider ... and some other, unseen providers that the routing library requires. We export this as the routing token.

app/app.routing.ts (excerpt)

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { CrisisListComponent }  from './crisis-list.component';
import { HeroListComponent }    from './hero-list.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes', component: HeroListComponent }
];

export const appRoutingProviders: any[] = [

];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

We also export an empty appRoutingProviders array so we can simplify registration of router dependencies later in app.module.ts. We don't have any providers to register right now. But we will.

Define routes

A router must be configured with a list of route definitions.

Our first configuration defines an array of two routes with simple paths leading to the CrisisListComponent and HeroListComponent components.

Each definition translates to a Route object which has a path, the URL path segment for this route, and a component, the component associated with this route.

The router draws upon its registry of such route definitions when the browser URL changes or when our code tells the router to navigate along a route path.

In plain English, we might say of the first route:

  • When the browser's location URL changes to match the path segment /crisis-center, create or retrieve an instance of the CrisisListComponent and display its view.

  • When the application requests navigation to the path /crisis-center, create or retrieve an instance of the CrisisListComponent, display its view, and update the browser's address location and history with the URL for that path.

Learn about providers in the Dependency Injection chapter.

Register routing in the AppModule

Our app launches from the app.module.ts file in the /app folder.

We import the routing token we exported from the app.routing.ts file and add it to the imports array.

We import our CrisisListComponent and HeroListComponent components and add them to our declarations so they will be registered within our AppModule.

We also import the appRoutingProviders array and add it to the providers array.

app/app.module.ts (excerpt)

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }         from './app.component';
import { routing,
         appRoutingProviders }  from './app.routing';

import { HeroListComponent }    from './hero-list.component';
import { CrisisListComponent }  from './crisis-list.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent
  ],
  providers: [
    appRoutingProviders
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}

Providing the router module in our AppModule makes the Router available everywhere in our application.

The AppComponent shell

The root AppComponent is the application shell. It has a title at the top, a navigation bar with two links, and a Router Outlet at the bottom where the router swaps views on and off the page. Here's what we mean:

Shell

The corresponding component template looks like this:

template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

RouterOutlet

RouterOutlet is a component from the router library. The router displays views within the bounds of the <router-outlet> tags.

A template may hold exactly one unnamed <router-outlet>. The router supports multiple named outlets, a feature we'll cover in future.

RouterLink binding

Above the outlet, within the anchor tags, we see Property Bindings to the RouterLink directive that look like routerLink="...". We use the RouterLink from the router library.

The links in this example each have a string path, the path of a route that we configured earlier. We don't have route parameters yet.

We can also add more contextual information to our RouterLink by providing query string parameters or a URL fragment for jumping to different areas on our page. Query string parameters are provided through the [queryParams] binding which takes an object (e.g. { name: 'value' }), while the URL fragment takes a single value bound to the [fragment] input binding.

Learn about the how we can also use the link parameters array in the appendix below.

RouterLinkActive binding

On each anchor tag, we also see Property Bindings to the RouterLinkActive directive that look like routerLinkActive="...".

The template expression to the right of the equals (=) contains our space-delimited string of CSS classes. We can also bind to the RouterLinkActive directive using an array of classes such as [routerLinkActive]="['...']".

The RouterLinkActive directive toggles css classes for active RouterLinks based on the current RouterState. This cascades down through each level in our route tree, so parent and child router links can be active at the same time. To override this behavior, we can bind to the [routerLinkActiveOptions] input binding with the { exact: true } expression. By using { exact: true }, a given RouterLink will only be active if its URL is an exact match to the current URL.

Router Directives

RouterLink, RouterLinkActive and RouterOutlet are directives provided by the Angular RouterModule package. They are readily available for us to use in our template.

The current state of app.component.ts looks like this:

app/app.component.ts (excerpt)

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Angular Router</h1>
    <nav>
      <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AppComponent { }

"Getting Started" wrap-up

We've got a very basic, navigating app, one that can switch between two views when the user clicks a link.

We've learned how to

  • load the router library
  • add a nav bar to the shell template with anchor tags, routerLink and routerLinkActive directives
  • add a router-outlet to the shell template where views will be displayed
  • configure the router module with RouterModule.forRoot
  • set the router to compose "HTML 5" browser URLs.

The rest of the starter app is mundane, with little interest from a router perspective. Here are the details for readers inclined to build the sample through to this milestone.

Our starter app's structure looks like this:

router-sample
app
app.component.ts
app.module.ts
app.routing.ts
crisis-list.component.ts
hero-list.component.ts
main.ts
node_modules ...
typings ...
index.html
package.json
styles.css
tsconfig.json
typings.json

Here are the files discussed in this milestone

app.component.ts
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 { }
app.module.ts
import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }         from './app.component';
import { routing,
         appRoutingProviders }  from './app.routing';

import { HeroListComponent }    from './hero-list.component';
import { CrisisListComponent }  from './crisis-list.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent
  ],
  providers: [
    appRoutingProviders
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}
app.routing.ts
import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { CrisisListComponent }  from './crisis-list.component';
import { HeroListComponent }    from './hero-list.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes', component: HeroListComponent }
];

export const appRoutingProviders: any[] = [

];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
hero-list.component.ts
import { Component } from '@angular/core';

@Component({
  template: `
    <h2>HEROES</h2>
    <p>Get your heroes here</p>`
})
export class HeroListComponent { }
crisis-list.component.ts
import { Component } from '@angular/core';

@Component({
  template: `
    <h2>CRISIS CENTER</h2>
    <p>Get your crisis here</p>`
})
export class CrisisListComponent { }
index.html
<html>
  <head>
    <!-- Set the base href -->
    <base href="/">
    <title>Router Sample</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">

    <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>

    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('app')
            .catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>loading...</my-app>
  </body>

</html>

Milestone #2: The Heroes Feature

We've seen how to navigate using the RouterLink directive.

Now we'll learn some new tricks such as how to

  • organize our app and routes into feature areas using modules
  • navigate imperatively from one component to another
  • pass required and optional information in route parameters

To demonstrate, we'll build out the Heroes feature.

The Heroes "feature area"

A typical application has multiple feature areas, each an island of functionality with its own workflow(s), dedicated to a particular business purpose.

We could continue to add files to the app/ folder. That's unrealistic and ultimately not maintainable. We think it's better to put each feature area in its own folder.

Our first step is to create a separate app/heroes/ folder and add Hero Management feature files there.

We won't be creative about it. Our example is pretty much a copy of the code and capabilities in the "Tutorial: Tour of Heroes".

Here's how the user will experience this version of the app

App in action

Add Heroes functionality

We want to break our app out into different feature modules that we then import into our main module so it can make use of them. First, we'll create a heroes.module.ts in our heroes folder.

We delete the placeholder hero-list.component.ts that's in the app/ folder.

We create a new hero-list.component.ts in the app/heroes/ folder and copy over the contents of the final heroes.component.ts from the tutorial. We copy the hero-detail.component.ts and the hero.service.ts files into the heroes/ folder.

We provide the HeroService in the providers array of our Heroes module so its available to all components within our module.

Our Heroes module is ready for routing.

app/heroes/heroes.module.ts (excerpt)

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [
    HeroService
  ]
})
export class HeroesModule {}

When we're done organizing, we have four Hero Management files:

app/heroes
hero-detail.component.ts
hero-list.component.ts
hero.service.ts
heroes.module.ts

Now it's time for some surgery to bring these files and the rest of the app into alignment with our application router.

Hero feature routing requirements

The new Heroes feature has two interacting components, the list and the detail. The list view is self-sufficient; we navigate to it, it gets a list of heroes and displays them. It doesn't need any outside information.

The detail view is different. It displays a particular hero. It can't know which hero on its own. That information must come from outside.

In our example, when the user selects a hero from the list, we navigate to the detail view to show that hero. We'll tell the detail view which hero to display by including the selected hero's id in the route URL.

Hero feature route configuration

We recommend giving each feature area its own route configuration file.

Create a new heroes.routing.ts in the heroes folder like this:

app/heroes/heroes.routing.ts (excerpt)

import { ModuleWithProviders }  from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

export const heroesRouting: ModuleWithProviders = RouterModule.forChild(heroesRoutes);

We use the same techniques we learned for app.routing.ts.

We import the two components from their new locations in the app/heroes/ folder, define the two hero routes. and add export our heroesRouting that returns configured RouterModule for our feature module.

Now that we have routes for our Heroes module, we'll need to register them with the Router. We'll import the RouterModule like we did in the app.routing.ts, but there is a slight difference here. In our app.routing.ts, we used the static forRoot method to register our routes and application level service providers. In a feature module we use static forChild method.

The RouterModule.forRoot should only be provided for the AppModule. Since we are in a feature module, we'll use RouterModule.forChild method to only register additional routes.

We import our heroesRouting token from heroes.routing.ts into our Heroes module and register the routing.

app/heroes/heroes.module.ts (heroes routing)

import { heroesRouting } from './heroes.routing';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    heroesRouting
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [
    HeroService
  ]
})

Route definition with a parameter

The route to HeroDetailComponent has a twist.

app/heroes/heroes.routing.ts (excerpt)

{ path: 'hero/:id', component: HeroDetailComponent }

Notice the :id token in the path. That creates a slot in the path for a Route Parameter. In this case, we're expecting the router to insert the id of a hero into that slot.

If we tell the router to navigate to the detail component and display "Magneta", we expect hero id (15) to appear in the browser URL like this:

localhost:3000/hero/15

If a user enters that URL into the browser address bar, the router should recognize the pattern and go to the same "Magneta" detail view.

Route parameter: Required or optional?

Embedding the route parameter token, :id, in the route definition path is a good choice for our scenario because the id is required by the HeroDetailComponent and because the value 15 in the path clearly distinguishes the route to "Magneta" from a route for some other hero.

An optional-route-parameter might be a better choice if we were passing an optional value to HeroDetailComponent.

Navigate to hero detail imperatively

We won't navigate to the detail component by clicking a link so we won't be adding a new RouterLink anchor tag to the shell.

Instead, when the user clicks a hero in the list, we'll command the router to navigate to the hero detail view for the selected hero.

We'll adjust the HeroListComponent to implement these tasks, beginning with its constructor which acquires the router service and the HeroService by dependency injection:

app/heroes/hero-list.component.ts (constructor)

constructor(
  private router: Router,
  private service: HeroService) { }

We make a few changes to the template:

template: `
  <h2>HEROES</h2>
  <ul class="items">
    <li *ngFor="let hero of heroes"
      (click)="onSelect(hero)">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </li>
  </ul>
`

The template defines an *ngFor repeater such as we've seen before. There's a (click) EventBinding to the component's onSelect method which we implement as follows:

app/heroes/hero-list.component.ts (select)

  onSelect(hero: Hero) {
    this.router.navigate(['/hero', hero.id]);
  }

It calls the router's navigate method with a Link Parameters Array. We can use this same syntax with a RouterLink if we want to use it in HTML rather than code.

Setting the route parameters in the list view

We're navigating to the HeroDetailComponent where we expect to see the details of the selected hero. We'll need two pieces of information: the destination and the hero's id.

Accordingly, the link parameters array has two items: the path of the destination route and a route parameter that specifies the id of the selected hero.

app/heroes/hero-list.component.ts (link-parameters-array)

['/hero', hero.id] // { 15 }

The router composes the appropriate two-part destination URL from this array:

localhost:3000/hero/15

Getting the route parameter in the details view

How does the target HeroDetailComponent learn about that id? Certainly not by analyzing the URL! That's the router's job.

The router extracts the route parameter (id:15) from the URL and supplies it to the HeroDetailComponent via the ActivatedRoute service.

ActivatedRoute: the one-stop-shop for route information

Each route contains information about its path, data parameters, URL segment and much more. All of this information is available in an injected service provided by the router called the ActivatedRoute.

The ActivatedRoute contains all the information you need from the current route component as well as ways to get information about other activated routes in the RouterState.

url: An Observable of the route path(s). The value is provided as an array of strings for each part of the route path.

data: An Observable that contains the data object provided for the route. Also contains any resolved values from the resolve guard.

params: An Observable that contains the required and optional parameters specific to the route.

queryParams: An Observable that contains the query parameters available to all routes.

fragment: An Observable of the URL fragment available to all routes.

outlet: The name of the RouterOutlet used to render the route. For an unnamed outlet, the outlet name is primary.

routeConfig: The route configuration used for the route that contains the origin path.

parent: an ActivatedRoute that contains the information from the parent route when using child routes.

firstChild: contains the first ActivatedRoute in the list of child routes.

children: contains all the child routes activated under the current route.

We import the Router, ActivatedRoute, and Params tokens from the router package.

app/heroes/hero-detail.component.ts (activated route)

import { Router, ActivatedRoute, Params } from '@angular/router';

As usual, we write a constructor that asks Angular to inject services that the component requires and reference them as private variables.

app/heroes/hero-detail.component.ts (constructor)

constructor(
  private route: ActivatedRoute,
  private router: Router,
  private service: HeroService) {}

Later, in the ngOnInit method, we use the ActivatedRoute service to retrieve the parameters for our route. Since our parameters are provided as an Observable, we use the forEach method to retrieve them for the id parameter by name and tell the HeroService to fetch the hero with that id.

app/heroes/hero-detail.component.ts (ngOnInit)

ngOnInit() {
  this.route.params.forEach((params: Params) => {
     let id = +params['id']; // (+) converts string 'id' to a number
     this.service.getHero(id).then(hero => this.hero = hero);
   });
}

Angular calls the ngOnInit method shortly after creating an instance of the HeroDetailComponent.

We put the data access logic in the ngOnInit method rather than inside the constructor to improve the component's testability. We explore this point in greater detail in the OnInit appendix below.

Learn about the ngOnInit method in the Lifecycle Hooks chapter.

Observable params and component re-use

In this example, we retrieve the route params from an Observable. That implies that the route params can change during the lifetime of this component.

They might. By default, the router reuses a component instance when it re-navigates to the same component type without visiting a different component first. The parameters can change between each re-use.

Suppose a parent component navigation bar had "forward" and "back" buttons that scrolled through the list of heroes. Each click navigated imperatively to the HeroDetailComponent with the next or previous id.

We don't want the router to remove the current HeroDetailComponent instance from the DOM only to re-create it for the next id. That could be visibly jarring. Better to simply re-use the same component instance and update the parameter.

But ngOnInit is only called once per instantiation. We need a way to detect when the route parameters change from within the same instance. The observable params property handles that beautifully.

Snapshot: the no-observable alternative

This application won't reuse the HeroDetailComponent. We always return to the hero list to select another hero to view. There's no way to navigate from hero detail to hero detail without visiting the list component in between. That means we get a new HeroDetailComponent instance every time.

Suppose we know for certain that HeroDetailComponent will never, never, ever be re-used. We'll always re-create the component each time we navigate to it.

The router offers a Snapshot alternative that gives us the initial value of the route parameters. We don't need to subscribe or unsubscribe. It's much simpler to write and read:

app/heroes/hero-detail.component.ts (ngOnInit snapshot)

ngOnInit() {
  // (+) converts string 'id' to a number
  let id = +this.route.snapshot.params['id'];
  this.service.getHero(id).then(hero => this.hero = hero);
}

Remember: we only get the initial value of the parameters with this technique. Stick with the observable params approach if there's even a chance that we might navigate to this component multiple times in a row. We are leaving the observable params strategy in place just in case.

Navigating back to the list component

The HeroDetailComponent has a "Back" button wired to its gotoHeroes method that navigates imperatively back to the HeroListComponent.

The router navigate method takes the same one-item link parameters array that we can bind to a [routerLink] directive. It holds the path to the HeroListComponent:

app/heroes/hero-detail.component.ts (excerpt)

gotoHeroes() { this.router.navigate(['/heroes']); }

Route Parameters

We use route parameters to specify a required parameter value within the route URL as we do when navigating to the HeroDetailComponent in order to view-and-edit the hero with id:15.

localhost:3000/hero/15

Sometimes we wish to add optional information to a route request. For example, the HeroListComponent doesn't need help to display a list of heroes. But it might be nice if the previously-viewed hero were pre-selected when returning from the HeroDetailComponent.

Selected hero

That becomes possible if we can include hero Magneta's id in the URL when we return from the HeroDetailComponent, a scenario we'll pursue in a moment.

Optional information takes other forms. Search criteria are often loosely structured, e.g., name='wind*'. Multiple values are common — after='12/31/2015' & before='1/1/2017' — in no particular order — before='1/1/2017' & after='12/31/2015' — in a variety of formats — during='currentYear' .

These kinds of parameters don't fit easily in a URL path. Even if we could define a suitable URL token scheme, doing so greatly complicates the pattern matching required to translate an incoming URL to a named route.

Optional parameters are the ideal vehicle for conveying arbitrarily complex information during navigation. Optional parameters aren't involved in pattern matching and affords enormous flexibility of expression.

The Router supports navigation with optional parameters as well as required route parameters. We define optional parameters in an object after we define our required route parameters.

Route Parameters: Required or Optional?

There is no hard-and-fast rule. In general,

prefer a required route parameter when

  • the value is required.
  • the value is necessary to distinguish one route path from another.

prefer an optional parameter when

  • the value is optional, complex, and/or multi-variate.

Route parameter

When navigating to the HeroDetailComponent we specified the required id of the hero-to-edit in the route parameter and made it the second item of the link parameters array.

app/heroes/hero-list.component.ts (link-parameters-array)

['/hero', hero.id] // { 15 }

The router embedded the id value in the navigation URL because we had defined it as a route parameter with an :id placeholder token in the route path:

app/heroes/heroes.routing.ts (hero-detail-route)

{ path: 'hero/:id', component: HeroDetailComponent }

When the user clicks the back button, the HeroDetailComponent constructs another link parameters array which it uses to navigate back to the HeroListComponent.

app/heroes/hero-detail.component.ts (gotoHeroes)

gotoHeroes() { this.router.navigate(['/heroes']); }

This array lacks a route parameter because we had no reason to send information to the HeroListComponent.

Now we have a reason. We'd like to send the id of the current hero with the navigation request so that the HeroListComponent can highlight that hero in its list. This is a nice-to-have feature; the list will display perfectly well without it.

We do that with an object that contains an optional id parameter. For demonstration purposes, we also defined a junk parameter (foo) that the HeroListComponent should ignore. Here's the revised navigation statement:

app/heroes/hero-detail.component.ts (go to heroes)

gotoHeroes() {
  let heroId = this.hero ? this.hero.id : null;
  // Pass along the hero id if available
  // so that the HeroList component can select that hero.
  this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
}

The application still works. Clicking "back" returns to the hero list view.

Look at the browser address bar.

pop out the window

When running in plunker, pop out the preview window by clicking the blue 'X' button in the upper right corner.

It should look something like this, depending on where you run it:

localhost:3000/heroes;id=15;foo=foo

The id value appears in the URL as (;id=15;foo=foo), not in the URL path. The path for the "Heroes" route doesn't have an :id token.

The optional route parameters are not separated by "?" and "&" as they would be in the URL query string. They are separated by semicolons ";" This is matrix URL notation — something we may not have seen before.

Matrix URL notation is an idea first floated in a 1996 proposal by the founder of the web, Tim Berners-Lee.

Although matrix notation never made it into the HTML standard, it is legal and it became popular among browser routing systems as a way to isolate parameters belonging to parent and child routes. The Router is such a system and provides support for the matrix notation across browsers.

The syntax may seem strange to us but users are unlikely to notice or care as long as the URL can be emailed and pasted into a browser address bar as this one can.

Route parameters in the ActivatedRoute service

The list of heroes is unchanged. No hero row is highlighted.

The live example does highlight the selected row because it demonstrates the final state of the application which includes the steps we're about to cover. At the moment we're describing the state of affairs prior to those steps.

The HeroListComponent isn't expecting any parameters at all and wouldn't know what to do with them. Let's change that.

Previously, when navigating from the HeroListComponent to the HeroDetailComponent, we subscribed to the route params Observable and made it available to the HeroDetailComponent in the ActivatedRoute service. We injected that service in the constructor of the HeroDetailComponent.

This time we'll be navigating in the opposite direction, from the HeroDetailComponent to the HeroListComponent.

First we extend the router import statement to include the ActivatedRoute service symbol;

app/heroes/hero-list.component.ts (import)

import { Router, ActivatedRoute, Params } from '@angular/router';

Then we use the ActivatedRoute to access the params Observable so we can subscribe and extract the id parameter as the selectedId:

app/heroes/hero-list.component.ts (constructor)

  private selectedId: number;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit() {
    this.route.params.forEach((params: Params) => {
        this.selectedId = +params['id'];
        this.service.getHeroes()
          .then(heroes => this.heroes = heroes);
      });
  }

All route/query parameters are strings. The (+) in front of the params['id'] expression is a JavaScript trick to convert the string to an integer.

We add an isSelected method that returns true when a hero's id matches the selected id.

app/heroes/hero-list.component.ts (isSelected)

isSelected(hero: Hero) { return hero.id === this.selectedId; }

Finally, we update our template with a Class Binding to that isSelected method. The binding adds the selected CSS class when the method returns true and removes it when false. Look for it within the repeated <li> tag as shown here:

app/heroes/hero-list.component.ts (template)

template: `
  <h2>HEROES</h2>
  <ul class="items">
    <li *ngFor="let hero of heroes"
      [class.selected]="isSelected(hero)"
      (click)="onSelect(hero)">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </li>
  </ul>
`

When the user navigates from the heroes list to the "Magneta" hero and back, "Magneta" appears selected:

Selected List

The optional foo route parameter is harmless and continues to be ignored.

Adding animations to the route component

Our heroes feature module is almost complete, but what is a feature without some smooth transitions? We already know that Angular supports animations and we want to take advantage of them by adding some animation to our Hero Detail component.

First, we'll start by importing our animation functions that build our animation triggers, control state and manage transitions between states. We'll use these functions to add transitions to our route component as it moves between states our application view. We'll also import the HostBinding decorator for binding to our route component.

app/heroes/hero-detail.component.ts (animation imports)

import { Component, OnInit, HostBinding,
         trigger, transition, animate,
         style, state } from '@angular/core';

Next, we'll use a host binding for route animations named @routeAnimation. There is nothing special about the choice of the binding name, but since we are controlling route animation, we'll go with routeAnimation. The binding value is set to true because we only care about the * and void states which are entering and leaving animation states.

We'll also add some display and positioning bindings for styling.

app/heroes/hero-detail.component.ts (route animation binding)

export class HeroDetailComponent implements OnInit {
  @HostBinding('@routeAnimation') get routeAnimation() {
    return true;
  }

  @HostBinding('style.display') get display() {
    return 'block';
  }

  @HostBinding('style.position') get position() {
    return 'absolute';
  }

  hero: Hero;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private service: HeroService) {}

  ngOnInit() {
    this.route.params.forEach((params: Params) => {
       let id = +params['id']; // (+) converts string 'id' to a number
       this.service.getHero(id).then(hero => this.hero = hero);
     });
  }

  gotoHeroes() {
    let heroId = this.hero ? this.hero.id : null;
    // Pass along the hero id if available
    // so that the HeroList component can select that hero.
    this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
  }
}

Now we can build our animation trigger, which we'll call routeAnimation to match the binding we previously setup. We'll use the wildcard state that matches any animation state our route component is in, along with two transitions. One transition animates the component as it enters the application view (void => *), while the other animates the component as it leaves the application view (* => void).

We could add different transitions to different route components depending on our needs. We'll just animate our HeroDetailComponent for this milestone.

Using route animations on individual components is something we don't want to do throughout our entire application. It would be better to animate routes based on route paths, a topic to cover in a future update to this chapter.

Our route component animation looks as such:

app/heroes/hero-detail.component.ts (route animation)

@Component({
  template: `
  <h2>HEROES</h2>
  <div *ngIf="hero">
    <h3>"{{hero.name}}"</h3>
    <div>
      <label>Id: </label>{{hero.id}}</div>
    <div>
      <label>Name: </label>
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </div>
    <p>
      <button (click)="gotoHeroes()">Back</button>
    </p>
  </div>
  `,
  animations: [
    trigger('routeAnimation', [
      state('*',
        style({
          opacity: 1,
          transform: 'translateX(0)'
        })
      ),
      transition('void => *', [
        style({
          opacity: 0,
          transform: 'translateX(-100%)'
        }),
        animate('0.2s ease-in')
      ]),
      transition('* => void', [
        animate('0.5s ease-out', style({
          opacity: 0,
          transform: 'translateY(100%)'
        }))
      ])
    ])
  ]
})
export class HeroDetailComponent implements OnInit {

Simply stated, our HeroDetailComponent will ease in from the left when routed to and will slide down when navigating away. We could add more complex animations here, but we'll leave our HeroDetailComponent as is for now.

Import hero module into AppModule

Our heroes feature module is ready, but application doesn't know about our heroes module yet. We'll need to import it into the AppModule we defined in app.module.ts.

Update app.module.ts as follows:

app/app.module.ts (heroes module import)

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }         from './app.component';
import { routing,
         appRoutingProviders }  from './app.routing';

import { HeroesModule }         from './heroes/heroes.module';

import { CrisisListComponent }  from './crisis-list.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing,
    HeroesModule
  ],
  declarations: [
    AppComponent,
    CrisisListComponent
  ],
  providers: [
    appRoutingProviders
  ],
  bootstrap: [ AppComponent ]
})

We imported the HeroesModule and added it to our AppModule's imports.

We removed the HeroListComponent from the AppModule's declarations because its being provided by the HeroesModule now. This is important because their can be only one owner for a declared component. In our case, the Heroes module is the owner of the Heroes components and is making them available to the AppModule.

Routes provided by feature modules will be combined together into their imported module's routes by the router. This allows us to continue defining our feature module routes without modifying our main route configuration.

As a result, the AppModule no longer has specific knowledge of the hero feature, its components, or its route details. We can evolve the hero feature with more components and different routes. That's a key benefit of creating a separate module for each feature area.

Since our Heroes routes are defined within our feature module, we can also remove our initial heroes route from the app.routing.ts.

app/app.routing.ts (v2)

import { ModuleWithProviders }  from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { CrisisCenterComponent } from './crisis-center/crisis-center.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisCenterComponent }
];

export const appRoutingProviders: any[] = [

];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

Heroes App Wrap-up

We've reached the second milestone in our router education.

We've learned how to

  • organize our app into feature areas
  • navigate imperatively from one component to another
  • pass information along in route parameters and subscribe to them in our component
  • import our feature area NgModule into our AppModule
  • apply animations to our route component

After these changes, the folder structure looks like this:

router-sample
app
heroes
hero-detail.component.ts
hero-list.component.ts
hero.service.ts
heroes.module.ts
heroes.routing.ts
app.component.ts
app.module.ts
app.routing.ts
crisis-list.component.ts
main.ts
node_modules ...
typings ...
index.html
package.json
styles.css
tsconfig.json
typings.json

The Heroes App code

Here are the relevant files for this version of the sample application.

app.component.ts
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 { }
app.module.ts
import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }         from './app.component';
import { routing,
         appRoutingProviders }  from './app.routing';

import { HeroesModule }         from './heroes/heroes.module';

import { CrisisListComponent }  from './crisis-list.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing,
    HeroesModule
  ],
  declarations: [
    AppComponent,
    CrisisListComponent
  ],
  providers: [
    appRoutingProviders
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}
app.routing.ts
import { ModuleWithProviders }  from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { CrisisCenterComponent } from './crisis-center/crisis-center.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisCenterComponent }
];

export const appRoutingProviders: any[] = [

];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
hero-list.component.ts
// TODO SOMEDAY: Feature Componetized like CrisisCenter
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';

import { Hero, HeroService }  from './hero.service';

@Component({
  template: `
    <h2>HEROES</h2>
    <ul class="items">
      <li *ngFor="let hero of heroes"
        [class.selected]="isSelected(hero)"
        (click)="onSelect(hero)">
        <span class="badge">{{hero.id}}</span> {{hero.name}}
      </li>
    </ul>
  `
})
export class HeroListComponent implements OnInit {
  heroes: Hero[];

  private selectedId: number;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute,
    private router: Router
  ) {}

  ngOnInit() {
    this.route.params.forEach((params: Params) => {
        this.selectedId = +params['id'];
        this.service.getHeroes()
          .then(heroes => this.heroes = heroes);
      });
  }

  isSelected(hero: Hero) { return hero.id === this.selectedId; }

  onSelect(hero: Hero) {
    this.router.navigate(['/hero', hero.id]);
  }

}
hero-detail.component.ts
import { Component, OnInit, HostBinding,
         trigger, transition, animate,
         style, state } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';

import { Hero, HeroService }  from './hero.service';

@Component({
  template: `
  <h2>HEROES</h2>
  <div *ngIf="hero">
    <h3>"{{hero.name}}"</h3>
    <div>
      <label>Id: </label>{{hero.id}}</div>
    <div>
      <label>Name: </label>
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </div>
    <p>
      <button (click)="gotoHeroes()">Back</button>
    </p>
  </div>
  `,
  animations: [
    trigger('routeAnimation', [
      state('*',
        style({
          opacity: 1,
          transform: 'translateX(0)'
        })
      ),
      transition('void => *', [
        style({
          opacity: 0,
          transform: 'translateX(-100%)'
        }),
        animate('0.2s ease-in')
      ]),
      transition('* => void', [
        animate('0.5s ease-out', style({
          opacity: 0,
          transform: 'translateY(100%)'
        }))
      ])
    ])
  ]
})
export class HeroDetailComponent implements OnInit {
  @HostBinding('@routeAnimation') get routeAnimation() {
    return true;
  }

  @HostBinding('style.display') get display() {
    return 'block';
  }

  @HostBinding('style.position') get position() {
    return 'absolute';
  }

  hero: Hero;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private service: HeroService) {}

  ngOnInit() {
    this.route.params.forEach((params: Params) => {
       let id = +params['id']; // (+) converts string 'id' to a number
       this.service.getHero(id).then(hero => this.hero = hero);
     });
  }

  gotoHeroes() {
    let heroId = this.hero ? this.hero.id : null;
    // Pass along the hero id if available
    // so that the HeroList component can select that hero.
    this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
  }
}
hero.service.ts
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));
  }
}
heroes.module.ts
import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

import { HeroService } from './hero.service';

import { heroesRouting } from './heroes.routing';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    heroesRouting
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [
    HeroService
  ]
})
export class HeroesModule {}
heroes.routing.ts
import { ModuleWithProviders }  from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

export const heroesRouting: ModuleWithProviders = RouterModule.forChild(heroesRoutes);

Milestone #3: The Crisis Center

The Crisis Center is a fake view at the moment. Time to make it useful.

The new Crisis Center begins as a virtual copy of the Heroes module. We create a new app/crisis-center folder, copy the Hero files, and change every mention of "hero" to "crisis".

A Crisis has an id and name, just like a Hero The new CrisisListComponent displays lists of crises. When the user selects a crisis, the app navigates to the CrisisDetailComponent for display and editing of the crisis name.

Voilà, another feature module!

There's no point to this exercise unless we can learn something. We do have new ideas and techniques in mind:

  • We'd like our route URLs to branch in to child route trees that reflect the component trees in our feature areas.

  • The application should navigate to the Crisis Center by default.

  • The router should prevent navigation away from the detail view while there are pending changes.

  • The user should be able to cancel unwanted changes.

  • The router should block access to certain features until the user logs-in.

  • Our CrisisService is only needed within the Crisis Center module. We should limit access to it to that module.

  • Changes to a feature module such as Crisis Center shouldn't provoke changes to the AppModule or any other feature's component. We need to separate our concerns.

We'll address all of these issues in the Crisis Center starting with the introduction of child routes

We'll leave Heroes in its less-than-perfect state to serve as a contrast with what we believe to be a superior Crisis Center design.

A Crisis Center with child routes

We'll organize the Crisis Center to conform to the following recommended pattern for Angular applications.

  • each feature area in its own folder within a defined module
  • each area with its own area root component
  • each area root component with its own router-outlet and child routes
  • area routes rarely (if ever) cross

If we had many feature areas, their component trees might look like this:

Component Tree

Child Routing Component

Add the following crisis-center.component.ts to the crisis-center folder:

app/crisis-center/crisis-center.component.ts (minus imports)

@Component({
  template:  `
    <h2>CRISIS CENTER</h2>
    <router-outlet></router-outlet>
  `
})
export class CrisisCenterComponent { }

The CrisisCenterComponent is much like the AppComponent shell.

  • It is the root of the Crisis Center area just as AppComponent is the root of the entire application.

  • It is a shell for the crisis management feature area just as the AppComponent is a shell to manage the high-level workflow.

  • It is dead simple — simpler even than the AppComponent template. It has no content, no links, just a <router-outlet> for the Crisis Center child views.

Unlike AppComponent (and most other components), it lacks a selector. It doesn't need one. We don't embed this component in a parent template. We navigate to it from the outside, via the router.

We can give it a selector. There's no harm in it. Our point is that we don't need one because we only navigate to it.

Service isolation

The CrisisService is neither needed nor wanted outside the Crisis Center domain. Instead of registering it with the AppModule's providers — which makes it visible everywhere — we register the CrisisService in the CrisisCenterModule providers array.

providers: [
  CrisisService
]

This limits the scope of the CrisisService to the Crisis Center routes. No module outside of the Crisis Center can access it.

There's a practical benefit to restricting its scope in this way.

First we can evolve the service independently of the rest of the application without fear of breaking what should be unrelated modules.

Second, we can delay loading this service into memory until we need it. We can remove it from the application launch bundle, reducing the size of the initial payload and improving performance. We can load it optionally, asynchronously with the other Crisis Center components if and when the user begins that workflow.

Child Route Configuration

The CrisisCenterComponent is a Routing Component like the AppComponent. It has its own RouterOutlet and its own child routes.

Add the following crisis-center-home.component.ts to the crisis-center folder.

app/crisis-center/crisis-center-home.component.ts (minus imports)

@Component({
  template: `
    <p>Welcome to the Crisis Center</p>
  `
})
export class CrisisCenterHomeComponent { }

We create a crisis-center.routing.ts file as we did the heroes.routing.ts file. But this time we define child routes within the parent crisis-center route.

app/crisis-center/crisis-center.routing.ts (Routes)

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);

Notice that the parent crisis-center route has a children property with a single route containing our CrisisListComponent. The CrisisListComponent route also has a children array with two routes.

These two routes navigate to the two Crisis Center child components, CrisisCenterHomeComponent and CrisisDetailComponent.

There are some important differences in the treatment of these routes.

The router displays the components of these routes in the RouterOutlet of the CrisisCenterComponent, not in the RouterOutlet of the AppComponent shell.

The CrisisListComponent contains the crisis list and a RouterOutlet to display the Crisis Center Home and Crisis Detail route components.

The Crisis Detail route is a child of the Crisis List. Since the router reuses components by default, the Crisis Detail component will be re-used as we select different crises.

In contrast, back in the Hero Detail route, the component was recreated each time we selected a different hero.

At the top level, paths that begin with / refer to the root of the application. But these are child routes. They extend the path of the parent route. With each step down the route tree, we add a slash followed by the route path (unless the route path is empty).

For example, the parent path to the CrisisCenterComponent is /crisis-center The router appends these child paths to the parent path to theCrisisCenterComponent(/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 origin, is

localhost:3000/crisis-center/2

Here's the complete crisis-center.routing.ts file with its imports.

app/crisis-center/crisis-center.routing.ts (excerpt)

import { ModuleWithProviders }   from '@angular/core';
import { Routes, RouterModule }  from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);

Import crisis center module into the AppModule routes

As with the Heroes module, we must import the Crisis Center module into the AppModule:

app/app.module.ts (Crisis Center Module)

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { AppComponent }           from './app.component';
import { routing,
         appRoutingProviders }    from './app.routing';

import { HeroesModule }           from './heroes/heroes.module';
import { CrisisCenterModule }     from './crisis-center/crisis-center.module';

import { DialogService }          from './dialog.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    routing,
    HeroesModule,
    CrisisCenterModule,
  ],
  declarations: [
    AppComponent
  ],
  providers: [
    appRoutingProviders,
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}

We also remove the initial crisis center route from our app.routing.ts. Our routes are now being provided by our HeroesModule and our CrisisCenter feature modules. We'll keep our app.routing.ts file for general routes which we'll cover later in the chapter.

app/app.routing.ts (v3)

import { ModuleWithProviders }   from '@angular/core';
import { Routes, RouterModule }  from '@angular/router';

const appRoutes: Routes = [

];

export const appRoutingProviders: any[] = [

];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

Redirecting routes

When the application launches, the initial URL in the browser bar is something like:

localhost:3000

That doesn't match any of our configured routes which means that our application won't display any component when it's launched. The user must click one of the navigation links to trigger a navigation and display something.

We prefer that the application display the list of crises as it would if the user clicked the "Crisis Center" link or pasted localhost:3000/crisis-center/ into the address bar. This is our intended default route.

The preferred solution is to add a redirect route that transparently translates from the initial relative URL ('') to the desired default path (/crisis-center):

{
  path: '',
  redirectTo: '/crisis-center',
  pathMatch: 'full'
},

A redirect route requires a pathMatch property to tell the router how to match a URL to the path of a route. In this app, the router should select the route to the CrisisListComponent when the entire URL matches '', so we set the pathMatch value to 'full'.

Technically, pathMatch = 'full' results in a route hit when the remaining, unmatched segments of the URL match ''. In our example, the redirect is at the top level of the route configuration tree so the remaining URL and the entire URL are the same thing.

The other possible pathMatch value is 'prefix' which tells the router to match the redirect route when the remaining URL begins with the redirect route's prefix path.

That's not what we want to do here. If the pathMatch value were 'prefix', every URL would match ''. We could never navigate to /crisis-center/1 because the redirect route would match first and send us to the CrisisListComponent.

We should redirect to the CrisisListComponent only when the entire (remaining) url is ''.

Learn more in Victor Savkin's blog post on redirects.

We'll discuss redirects in more detail in a future update to this chapter.

The updated route definitions look like this:

app/crisis-center/crisis-center.routing.ts (routes v2)

import { ModuleWithProviders }   from '@angular/core';
import { Routes, RouterModule }  from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);

Relative Navigation

While building out our Crisis Center feature, we've navigated to the Crisis Detail route using an absolute path that begins with a slash. This navigation starts from the top of our route configuration to find the matching path to our route.

We could continue to use absolute paths to navigate inside our Crisis Center feature, but that makes our links very rigid. If we changed our parent /crisis-center path, we would have to change our link parameters array.

We can make our links more flexible by using relative navigation with the router.

  • The full path to the route is not required.
  • Navigation within our feature area remains intact if the parent route path is changed.
  • The link parameters array only contains navigation relative to the current URL.

The link parameters array supports a directory-like syntax for relative navigation.

./ or no leading slash is relative to the current level.

../ to go up one level in the route path.

The relative navigation syntax can be used in combination with a path. If we wanted to navigate from one route path to another sibling route path we could use ../path convention to go up one level and down to the sibling route path.

In order to navigate relatively using the Router service, we use the ActivatedRoute to give the router knowledge of where we are in the RouterState, which is our tree of activated routes. We do this by adding an object as the second argument in our router.navigate method after the link parameters array specifying the relativeTo property. We set the relativeTo property to our ActivatedRoute and the router will merge our navigation information into to the current URL.

When using router's navigateByUrl method, the navigation is always absolute.

Navigate to Crisis Detail relatively

Let's update our Crisis List onSelect method to use relative navigation so we don't have to start from the top of our route configuration. We've already injected the ActivatedRoute into our constructor that we'll need for the relative navigation.

app/crisis-center/crisis-list.component.ts (constructor)

constructor(
  private service: CrisisService,
  private route: ActivatedRoute,
  private router: Router) {}

When we visit the Crisis Center, our path is /crisis-center, so we just want to add the id of the Crisis Center to our existing path. When the router navigates, it will use the current path /crisis-center, adding on our id. If our id were 1, the resulting path would be /crisis-center/1.

app/crisis-center/crisis-list.component.ts (relative navigation)

onSelect(crisis: Crisis) {
  this.selectedId = crisis.id;

  // Navigate with relative link
  this.router.navigate([crisis.id], { relativeTo: this.route });
}

We'll also update the Crisis Detail component to navigate back to our Crisis Center list. We want to go back up a level in the path, so we use to the ../ syntax. If our current id is 1, the resulting path coming from /crisis-center/1 would be /crisis-center.

app/crisis-center/crisis-detail.component.ts (relative navigation)

gotoCrises() {
  let crisisId = this.crisis ? this.crisis.id : null;
  // Pass along the crisis id if available
  // so that the CrisisListComponent can select that crisis.
  // Add a totally useless `foo` parameter for kicks.
  // Relative navigation back to the crises
  this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
}

If we are using a RouterLink to navigate instead of the Router service, we can use the same link parameters array, but we don't have to provide the object with the relativeTo property. The ActivatedRoute is implicit in the RouterLink directive.

app/crisis-center/crisis-list.component.ts (relative routerLink)

<a [routerLink]="[crisis.id]">{{ crisis.name }}</a>

Route Guards

Milestone #4: Route Guards

At the moment, any user can navigate anywhere in the application anytime.

That's not always the right thing to do.

  • Perhaps the user is not authorized to navigate to the target component.
  • Maybe the user must login (authenticate) first.
  • Maybe we should fetch some data before we display the target component.
  • We might want to save pending changes before leaving a component.
  • We might ask the user if it's OK to discard pending changes rather than save them.

We can add guards to our route configuration to handle these scenarios.

A guard's return value controls the router's behavior:

  • if it returns true, the navigation process continues
  • if it returns false, the navigation process stops and the user stays put

The guard can also tell the router to navigate elsewhere, effectively canceling the current navigation.

The guard might return its boolean answer synchronously. But in many cases, the guard can't produce an answer synchronously. The guard could ask the user a question, save changes to the server, or fetch fresh data. These are all asynchronous operations.

Accordingly, a routing guard can return an Observable<boolean> or a Promise<boolean> and the router will wait for the observable to resolve to true or false.

The router supports multiple kinds of guards:

  1. CanActivate to mediate navigation to a route.

  2. CanActivateChild to mediate navigation to a child route.

  3. CanDeactivate to mediate navigation away from the current route.

  4. Resolve to perform route data retrieval before route activation.

  5. CanLoad to mediate navigation to a feature module loaded asynchronously.

We can have multiple guards at every level of a routing hierarchy. The router checks the CanDeactivate and CanActivateChild guards first, from deepest child route to the top. Then it checks the CanActivate guards from the top down to the deepest child route. If the feature module is loaded asynchronously, the CanLoad guard is checked before the module is loaded. If any guard returns false, pending guards that have not completed will be canceled, and the entire navigation is canceled.

Let's look at some examples.

CanActivate: requiring authentication

Applications often restrict access to a feature area based on who the user is. We could permit access only to authenticated users or to users with a specific role. We might block or limit access until the user's account is activated.

The CanActivate guard is the tool to manage these navigation business rules.

Add an admin feature module

We intend to extend the Crisis Center with some new administrative features. Those features aren't defined yet. So we add a new feature module named AdminModule. We'll follow our same convention by creating an admin folder with a feature module file, route file and supporting components.

Our admin feature module file structure looks like this:

app/admin
admin-dashboard.component.ts
admin.component.ts
admin.module.ts
admin.routing.ts
manage-crises.component.ts
manage-heroes.component.ts

Our admin feature module contains our AdminComponent used for routing within our feature module, a dashboard route and two unfinished components to manage crises and heroes.

app/admin/admin-dashboard.component.ts
import { Component } from '@angular/core';

@Component({
  template:  `
    <p>Dashboard</p>
  `
})
export class AdminDashboardComponent { }
app/admin/admin.component.ts
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 {
}
app/admin/admin.module.ts
import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';

import { AdminComponent }           from './admin.component';
import { AdminDashboardComponent }  from './admin-dashboard.component';
import { ManageCrisesComponent }    from './manage-crises.component';
import { ManageHeroesComponent }    from './manage-heroes.component';

import { adminRouting } from './admin.routing';

@NgModule({
  imports: [
    CommonModule,
    adminRouting
  ],
  declarations: [
    AdminComponent,
    AdminDashboardComponent,
    ManageCrisesComponent,
    ManageHeroesComponent
  ]
})
export class AdminModule {}
app/admin/manage-crises.component.ts
import { Component } from '@angular/core';

@Component({
  template:  `
    <p>Manage your crises here</p>
  `
})
export class ManageCrisesComponent { }
app/admin/manage-heroes.component.ts
import { Component } from '@angular/core';

@Component({
  template:  `
    <p>Manage your heroes here</p>
  `
})
export class ManageHeroesComponent { }

Since our admin dashboard RouterLink is an empty path route in our AdminModule, it is considered a match to any route within our admin feature area. We only want the Dashboard link to be active when we visit that route. We've added an additional binding to our Dashboard routerLink, [routerLinkActiveOptions]="{ exact: true }" which will only mark the ./ link as active when we navigate the to /admin URL and not when we navigate to one the other child routes.

Our initial admin routing configuration:

app/admin/admin.routing.ts (admin routing)

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

export const adminRouting: ModuleWithProviders = RouterModule.forChild(adminRoutes);

Component-Less Route: grouping routes without a component

Looking at our child route under the AdminComponent, we have a route with a path and a children property but it's not using a component. We haven't made a mistake in our configuration, because we can use a component-less route.

We want to group our Crisis Center management routes under the admin path, but we don't need a component just to group those routes under an additional RouterOutlet. This also allows us to guard child routes.

Next, we'll import the AdminModule into our app.module.ts and add it to the imports array to register our admin routes.

app/app.module.ts (admin module)

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { AppComponent }           from './app.component';
import { routing,
         appRoutingProviders }    from './app.routing';

import { HeroesModule }           from './heroes/heroes.module';
import { CrisisCenterModule }     from './crisis-center/crisis-center.module';
import { AdminModule }            from './admin/admin.module';

import { DialogService }          from './dialog.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    routing,
    HeroesModule,
    CrisisCenterModule,
    AdminModule
  ],
  declarations: [
    AppComponent
  ],
  providers: [
    appRoutingProviders,
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}

And we add a link to the AppComponent shell that users can click to get to this feature.

app/app.component.ts (template)

template: `
  <h1 class="title">Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
    <a routerLink="/admin" routerLinkActive="active">Admin</a>
  </nav>
  <router-outlet></router-outlet>
`

Guard the admin feature

Currently every route within our Crisis Center is open to everyone. The new admin feature should be accessible only to authenticated users.

We could hide the link until the user logs in. But that's tricky and difficult to maintain.

Instead we'll write a CanActivate guard to redirect anonymous users to the login page when they try to reach the admin component.

This is a general purpose guard — we can imagine other features that require authenticated users — so we create an auth-guard.service.ts in the application root folder.

At the moment we're interested in seeing how guards work so our first version does nothing useful. It simply logs to console and returns true immediately, allowing navigation to proceed:

app/auth-guard.service.ts (excerpt)

import { Injectable }     from '@angular/core';
import { CanActivate }    from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate() {
    console.log('AuthGuard#canActivate called');
    return true;
  }
}

Next we open admin.routing.ts, import the AuthGuard class, and update the admin route with a CanActivate guard property that references it:

app/admin/admin.routing.ts (guarded admin route)

import { AuthGuard }                from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ],
      }
    ]
  }
];

export const adminRouting: ModuleWithProviders = RouterModule.forChild(adminRoutes);

Our admin feature is now protected by the guard, albeit protected poorly.

Teach AuthGuard to authenticate

Let's make our AuthGuard at least pretend to authenticate.

The AuthGuard should call an application service that can login a user and retain information about the current user. Here's a demo AuthService:

app/auth.service.ts (excerpt)

import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/delay';

@Injectable()
export class AuthService {
  isLoggedIn: boolean = false;

  // store the URL so we can redirect after logging in
  redirectUrl: string;

  login(): Observable<boolean> {
    return Observable.of(true).delay(1000).do(val => this.isLoggedIn = true);
  }

  logout(): void {
    this.isLoggedIn = false;
  }
}

Although it doesn't actually log in, it has what we need for this discussion. It has an isLoggedIn flag to tell us whether the user is authenticated. Its login method simulates an API call to an external service by returning an observable that resolves successfully after a short pause. The redirectUrl property will store our attempted URL so we can navigate to it after authenticating.

Let's revise our AuthGuard to call it.

app/auth-guard.service.ts (v2)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Navigate to the login page with extras
    this.router.navigate(['/login']);
    return false;
  }
}

Notice that we inject the AuthService and the Router in the constructor. We haven't provided the AuthService yet but it's good to know that we can inject helpful services into our routing guards.

This guard returns a synchronous boolean result. If the user is logged in, it returns true and the navigation continues.

The ActivatedRouteSnapshot contains the future route that will be activated and the RouterStateSnapshot contains the future RouterState of our application, should we pass through our guard check.

If the user is not logged in, we store the attempted URL the user came from using the RouterStateSnapshot.url and tell the router to navigate to a login page — a page we haven't created yet. This secondary navigation automatically cancels the current navigation; we return false just to be clear about that.

Add the LoginComponent

We need a LoginComponent for the user to log in to the app. After logging in, we'll redirect to our stored URL if available, or use the default URL. There is nothing new about this component or the way we wire it into the router configuration.

We'll register a /login route in our app.routing.ts and add the necessary providers to the appRoutingProviders array we created earlier. In our app.module.ts, we'll import the LoginComponent and add it to our AppModule declarations.

app/app.module.ts
import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }         from './app.component';
import { routing,
         appRoutingProviders }  from './app.routing';

import { HeroesModule }         from './heroes/heroes.module';
import { CrisisCenterModule }   from './crisis-center/crisis-center.module';

import { LoginComponent }       from './login.component';

import { DialogService }        from './dialog.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing,
    HeroesModule,
    CrisisCenterModule
  ],
  declarations: [
    AppComponent,
    LoginComponent
  ],
  providers: [
    appRoutingProviders,
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}
app/app.routing.ts
import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule }   from '@angular/router';

import { loginRoutes,
         authProviders }  from './login.routing';

import { CanDeactivateGuard } from './can-deactivate-guard.service';

const appRoutes: Routes = [
  ...loginRoutes
];

export const appRoutingProviders: any[] = [
  authProviders,
  CanDeactivateGuard
];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
app/login.component.ts
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();
  }
}
app/login.routing.ts
import { Routes }         from '@angular/router';
import { AuthGuard }      from './auth-guard.service';
import { AuthService }    from './auth.service';
import { LoginComponent } from './login.component';

export const loginRoutes: Routes = [
  { path: 'login', component: LoginComponent }
];

export const authProviders = [
  AuthGuard,
  AuthService
];

Guards and the service providers they require must be provided at the module-level. This allows the Router access to retrieve these services from the Injector during the navigation process. The same rule applies for feature modules loaded asynchronously.

CanActivateChild: guarding child routes

As we learned about guarding routes with CanActivate, we can also protect child routes with the CanActivateChild guard. The CanActivateChild guard works similarly to the CanActivate guard, but the difference is its run before each child route is activated. We protected our admin feature module from unauthorized access, but we could also protect child routes within our feature module.

Let's extend our AuthGuard to protect when navigating between our admin routes. First we'll open our auth-guard.service.ts and add CanActivateChild interface to our imported tokens from the router package.

Next, we'll implement the canActivateChild method with takes the same arguments as the canActivate method, an ActivatedRouteSnapshot and RouterStateSnapshot. The canActivateChild behaves the same way the other guards do, returning an Observable<boolean> or Promise<boolean> for async checks and boolean for sync checks. We'll return a boolean

app/auth-guard.service.ts (excerpt)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

}

We add the same AuthGuard to our component-less admin route to protect all other child routes at one time instead of adding the AuthGuard to each route individually.

app/admin/admin.routing.ts (excerpt)

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

export const adminRouting: ModuleWithProviders = RouterModule.forChild(adminRoutes);

CanDeactivate: handling unsaved changes

Back in the "Heroes" workflow, the app accepts every change to a hero immediately without hesitation or validation.

In the real world, we might have to accumulate the users changes. We might have to validate across fields. We might have to validate on the server. We might have to hold changes in a pending state until the user confirms them as a group or cancels and reverts all changes.

What do we do about unapproved, unsaved changes when the user navigates away? We can't just leave and risk losing the user's changes; that would be a terrible experience.

We'd like to pause and let the user decide what to do. If the user cancels, we'll stay put and allow more changes. If the user approves, the app can save.

We still might delay navigation until the save succeeds. If we let the user move to the next screen immediately and the save failed (perhaps the data are ruled invalid), we would have lost the context of the error.

We can't block while waiting for the server — that's not possible in a browser. We need to stop the navigation while we wait, asynchronously, for the server to return with its answer.

We need the CanDeactivate guard.

Cancel and Save

Our sample application doesn't talk to a server. Fortunately, we have another way to demonstrate an asynchronous router hook.

Users update crisis information in the CrisisDetailComponent. Unlike the HeroDetailComponent, the user changes do not update the crisis entity immediately. We update the entity when the user presses the Save button. We discard the changes if the user presses he Cancel button.

Both buttons navigate back to the crisis list after save or cancel.

app/crisis-center/crisis-detail.component.ts (excerpt)

export class CrisisDetailComponent implements OnInit {
  @HostBinding('@routeAnimation') get routeAnimation() {
    return true;
  }

  @HostBinding('style.display') get display() {
    return 'block';
  }

  @HostBinding('style.position') get position() {
    return 'absolute';
  }

  crisis: Crisis;
  editName: string;

  cancel() {
    this.gotoCrises();
  }

  save() {
    this.crisis.name = this.editName;
    this.gotoCrises();
  }
}

What if the user tries to navigate away without saving or canceling? The user could push the browser back button or click the heroes link. Both actions trigger a navigation. Should the app save or cancel automatically?

We'll do neither. Instead we'll ask the user to make that choice explicitly in a confirmation dialog box that waits asynchronously for the user's answer.

We could wait for the user's answer with synchronous, blocking code. Our app will be more responsive ... and can do other work ... by waiting for the user's answer asynchronously. Waiting for the user asynchronously is like waiting for the server asynchronously.

The DialogService (provided in the AppModule for app-wide use) does the asking.

It returns a promise that resolves when the user eventually decides what to do: either to discard changes and navigate away (true) or to preserve the pending changes and stay in the crisis editor (false).

We create a Guard that will check for the presence of a canDeactivate function in our component, in this case being CrisisDetailComponent. We don't need to know the details of how our CrisisDetailComponent confirms deactivation. This makes our guard reusable, which is an easy win for us.

app/can-deactivate-guard.service.ts

import { Injectable }    from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable }    from 'rxjs/Observable';

export interface CanComponentDeactivate {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

Alternatively, We could make a component-specific CanDeactivate guard for our CrisisDetailComponent. The canDeactivate method provides us with the current instance of our component, the current ActivatedRoute and RouterStateSnapshot in case we needed to access some external information. This would be useful if we only wanted to use this guard for this component and needed to ask the component's properties in or to confirm whether the router should allow navigation away from it.

app/can-deactivate-guard.service.ts (component-specific)

import { Injectable }           from '@angular/core';
import { CanDeactivate,
         ActivatedRouteSnapshot,
         RouterStateSnapshot }  from '@angular/router';

import { CrisisDetailComponent } from './crisis-center/crisis-detail.component';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> {

  canDeactivate(
    component: CrisisDetailComponent,
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Promise<boolean> | boolean {
    // Get the Crisis Center ID
    console.log(route.params['id']);

    // Get the current URL
    console.log(state.url);

    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!component.crisis || component.crisis.name === component.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // promise which resolves to true or false when the user decides
    return component.dialogService.confirm('Discard changes?');
  }
}

Looking back at our CrisisDetailComponent, we have implemented our confirmation workflow for unsaved changes.

app/crisis-center/crisis-detail.component.ts (excerpt)

canDeactivate(): Promise<boolean> | boolean {
  // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
  if (!this.crisis || this.crisis.name === this.editName) {
    return true;
  }
  // Otherwise ask the user with the dialog service and return its
  // promise which resolves to true or false when the user decides
  return this.dialogService.confirm('Discard changes?');
}

Notice that the canDeactivate method can return synchronously; it returns true immediately if there is no crisis or there are no pending changes. But it can also return a Promise or an Observable and the router will wait for that to resolve to truthy (navigate) or falsey (stay put).

We add the Guard to our crisis detail route in crisis-center.routing.ts using the canDeactivate array.

app/crisis-center/crisis-center.routing.ts (can deactivate guard)

import { ModuleWithProviders }   from '@angular/core';
import { Routes, RouterModule }  from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

import { CanDeactivateGuard }    from '../can-deactivate-guard.service';

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard]
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);

We also need to add the Guard to our main appRoutingProviders so the Router can inject it during the navigation process.

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule }   from '@angular/router';

import { loginRoutes,
         authProviders }  from './login.routing';

import { CanDeactivateGuard } from './can-deactivate-guard.service';

const appRoutes: Routes = [
  ...loginRoutes
];

export const appRoutingProviders: any[] = [
  authProviders,
  CanDeactivateGuard
];

export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

Now we have given our user a safeguard against unsaved changes.

Resolve: pre-fetching component data

In our Hero Detail and Crisis Detail, we waited until the route was activated to fetch our respective hero or crisis.

This worked well for us, but we can always do better. If we were using a real world api, there may be some delay in when the data we want to display gets returned. We don't want to display a blank component until the data loads in this situation.

We'd like to pre-fetch data from the server so it's ready the moment our route is activated. We'd also like to handle the situation where our data fails to load or some other error condition occurs. This would help us in our Crisis Center if we navigated to an id that doesn't return a record. We could send the user back to the Crisis List where we only show valid crisis centers. We want to delay rendering of our route component until all necessary data has been fetched or some action has occurred.

We need the Resolve guard.

Preload route information

We'll update our Crisis Detail route to resolve our Crisis before loading the route, or if the user happens to navigate to an invalid crisis center :id, we'll navigate back to our list of existing crises.

Like the CanActivate and CanDeactivate guards, the Resolve guard is an interface we can implement as a service to resolve route data synchronously or asynchronously. In CrisisDetailComponent, we used the ngOnInit to retrieve the Crisis information. We also navigated the user away from the route if the Crisis was not found. It would be more efficient to perform this action before the route is ever activated.

We'll create a CrisisDetailResolve service that will handle retrieving the Crisis and navigating the user away if the Crisis does not exist. Then we can be assured that when we activate the CrisisDetailComponent, the associated Crisis will already be available for display.

Let's create our crisis-detail-resolve.service.ts file within our Crisis Center feature area.

app/crisis-center/crisis-detail-resolve.service.ts

import { Injectable }             from '@angular/core';
import { Router, Resolve,
         ActivatedRouteSnapshot } from '@angular/router';

import { Crisis, CrisisService } from './crisis.service';

@Injectable()
export class CrisisDetailResolve implements Resolve<Crisis> {
  constructor(private cs: CrisisService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot): Promise<Crisis>|boolean {
    let id = +route.params['id'];

    return this.cs.getCrisis(id).then(crisis => {
      if (crisis) {
        return crisis;
      } else { // id not found
        this.router.navigate(['/crisis-center']);
        return false;
      }
    });
  }
}

We'll take the relevant parts of the ngOnInit lifecycle hook in our CrisisDetailComponent and moved them into our CrisisDetailResolve guard. We import the Crisis model and CrisisService and also the Router for navigation from our resolve implementation. We want to be explicit about the data we are resolving, so we implement the Resolve interface with a type of Crisis. This lets us know that what we will resolve will match our Crisis model. We inject the CrisisService and Router and implement the resolve method that supports a Promise, Observable or a synchronous return value.

We'll use our CrisisService.getCrisis method that returns a promise to prevent our route from loading until the data is fetched. If we don't find a valid Crisis, we navigate the user back to the CrisisList, canceling the previous in-flight navigation to the crisis details.

Now that our guard is ready, we'll import it in our crisis-center.routing.ts and use the resolve object in our route configuration.

app/crisis-center/crisis-center.routing.ts (resolve)

import { CrisisDetailResolve }   from './crisis-detail-resolve.service';

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard],
            resolve: {
              crisis: CrisisDetailResolve
            }
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);

We'll add the CrisisDetailResolve service to our crisis center module's providers, so its available to the Router during the navigation process.

app/crisis-center/crisis-center.module.ts (crisis detail resolve provider)

import { CrisisDetailResolve }  from './crisis-detail-resolve.service';

  providers: [
    CrisisService,
    CrisisDetailResolve
  ]

Now that we've added our Resolve guard to fetch data before the route loads, we no longer need to do this once we get into our CrisisDetailComponent. We'll update the CrisisDetailComponent to use the ActivatedRoute data, which is where our crisis property from our Resolve guard will be provided. Once activated, all we need to do is set our local crisis and editName properties from our resolved Crisis information. The Crisis is being provided at the time the route component is activated.

app/crisis-center/crisis-detail.component.ts (ngOnInit v2)

ngOnInit() {
  this.route.data.forEach((data: { crisis: Crisis }) => {
    this.editName = data.crisis.name;
    this.crisis = data.crisis;
  });
}

Two critical points

  1. The router interface is optional. We don't inherit from a base class. We simply implement the interface method or not.

  2. We rely on the router to call the guard. We don't worry about all the ways that the user could navigate away. That's the router's job. We simply write this class and let the router take it from there.

The relevant Crisis Center code for this milestone is

app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1 class="title">Angular Router</h1>
    <nav>
      <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
      <a routerLink="/admin" routerLinkActive="active">Admin</a>
      <a routerLink="/login" routerLinkActive="active">Login</a>
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AppComponent {
}
crisis-center-home.component.ts
// #docplaster
import { Component } from '@angular/core';

@Component({
  template: `
    <p>Welcome to the Crisis Center</p>
  `
})
export class CrisisCenterHomeComponent { }
crisis-center.component.ts
// #docplaster
import { Component } from '@angular/core';

@Component({
  template:  `
    <h2>CRISIS CENTER</h2>
    <router-outlet></router-outlet>
  `
})
export class CrisisCenterComponent { }
crisis-center.routing.ts
import { ModuleWithProviders }   from '@angular/core';
import { Routes, RouterModule }  from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent }       from './crisis-list.component';
import { CrisisCenterComponent }     from './crisis-center.component';
import { CrisisDetailComponent }     from './crisis-detail.component';

import { CanDeactivateGuard }    from '../can-deactivate-guard.service';

import { CrisisDetailResolve }   from './crisis-detail-resolve.service';

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard],
            resolve: {
              crisis: CrisisDetailResolve
            }
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

export const crisisCenterRouting: ModuleWithProviders = RouterModule.forChild(crisisCenterRoutes);
crisis-list.component.ts
import { Component, OnInit }      from '@angular/core';
import { ActivatedRoute, Router, Params } from '@angular/router';

import { Crisis, CrisisService } from './crisis.service';

@Component({
  template: `
    <ul class="items">
      <li *ngFor="let crisis of crises"
        [class.selected]="isSelected(crisis)"
        (click)="onSelect(crisis)">
        <span class="badge">{{crisis.id}}</span> {{crisis.name}}
      </li>
    </ul>

    <router-outlet></router-outlet>
  `
})
export class CrisisListComponent implements OnInit {
  crises: Crisis[];
  public selectedId: number;

  constructor(
    private service: CrisisService,
    private route: ActivatedRoute,
    private router: Router
  ) { }

  isSelected(crisis: Crisis) {
    return crisis.id === this.selectedId;
  }

  ngOnInit() {
    this.route.params.forEach((params: Params) => {
      this.selectedId = params['id'];
      this.service.getCrises()
        .then(crises => this.crises = crises);
    });
  }

  onSelect(crisis: Crisis) {
    this.selectedId = crisis.id;

    // Navigate with relative link
    this.router.navigate([crisis.id], { relativeTo: this.route });
  }
}
crisis-detail.component.ts
import { Component, OnInit, HostBinding,
         trigger, transition,
         animate, style, state }  from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

import { Crisis }         from './crisis.service';
import { DialogService }  from '../dialog.service';

@Component({
  template: `
  <div *ngIf="crisis">
    <h3>"{{editName}}"</h3>
    <div>
      <label>Id: </label>{{crisis.id}}</div>
    <div>
      <label>Name: </label>
      <input [(ngModel)]="editName" placeholder="name"/>
    </div>
    <p>
      <button (click)="save()">Save</button>
      <button (click)="cancel()">Cancel</button>
    </p>
  </div>
  `,
  styles: ['input {width: 20em}'],
  animations: [
    trigger('routeAnimation', [
      state('*',
        style({
          opacity: 1,
          transform: 'translateX(0)'
        })
      ),
      transition('void => *', [
        style({
          opacity: 0,
          transform: 'translateX(-100%)'
        }),
        animate('0.2s ease-in')
      ]),
      transition('* => void', [
        animate('0.5s ease-out', style({
          opacity: 0,
          transform: 'translateY(100%)'
        }))
      ])
    ])
  ]
})
export class CrisisDetailComponent implements OnInit {
  @HostBinding('@routeAnimation') get routeAnimation() {
    return true;
  }

  @HostBinding('style.display') get display() {
    return 'block';
  }

  @HostBinding('style.position') get position() {
    return 'absolute';
  }

  crisis: Crisis;
  editName: string;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    public dialogService: DialogService
  ) { }

  ngOnInit() {
    this.route.data.forEach((data: { crisis: Crisis }) => {
      this.editName = data.crisis.name;
      this.crisis = data.crisis;
    });
  }

  cancel() {
    this.gotoCrises();
  }

  save() {
    this.crisis.name = this.editName;
    this.gotoCrises();
  }

  canDeactivate(): Promise<boolean> | boolean {
    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!this.crisis || this.crisis.name === this.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // promise which resolves to true or false when the user decides
    return this.dialogService.confirm('Discard changes?');
  }

  gotoCrises() {
    let crisisId = this.crisis ? this.crisis.id : null;
    // Pass along the crisis id if available
    // so that the CrisisListComponent can select that crisis.
    // Add a totally useless `foo` parameter for kicks.
    // Relative navigation back to the crises
    this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
  }
}
crisis-detail-resolve.service.ts
import { Injectable }             from '@angular/core';
import { Router, Resolve,
         ActivatedRouteSnapshot } from '@angular/router';

import { Crisis, CrisisService } from './crisis.service';

@Injectable()
export class CrisisDetailResolve implements Resolve<Crisis> {
  constructor(private cs: CrisisService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot): Promise<Crisis>|boolean {
    let id = +route.params['id'];

    return this.cs.getCrisis(id).then(crisis => {
      if (crisis) {
        return crisis;
      } else { // id not found
        this.router.navigate(['/crisis-center']);
        return false;
      }
    });
  }
}
crisis.service.ts
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));
  }

}
auth-guard.service.ts
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;
  }
}
can-deactivate-guard.service.ts
import { Injectable }    from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable }    from 'rxjs/Observable';

export interface CanComponentDeactivate {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

Query Parameters and Fragments

In our route parameters example, we only dealt with parameters specific to our route, but what if we wanted optional parameters available to all routes? This is where our query parameters come into play and serve a special purpose in our application.

Fragments refer to certain elements on the page identified with an id attribute.

We'll update our AuthGuard to provide a session_id query that will remain after navigating to another route.

We'll also provide an arbitrary anchor fragment, which we would use to jump to a certain point on our page.

We'll add the NavigationExtras object to our router.navigate method that navigates us to our /login route.

app/auth-guard.service.ts (v3)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  NavigationExtras
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Create a dummy session id
    let sessionId = 123456789;

    // Set our navigation extras object
    // that contains our global query params and fragment
    let navigationExtras: NavigationExtras = {
      queryParams: { 'session_id': sessionId },
      fragment: 'anchor'
    };

    // Navigate to the login page with extras
    this.router.navigate(['/login'], navigationExtras);
    return false;
  }
}

We can also preserve query parameters and fragments across navigations without having to re-provide them when navigating. In our LoginComponent, we'll add an object as the second argument in our router.navigate function and provide the preserveQueryParams and preserveFragment to pass along the current query parameters and fragment to the next route.

app/login.component.ts (preserve)

// Set our navigation extras object
// that passes on our global query params and fragment
let navigationExtras: NavigationExtras = {
  preserveQueryParams: true,
  preserveFragment: true
};

// Redirect the user
this.router.navigate([redirect], navigationExtras);

Since we'll be navigating to our Admin Dashboard route after logging in, we'll update it to handle our query parameters and fragment.

app/admin/admin-dashboard.component.ts (v2)

import { Component, OnInit }  from '@angular/core';
import { ActivatedRoute }     from '@angular/router';
import { Observable }         from 'rxjs/Observable';
import 'rxjs/add/operator/map';

@Component({
  template:  `
    <p>Dashboard</p>

    <p>Session ID: {{ sessionId | async }}</p>
    <a id="anchor"></a>
    <p>Token: {{ token | async }}</p>
  `
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable<string>;
  token: Observable<string>;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParams
      .map(params => params['session_id'] || 'None');

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .map(fragment => fragment || 'None');
  }
}

Query Parameters and Fragments are also available through the ActivatedRoute service available to route components. Just like our route parameters, query parameters and fragments are provided as an Observable. For our updated Crisis Admin component we'll feed the Observable directly into our template using the AsyncPipe, which will handle unsubscribing from the Observable for us when the component is destroyed.

pop out the window

When running in plunker, pop out the preview window by clicking the blue 'X' button in the upper right corner.

Following the steps in this process, we can click on the Admin button, that takes us to the Login page with our provided query params and fragment. After we click the login button, we notice that we have been redirected to the Admin Dashboard page with our query params and fragment still intact. We can use these persistent bits of information for things that need to be provided with across pages interaction like authentication tokens or session ids.

The query params and fragment can also be preserved using a RouterLink with the preserveQueryParams and preserveFragment bindings respectively.

Milestone #5: Asynchronous Routing

As we have completed our milestones, our application has naturally gotten larger. As we continue to build out feature areas our overall application size will get larger also. At some point we'll reach a tipping point in where our application takes a significant enough time to load. This is not a viable long term solution.

So how do we combat this problem? We introduce asynchronous routing into our application and take advantage of loading feature areas lazily. This buys us multiple things:

  • We can continue building out feature areas without increasing our initial bundle.
  • We can load feature areas only when requested by the user.
  • We can speed up load time for users that only visit certain areas of our application.

These are all things we want to have in our application, so let's apply this to our current setup. We've already made great strides by organizing our application into four modules: AppModule, HeroesModule, AdminModule and CrisisCenterModule. Our AdminModule is the area of our application that would be scoped to a small set of users, so we'll take advantage of asynchronous routing and only load the Admin feature area when requested.

Lazy-Loading route configuration

We'll start by adding an admin route to our app.routing.ts file. We want to load our Admin module asynchronously, so we'll use the loadChildren property in our route config where previously we used the children property to include our child routes.

We'll also change our admin path in our admin.routing.ts to an empty path. The Router supports empty path routes, which we can use for grouping routes together without adding anything additional paths to the URL. Our users will still visit /admin and our AdminComponent still serves as our Routing Component which contains our child routes.

app.routing.ts (load children)
const adminRoutes: Routes = [
  {
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
  }
];

const appRoutes: Routes = [
  ...loginRoutes,
  ...adminRoutes
];
app/admin/admin.routing.ts (empty path admin)
import { ModuleWithProviders }   from '@angular/core';
import { Routes, RouterModule }  from '@angular/router';

import { AdminComponent }           from './admin.component';
import { AdminDashboardComponent }  from './admin-dashboard.component';
import { ManageCrisesComponent }    from './manage-crises.component';
import { ManageHeroesComponent }    from './manage-heroes.component';

import { AuthGuard }                from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: '',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

export const adminRouting: ModuleWithProviders = RouterModule.forChild(adminRoutes);

We use the ES2015 spread feature to flatten the route arrays of our adminRoutes and loginRoutes into our appRoutes array to provide a simple array of routes.

The loadChildren property is used by the Router to map to our bundle we want to lazy-load, in this case being the AdminModule.

If we look closer at the loadChildren string, we can see that it maps directly to our admin.module.ts file where we previously built out our Admin feature area. After the path to the file we use a # to denote where our file path ends and to tell the Router the name of our AdminModule. If we look in our admin.module.ts file, we can see it matches name of our exported module class.

app/admin/admin.module.ts (export)

export class AdminModule {}

The loadChildren property is used by the Router to map to our bundle we want to lazy-load, in this case being the AdminModule. The router will take our loadChildren string and dynamically load in our AdminModule, add its routes to our configuration dynamically and then load the requested route. This will only happen when the route is first requested and the module will be immediately be available for subsequent requests.

Angular provides a built-in module loader that supports SystemJS to load modules asynchronously. If we were using another bundling tool, such as Webpack, we would use the Webpack mechanism for asynchronously loading modules.

We've built our feature area, we've updated our route configuration to take advantage of lazy-loading, now we have to do the final step to break our AdminModule into a completely separate module. In our app.module.ts, we'll remove our AdminModule from the imports array since we'll be loading it on-demand an we'll remove the imported AdminModule.

app/app.module.ts (async admin module)

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';

import { AppComponent }         from './app.component';
import { routing,
         appRoutingProviders }  from './app.routing';

import { HeroesModule }         from './heroes/heroes.module';
import { CrisisCenterModule }   from './crisis-center/crisis-center.module';

import { LoginComponent }       from './login.component';

import { DialogService }        from './dialog.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    routing,
    HeroesModule,
    CrisisCenterModule
  ],
  declarations: [
    AppComponent,
    LoginComponent
  ],
  providers: [
    appRoutingProviders,
    DialogService
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}

CanLoad Guard: guarding against loading of feature modules

We're already protecting our AdminModule with a CanActivate guard that prevents the user from accessing the admin feature area unless authorized. We're currently loading the admin routing asynchronously when requested, checking the user access and redirecting to the login page if not authorized. Ideally, we only want to load the AdminModule if the user is logged in and prevent the AdminModule and its routing from being loaded until then.

The CanLoad guard covers this scenario.

We can use the CanLoad guard to only load the AdminModule once the user is logged in and attempts to access the admin feature area. We'll update our existing AuthGuard to support the CanLoad guard. We'll import the CanLoad interface and the Route the guard provides when called that contains the requested path.

We'll add the interface to our service, and then we'll implement the interface. Since our AuthGuard already checks the user's logged in state, we can pass that access check to our canLoad method. The Route in the canLoad method provides a path which comes from our route configuration.

app/auth-guard.service.ts (can load guard)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  NavigationExtras,
  CanLoad, Route
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    let url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

  canLoad(route: Route): boolean {
    let url = `/${route.path}`;

    return this.checkLogin(url);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Create a dummy session id
    let sessionId = 123456789;

    // Set our navigation extras object
    // that contains our global query params and fragment
    let navigationExtras: NavigationExtras = {
      queryParams: { 'session_id': sessionId },
      fragment: 'anchor'
    };

    // Navigate to the login page with extras
    this.router.navigate(['/login'], navigationExtras);
    return false;
  }
}

Next, we'll import the AuthGuard into our app.routing.ts and add the AuthGuard to the canLoad array for our admin route. Now our admin feature area is only loaded when the proper access has been granted.

app/app.routing.ts (can load guard)

import { AuthGuard }          from './auth-guard.service';

const adminRoutes: Routes = [
  {
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
    canLoad: [AuthGuard]
  }
];

Wrap Up

We've covered a lot of ground in this chapter and the application is too big to reprint here. Please visit the live example and where you can download the final source code.

Appendices

The balance of this chapter is a set of appendices that elaborate some of the points we covered quickly above.

The appendix material isn't essential. Continued reading is for the curious.

Appendix: Link Parameters Array

We've mentioned the Link Parameters Array several times. We've used it several times.

A link parameters array holds the ingredients for router navigation:

  • the path of the route to the destination component
  • required and optional route parameters that go into the route URL

We can bind the RouterLink directive to such an array like this:

<a [routerLink]="['/heroes']">Heroes</a>

We've written a two element array when specifying a route parameter like this

this.router.navigate(['/hero', hero.id]);

We can provide optional route parameters in an object like this:

<a [routerLink]="['/crisis-center', { foo: 'foo' }]">Crisis Center</a>

These three examples cover our needs for an app with one level routing. The moment we add a child router, such as the Crisis Center, we create new link array possibilities.

Recall that we specified a default child route for Crisis Center so this simple RouterLink is fine.

<a [routerLink]="['/crisis-center']">Crisis Center</a>

Let's parse it out.

  • The first item in the array identifies the parent route ('/crisis-center').
  • There are no parameters for this parent route so we're done with it.
  • There is no default for the child route so we need to pick one.
  • We decide to go to the CrisisListComponent whose route path is '/' but we don't need to explicitly add it
  • Voila! ['/crisis-center'].

Let's take it a step further. This time we'll build a link parameters array that navigates from the root of the application down to the "Dragon Crisis".

  • The first item in the array identifies the parent route ('/crisis-center').
  • There are no parameters for this parent route so we're done with it.
  • The second item identifies the child route for details about a particular crisis ('/:id').
  • The details child route requires an id route parameter
  • We add id of the Dragon Crisis as the second item in the array (1)

It looks like this!

<a [routerLink]="['/crisis-center', 1]">Dragon Crisis</a>

If we wanted to, we could redefine our AppComponent template with Crisis Center routes exclusively:

template: `
  <h1 class="title">Angular Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/crisis-center/1', { foo: 'foo' }]">Dragon Crisis</a>
    <a [routerLink]="['/crisis-center/2']">Shark Crisis</a>
  </nav>
  <router-outlet></router-outlet>
`

In sum, we can write applications with one, two or more levels of routing. The link parameters array affords the flexibility to represent any routing depth and any legal sequence of route paths, (required) router parameters and (optional) route parameter objects.

Appendix: Why use an ngOnInit method

We implemented an ngOnInit method in many of our Component classes. We did so, for example, in the HeroDetailComponent. We might have put the ngOnInit logic inside the constructor instead. We didn't for a reason. The reason is testability.

A constructor that has major side-effects can be difficult to test because it starts doing things as soon as we create a test instance. In this case, it might have made a request to a remote server, something it shouldn't do under test. It may even be impossible to reach the server in the test environment.

The better practice is to limit what the constructor can do. Mostly it should stash parameters in local variables and perform simple instance configuration.

Yet we want an instance of this class to get the hero data from the HeroService soon after it is created. How do we ensure that happens if not in the constructor?

Angular detects when a component has certain lifecycle methods like ngOnInit and ngOnDestroy and calls them at the appropriate moment.

Angular will call ngOnInit when we navigate to the HeroDetailComponent, we'll get the id from the ActivatedRoute params and ask the server for the hero with that id.

We too can call that ngOnInit method in our tests if we wish ... after taking control of the injected HeroService and (perhaps) mocking it.

Appendix: LocationStrategy and browser URL styles

When the router navigates to a new component view, it updates the browser's location and history with a URL for that view. This is a strictly local URL. The browser shouldn't send this URL to the server and should not reload the page.

Modern HTML 5 browsers support history.pushState, a technique that changes a browser's location and history without triggering a server page request. The router can compose a "natural" URL that is indistinguishable from one that would otherwise require a page load.

Here's the Crisis Center URL in this "HTML 5 pushState" style:

localhost:3002/crisis-center/

Older browsers send page requests to the server when the location URL changes ... unless the change occurs after a "#" (called the "hash"). Routers can take advantage of this exception by composing in-application route URLs with hashes. Here's a "hash URL" that routes to the Crisis Center

localhost:3002/src/#/crisis-center/

The Router supports both styles with two LocationStrategy providers:

  1. PathLocationStrategy - the default "HTML 5 pushState" style.
  2. HashLocationStrategy - the "hash URL" style.

The RouterModule.forRoot function sets the LocationStrategy to the PathLocationStrategy, making it the default strategy. We can switch to the HashLocationStrategy with an override during the bootstrapping process if we prefer it.

Learn about "providers" and the bootstrap process in the Dependency Injection chapter

Which Strategy is Best?

We must choose a strategy and we need to make the right call early in the project. It won't be easy to change later once the application is in production and there are lots of application URL references in the wild.

Almost all Angular 2 projects should use the default HTML 5 style. It produces URLs that are easier for users to understand. And it preserves the option to do server-side rendering later.

Rendering critical pages on the server is a technique that can greatly improve perceived responsiveness when the app first loads. An app that would otherwise take ten or more seconds to start could be rendered on the server and delivered to the user's device in less than a second.

This option is only available if application URLs look like normal web URLs without hashes (#) in the middle.

Stick with the default unless you have a compelling reason to resort to hash routes.

HTML 5 URLs and the <base href>

While the router uses the "HTML 5 pushState" style by default, we must configure that strategy with a base href

The preferred way to configure the strategy is to add a <base href> element tag in the <head> of the index.html.

<base href="/">

Without that tag, the browser may not be able to load resources (images, css, scripts) when "deep linking" into the app. Bad things could happen when someone pastes an application link into the browser's address bar or clicks such a link in an email link.

Some developers may not be able to add the <base> element, perhaps because they don't have access to <head> or the index.html.

Those developers may still use HTML 5 URLs by taking two remedial steps:

  1. Provide the router with an appropriate APP_BASE_HREF value.
  2. Use absolute URLs for all web resources: css, images, scripts, and template html files.

Learn about the APP_BASE_HREF in the API Guide.

HashLocationStrategy

We can go old-school with the HashLocationStrategy by providing the useHash: true in an object as the second argument of the RouterModule.forRoot in our AppModule.

app/app.module.ts (hash URL strategy)

import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { FormsModule }          from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { AppComponent }  from './app.component';

const routes: Routes = [

];

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(routes, { useHash: true })  // .../#/crisis-center/
  ],
  declarations: [
    AppComponent
  ],
  providers: [

  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}
doc_Angular
2016-10-06 09:47:01
Comments
Leave a Comment

Please login to continue.