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: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
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.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 Detail
route, the component was recreated each time you selected a different hero.
At the top level, paths that begin with
/
refer to the root of the application. But child routes extend the path of the parent route. With each step down the route tree, you add a slash followed by the route path, unless the path is empty.
Apply that logic to navigation within the crisis center for which the parent path is
/crisis-center
.- To navigate to the
CrisisCenterHomeComponent
, the full URL is/crisis-center
(/crisis-center
+''
+''
). - To navigate to the
CrisisDetailComponent
for a crisis 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 AppModule
before the AppRoutingModule
:import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { DialogService } from './dialog.service';
@NgModule({
imports: [
CommonModule,
FormsModule,
HeroesModule,
CrisisCenterModule,
AppRoutingModule
],
declarations: [
AppComponent,
PageNotFoundComponent
],
providers: [
DialogService
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Remove the initial crisis center route from the
app-routing.module.ts
. The feature routes are now provided by the HeroesModule
and the CrisisCenter
modules.
The
app-routing.module.ts
file retains the top-level application routes such as the default and wildcard routes.import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ComposeMessageComponent } from './compose-message.component';
import { PageNotFoundComponent } from './not-found.component';
const appRoutes: Routes = [
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{ 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
compose
route in the popup
outlet.
This
outlets
object within an outer object was completely unnecessary when there was only one route and one unnamed outlet to think about.
The router assumed that your route specification targeted the unnamed primary outlet and created these objects for you.
Routing to a named outlet has revealed a previously hidden router truth: you can target multiple outlets with multiple routes in the same
RouterLink
directive.
You're not actually doing that here. But to target a named outlet, you must use the richer, more verbose syntax.
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
), acolon
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: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