Folks, I've been having some trouble trying to make this code work.
Basically, I want to pass some parameters to a nested and named router-outlet
.
From the repo: https://stackblitz.com/edit/angular-hsezw8-rp5ne9?file=src/app/crisis-center/crisis-list/crisis-list.component.html
I'm using the following routerLink
:
<a [routerLink]="[{ outlets: { test: [ crisis.id] } }]">
<span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
</a>
...
<router-outlet name="test"></router-outlet>
And my routes are as below:
{
path: '',
component: CrisisListComponent,
children: [
{
path: ':id',
component: CrisisDetailComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
crisis: CrisisDetailResolverService
},
outlet: 'test'
},
{
path: '',
component: CrisisCenterHomeComponent
}
]
}
After searching in stackoverflow, based on answers, that should be working, but it's not. Does anybody has any inside on what I am missing? Thanks in advance
I've written an article that goes into detail about the new fix that came with Angular 11: Angular Router: empty paths, named outlets and a fix that came with Angular 11.
A TLRD for that is that named outlets with (nested) empty paths don't always play well(at lest in older versions). This is one of those cases.
Firstly, it's important to mention how Angular Router handles the routes and their changes. For example, it keeps track of the current URL with the help of a data structure called UrlTree
1. A UrlTree can be thought of as the deserialized version of an URL, which is a string. Given a UrlTree
, the route configurations array will be traversed(in a DFS fashion) based on this UrlTree
.
The root of the UrlTree is another structure, UrlSegmentGroup
:
constructor(
/** The URL segments of this group. See `UrlSegment` for more information */
public segments: UrlSegment[],
/** The list of children of this group */
public children: {[key: string]: UrlSegmentGroup}) {
forEach(children, (v: any, k: any) => v.parent = this);
as you can see, it resembles a regular tree structure. The a segment from the segments
array contains the path
(e.g 'crisis-center'
) and the matrix params for a given path. As for the children
property, its keys are the name of the outlets(e.g 'test'
) and the values are UrlSegmentGroup
instances. By using these structures, Angular Router can represent URL strings and can provide many features and customizations.
Given the route configuration from crisis-center-routing.module.ts
:
const crisisCenterRoutes: Routes = [
{
path: '', // (1)
component: CrisisCenterComponent,
children: [
{
path: '', // (2)
component: CrisisListComponent,
children: [
{
path: ':id', // (3)
component: CrisisDetailComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
crisis: CrisisDetailResolverService
},
outlet: 'test'
},
{
path: '',
component: CrisisCenterHomeComponent
}
]
}
]
}
];
an UrlTree
that would match the route config object with path: ':id'
would rough look like this:
// Note: For simplicity, I've only used the `path` to represent the `segments` property
UrlTree {
// UrlSegmentGroup
root: {
children: {
primary: {
children: {
primary: {
children: {
test: {
children: {}
segments: [1], // Matches (3)
}
},
segments: [''], // Matches (1) and (2)
}
}
segments: ['crisis-center'],
}
}
segments: [],
}
}
Note: the default name for an outlet is 'primary'
.
The above UrlTree can be achieved with this:
<a [routerLink]="['/crisis-center', { outlets: { primary: ['', { outlets: { test: [crisis.id] } }] } }]">
<span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
</a>
Although the above approach theoretically should work, it doesn't. That's because the ''
is not consumed and when this happens, the primary
outlet will be chosen. More specifically, the ''
from primary: ['' ...
will be matched with (1)
, but then the second ''
(from (2)
) won't be consumed.
However, this seems to be fixed in Angular v12:
// At this point, not all paths of the `segment` array have been consumed.
// This time, the route's outlet is taken into account.
const matchedOnOutlet = getOutlet(route) === outlet;
const expanded$ = this.expandSegment(
childModule, segmentGroup, childConfig, slicedSegments,
matchedOnOutlet ? PRIMARY_OUTLET : outlet, true);
But this is how it used to be:
// At this point, not all paths of the `segment` array have been consumed.
// As you can see, it always chooses the `primary` outlet
const expanded$ = this.expandSegment(
childModule, segmentGroup, childConfig, slicedSegments, PRIMARY_OUTLET, true);
Since you're an older version, we'll need to find another approach.
A similar thing happens with the current approach:
<a [routerLink]="[{ outlets: { test: [ crisis.id] } }]">
<span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
</a>
but this time the navigation won't succeed because there will be a mismatch between outlets. It will fail at (2)
, where the route's outlet is primary
, but the outlet according to the current UrlSegmentGroup
at that moment is test
.
First of all, I noticed that the CrisisCenterComponent
doesn't do much:
<h2>CRISIS CENTER</h2>
<router-outlet></router-outlet>
so, we'll get rid of it. This is what we'll be left with:
crisis-center-routing.module.ts:
const crisisCenterRoutes: Routes = [
{
// path: '',
// component: CrisisCenterComponent,
// children: [
// {
path: '',
component: CrisisListComponent,
children: [
{
path: ':id',
component: CrisisDetailComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
crisis: CrisisDetailResolverService
},
outlet: 'test'
},
{
path: '',
component: CrisisCenterHomeComponent
}
]
}
// ]
// }
]
Now, since CrisisListComponent
acts as a container, we can do the following replacement:
crisis-center-routing.module.ts:
const crisisCenterRoutes: Routes = [
{
// path: '',
// component: CrisisCenterComponent,
// children: [
// {
// path: '',
// component: CrisisListComponent,
// children: [
// {
path: ':id',
component: CrisisDetailComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
crisis: CrisisDetailResolverService
},
outlet: 'test'
},
{
path: '',
component: CrisisCenterHomeComponent
}
// ]
// }
// ]
// }
]
app-routing.module.ts
{
path: 'crisis-center',
loadChildren: () =>
import('./crisis-center/crisis-center.module').then(
mod => mod.CrisisCenterModule
),
data: { preload: true },
// !
component: CrisisListComponent
},
So, there won't be an additional ''
(which can cause problems sometimes, as we've seen) to match.
And the routerLink
in the view will remain the same:
<a [routerLink]="[{ outlets: { test: [ crisis.id] } }]">
<span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
</a>
As a side note, now when you select one crisis center, the CrisisCenterHomeComponent
will be active too. If you want to avoid that, you can do this:
crisis-center-routing.module.ts:
/* ... */
{
path: ':id'
component: CrisisDetailComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
crisis: CrisisDetailResolverService
},
outlet: 'test'
},
{
path: '',
// Uncomment this if you want these 2 Route objects
// to be mutually exclusive(either one of them, not both at the same time)
pathMatch: 'full',
component: CrisisCenterHomeComponent
}
/* ... */
1: Angular Router: Getting to know UrlTree, ActivatedRouteSnapshot and ActivatedRoute
If you want to read more about Angular Router and its inner workings/features, I'd recommend: