Skip to main content

Angular - Routing and Navigation Part 5

Milestone 4: Crisis center feature

It's time to add real features to the app's current placeholder crisis center.
Begin by imitating the heroes feature:
  • Delete the placeholder crisis center file.
  • Create an app/crisis-center folder.
  • Copy the files from app/heroes into the new crisis center folder.
  • In the new files, change every mention of "hero" to "crisis", and "heroes" to "crises".
You'll turn the CrisisService into a purveyor of mock crises instead of mock heroes:
src/app/crisis-center/crisis.service.ts (mock-crises)
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';

export class Crisis {
  constructor(public id: number, public name: string) { }
}

const CRISES = [
  new Crisis(1, 'Dragon Burning Cities'),
  new Crisis(2, 'Sky Rains Great White Sharks'),
  new Crisis(3, 'Giant Asteroid Heading For Earth'),
  new Crisis(4, 'Procrastinators Meeting Delayed Again'),
];

The resulting crisis center is a foundation for introducing a new concept—child routing. You can leave Heroes in its current state as a contrast with the Crisis Center and decide later if the differences are worthwhile.
In keeping with the Separation of Concerns principle, changes to the Crisis Center won't affect the AppModule or any other feature's component.

A crisis center with child routes

This section shows you how to organize the crisis center to conform to the following recommended pattern for Angular applications:
  • Each feature area resides in its own folder.
  • Each feature has its own Angular feature module.
  • Each area has its own area root component.
  • Each area root component has its own router outlet and child routes.
  • Feature area routes rarely (if ever) cross with routes of other features.
If your app had many feature areas, the app component trees might look like this:
Component Tree

Child routing component

Add the following crisis-center.component.ts to the crisis-center folder:
src/app/crisis-center/crisis-center.component.ts
import { Component } from '@angular/core';

@Component({
  template:  `
    

CRISIS CENTER

<
router-outlet></router-outlet> ` }) export class CrisisCenterComponent { }

The CrisisCenterComponent has the following in common with the AppComponent:
  • 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.
Like most shells, the CrisisCenterComponent class is very simple, simpler even than AppComponent: it has no business logic, and its template has no links, just a title and <router-outlet> for the crisis center child views.
Unlike AppComponent, and most other components, it lacks a selector. It doesn't need one since you don't embed this component in a parent template, instead you use the router to navigate to it.

Child route configuration

As a host page for the "Crisis Center" feature, add the following crisis-center-home.component.ts to the crisis-center folder.
src/app/crisis-center/crisis-center-home.component.ts
import { Component } from '@angular/core';

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

Create a crisis-center-routing.module.ts file as you did the heroes-routing.module.ts file. This time, you define child routes within the parent crisis-center route.
src/app/crisis-center/crisis-center-routing.module.ts (Routes)
const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

Notice that the parent crisis-center route has a children property with a single route containing the CrisisListComponent. The CrisisListComponent route also has a children array with two routes.
These two routes navigate to the crisis center child components, CrisisCenterHomeComponent and CrisisDetailComponent, respectively.
There are important differences in the way the router treats these child routes.
The router displays the components of these routes in the RouterOutlet of the CrisisCenterComponent, not in the RouterOutlet of the AppComponent shell.
The CrisisListComponent contains the crisis list and a RouterOutlet to display the Crisis Center Home and Crisis Detail route components.
The Crisis Detail route is a child of the Crisis List. Since the router reuses components by default, the Crisis Detail component will be re-used as you select different crises. In contrast, back in the Hero Detailroute, the component was recreated each time you selected a different hero.
At the top level, paths that begin with / refer to the root of the application. But child routes extend the path of the parent route. With each step down the route tree, you add a slash followed by the route path, unless the path is empty.
Apply that logic to navigation within the crisis center for which the parent path is /crisis-center.
  • To navigate to the CrisisCenterHomeComponent, the full URL is /crisis-center (/crisis-center + '' + '').
  • To navigate to the CrisisDetailComponent for a crisis with id=2, the full URL is /crisis-center/2(/crisis-center + '' + '/2').
The absolute URL for the latter example, including the localhost origin, is
localhost:3000/crisis-center/2

Here's the complete crisis-center-routing.module.ts file with its imports.
src/app/crisis-center/crisis-center-routing.module.ts (excerpt)
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

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

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

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class CrisisCenterRoutingModule { }

Import crisis center module into the AppModule routes

As with the HeroesModule, you must add the CrisisCenterModule to the imports array of the AppModulebefore the AppRoutingModule:
src/app/app.module.ts (import CrisisCenterModule)
import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

import { AppComponent }            from './app.component';
import { PageNotFoundComponent }   from './not-found.component';

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

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

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

Remove the initial crisis center route from the app-routing.module.ts. The feature routes are now provided by the HeroesModule and the CrisisCenter modules.
The app-routing.module.ts file retains the top-level application routes such as the default and wildcard routes.
src/app/app-routing.module.ts (v3)
import { NgModule }                from '@angular/core';
import { RouterModule, Routes }    from '@angular/router';

import { ComposeMessageComponent } from './compose-message.component';
import { PageNotFoundComponent }   from './not-found.component';

const appRoutes: Routes = [
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging="" only="" purposes="" span="">
    )
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}

Relative navigation

While building out the crisis center feature, you navigated to the crisis detail route using an absolute paththat begins with a slash.
The router matches such absolute paths to routes starting from the top of the route configuration.
You could continue to use absolute paths like this to navigate inside the Crisis Center feature, but that pins the links to the parent routing structure. If you changed the parent /crisis-center path, you would have to change the link parameters array.
You can free the links from this dependency by defining paths that are relative to the current URL segment. Navigation within the feature area remains intact even if you change the parent route path to the feature.
Here's an example:
The router supports directory-like syntax in a link parameters list to help guide route name lookup:
./ or no leading slash is relative to the current level.
../ to go up one level in the route path.
You can combine relative navigation syntax with an ancestor path. If you must navigate to a sibling route, you could use the ../ convention to go up one level, then over and down the sibling route path.
To navigate a relative path with the Router.navigate method, you must supply the ActivatedRoute to give the router knowledge of where you are in the current route tree.
After the link parameters array, add an object with a relativeTo property set to the ActivatedRoute. The router then calculates the target URL based on the active route's location.
Always specify the complete absolute path when calling router's navigateByUrl method.
You've already injected the ActivatedRoute that you need to compose the relative navigation path.
When using a RouterLink to navigate instead of the Router service, you'd use the same link parameters array, but you wouldn't provide the object with the relativeTo property. The ActivatedRoute is implicit in a RouterLink directive.
Update the gotoCrises method of the CrisisDetailComponent to navigate back to the Crisis Center list using relative path navigation.
src/app/crisis-center/crisis-detail.component.ts (relative navigation)
// Relative navigation back to the crises
this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });

Notice that the path goes up a level using the ../ syntax. If the current crisis id is 3, the resulting path back to the crisis list is /crisis-center/;id=3;foo=foo.

Displaying multiple routes in named outlets

You decide to give users a way to contact the crisis center. When a user clicks a "Contact" button, you want to display a message in a popup view.
The popup should stay open, even when switching between pages in the application, until the user closes it by sending the message or canceling. Clearly you can't put the popup in the same outlet as the other pages.
Until now, you've defined a single outlet and you've nested child routes under that outlet to group routes together. The router only supports one primary unnamed outlet per template.
A template can also have any number of named outlets. Each named outlet has its own set of routes with their own components. Multiple outlets can be displaying different content, determined by different routes, all at the same time.
Add an outlet named "popup" in the AppComponent, directly below the unnamed outlet.
src/app/app.component.ts (outlets)
<router-outlet></router-outlet>
<router-outlet name="popup"></router-outlet>

That's where a popup will go, once you learn how to route a popup component to it.

Secondary routes

Named outlets are the targets of secondary routes.
Secondary routes look like primary routes and you configure them the same way. They differ in a few key respects.
  • They are independent of each other.
  • They work in combination with other routes.
  • They are displayed in named outlets.
Create a new component named ComposeMessageComponent in src/app/compose-message.component.ts. It displays a simple form with a header, an input box for the message, and two buttons, "Send" and "Cancel".
Contact popup
Here's the component and its template:

  1. import { Component, HostBinding } from '@angular/core';
  2. import { Router } from '@angular/router';
  3.  
  4. import { slideInDownAnimation } from './animations';
  5.  
  6. @Component({
  7. templateUrl: './compose-message.component.html',
  8. styles: [ ':host { position: relative; bottom: 10%; }' ],
  9. animations: [ slideInDownAnimation ]
  10. })
  11. export class ComposeMessageComponent {
  12. @HostBinding('@routeAnimation') routeAnimation = true;
  13. @HostBinding('style.display') display = 'block';
  14. @HostBinding('style.position') position = 'absolute';
  15.  
  16. details: string;
  17. sending = false;
  18.  
  19. constructor(private router: Router) {}
  20.  
  21. send() {
  22. this.sending = true;
  23. this.details = 'Sending Message...';
  24.  
  25. setTimeout(() => {
  26. this.sending = false;
  27. this.closePopup();
  28. }, 1000);
  29. }
  30.  
  31. cancel() {
  32. this.closePopup();
  33. }
  34.  
  35. closePopup() {
  36. // Providing a `null` value to the named outlet
  37. // clears the contents of the named outlet
  38. this.router.navigate([{ outlets: { popup: null }}]);
  39. }
  40. }
It looks about the same as any other component you've seen in this guide. There are two noteworthy differences.
Note that the send() method simulates latency by waiting a second before "sending" the message and closing the popup.
The closePopup() method closes the popup view by navigating to the popup outlet with a null. That's a peculiarity covered below.
As with other application components, you add the ComposeMessageComponent to the declarations of an NgModule. Do so in the AppModule.

Add a secondary route

Open the AppRoutingModule and add a new compose route to the appRoutes.
src/app/app-routing.module.ts (compose route)
{
  path: 'compose',
  component: ComposeMessageComponent,
  outlet: 'popup'
},

The path and component properties should be familiar. There's a new property, outlet, set to 'popup'. This route now targets the popup outlet and the ComposeMessageComponent will display there.
The user needs a way to open the popup. Open the AppComponent and add a "Contact" link.
src/app/app.component.ts (contact-link)
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>

Although the compose route is pinned to the "popup" outlet, that's not sufficient for wiring the route to a RouterLink directive. You have to specify the named outlet in a link parameters array and bind it to the RouterLink with a property binding.
The link parameters array contains an object with a single outlets property whose value is another object keyed by one (or more) outlet names. In this case there is only the "popup" outlet property and its value is another link parameters array that specifies the compose route.
You are in effect saying, when the user clicks this link, display the component associated with the composeroute in the popup outlet.
This outlets object within an outer object was completely unnecessary when there was only one route and one unnamed outlet to think about.
The router assumed that your route specification targeted the unnamed primary outlet and created these objects for you.
Routing to a named outlet has revealed a previously hidden router truth: you can target multiple outlets with multiple routes in the same RouterLink directive.
You're not actually doing that here. But to target a named outlet, you must use the richer, more verbose syntax.

Secondary route navigation: merging routes during navigation

Navigate to the Crisis Center and click "Contact". you should see something like the following URL in the browser address bar.
http://.../crisis-center(popup:compose)

The interesting part of the URL follows the ...:
  • The crisis-center is the primary navigation.
  • Parentheses surround the secondary route.
  • The secondary route consists of an outlet name (popup), a colon separator, and the secondary route path (compose).
Click the Heroes link and look at the URL again.
http://.../heroes(popup:compose)

The primary navigation part has changed; the secondary route is the same.
The router is keeping track of two separate branches in a navigation tree and generating a representation of that tree in the URL.
You can add many more outlets and routes, at the top level and in nested levels, creating a navigation tree with many branches. The router will generate the URL to go with it.
You can tell the router to navigate an entire tree at once by filling out the outlets object mentioned above. Then pass that object inside a link parameters array to the router.navigate method.
Experiment with these possibilities at your leisure.

Clearing secondary routes

As you've learned, a component in an outlet persists until you navigate away to a new component. Secondary outlets are no different in this regard.
Each secondary outlet has its own navigation, independent of the navigation driving the primary outlet. Changing a current route that displays in the primary outlet has no effect on the popup outlet. That's why the popup stays visible as you navigate among the crises and heroes.
Clicking the "send" or "cancel" buttons does clear the popup view. To see how, look at the closePopup()method again:
src/app/compose-message.component.ts (closePopup)
closePopup() {
  // Providing a `null` value to the named outlet
  // clears the contents of the named outlet
  this.router.navigate([{ outlets: { popup: null }}]);
}

It navigates imperatively with the Router.navigate() method, passing in a link parameters array.
Like the array bound to the Contact RouterLink in the AppComponent, this one includes an object with an outlets property. The outlets property value is another object with outlet names for keys. The only named outlet is 'popup'.
This time, the value of 'popup' is null. That's not a route, but it is a legitimate value. Setting the popup RouterOutlet to null clears the outlet and removes the secondary popup route from the current URL.

Comments