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-centerfolder. - Copy the files from
app/heroesinto 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: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:

Child routing component
Add the following
crisis-center.component.ts to the crisis-center folder: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
AppComponentis the root of the entire application. - It is a shell for the crisis management feature area, just as the
AppComponentis 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.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.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
CrisisDetailComponentfor a crisis withid=2, the full URL is/crisis-center/2(/crisis-center+''+'/2').
The absolute URL for the latter example, including the
localhost origin, islocalhost:3000/crisis-center/2
Here's the complete
crisis-center-routing.module.ts file with its imports.import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent } from './crisis-list.component';
import { CrisisCenterComponent } from './crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail.component';
const crisisCenterRoutes: Routes = [
{
path: 'crisis-center',
component: CrisisCenterComponent,
children: [
{
path: '',
component: CrisisListComponent,
children: [
{
path: ':id',
component: CrisisDetailComponent
},
{
path: '',
component: CrisisCenterHomeComponent
}
]
}
]
}
];
@NgModule({
imports: [
RouterModule.forChild(crisisCenterRoutes)
],
exports: [
RouterModule
]
})
export class CrisisCenterRoutingModule { }
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:import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { DialogService } from './dialog.service';
@NgModule({
imports: [
CommonModule,
FormsModule,
HeroesModule,
CrisisCenterModule,
AppRoutingModule
],
declarations: [
AppComponent,
PageNotFoundComponent
],
providers: [
DialogService
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Remove the initial crisis center route from the
app-routing.module.ts. The feature routes are now provided by the HeroesModule and the CrisisCenter modules.
The
app-routing.module.ts file retains the top-level application routes such as the default and wildcard routes.import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ComposeMessageComponent } from './compose-message.component';
import { PageNotFoundComponent } from './not-found.component';
const appRoutes: Routes = [
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{ 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.Navigate to crisis list with a relative URL
You've already injected the
ActivatedRoute that you need to compose the relative navigation path.
When using a
RouterLink to navigate instead of the Router service, you'd use the same link parameters array, but you wouldn't provide the object with the relativeTo property. The ActivatedRoute is implicit in a RouterLink directive.
Update the
gotoCrises method of the CrisisDetailComponent to navigate back to the Crisis Center list using relative path navigation.// Relative navigation back to the crises
this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
Notice that the path goes up a level using the
../ syntax. If the current crisis id is 3, the resulting path back to the crisis list is /crisis-center/;id=3;foo=foo.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.<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".
Here's the component and its template:
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.{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'popup'
},
The
path and component properties should be familiar. There's a new property, outlet, set to 'popup'. This route now targets the popup outlet and the ComposeMessageComponent will display there.
The user needs a way to open the popup. Open the
AppComponent and add a "Contact" link.<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
Although the
compose route is pinned to the "popup" outlet, that's not sufficient for wiring the route to a RouterLink directive. You have to specify the named outlet in a link parameters array and bind it to the RouterLink with a property binding.
The link parameters array contains an object with a single
outlets property whose value is another object keyed by one (or more) outlet names. In this case there is only the "popup" outlet property and its value is another link parameters array that specifies the compose route.
You are in effect saying, when the user clicks this link, display the component associated with the
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-centeris the primary navigation. - Parentheses surround the secondary route.
- The secondary route consists of an outlet name (
popup), acolonseparator, 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: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
Post a Comment