Named-Outlets Require Non-Empty Parent Route-Segment Paths In Angular 4.4.4
One of the more interesting features in the Angular Router is that it allows for secondary routes to be rendered alongside your primary layout routes. These secondary routes are rendered inside <router-outlet> elements, just as the primary routes are; however, these router-outlets and their route definitions must be "named." And, as it turns out - at least in Angular 4.4.4 - these secondary routes must also have non-empty parent route-segmenets.
Run this demo in my JavaScript Demos project on GitHub.
First, I wanted to give a big "Thank You" to the following people who helped me think this through and understand and identify this constraint within the router:
In the Angular Router, it's not uncommon to define a route segment whose path value is "". This allows you to wrap set of children segments in a common Component without having to represent that common component in the URL. From the Angular documentation:
The Router supports empty path routes; use them to group routes together without adding any additional path segments to the URL.
At this time, however, such empty-path wrappers cannot be used if one of the direct descendants of the wrapper is a secondary route. All secondary routes must be directly contained within a non-empty parent path segment.
To demonstrate this, let's look at a working example. In the following application Module route definitions, I have a wrapper component - "main" - that contains two sibling layout options and a secondary route for a chat widget. Notice that that secondary chat route is explicitly providing an outlet name, "chat."
// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Routes } from "@angular/router";
// Import the application components and services.
import { AppComponent } from "./app.component";
import { ChatComponent } from "./chat.component";
import { LayoutAComponent } from "./layout-a.component";
import { LayoutBComponent } from "./layout-b.component";
import { LayoutWrapperComponent } from "./layout-wrapper.component";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var routes: Routes = [
{
// All routes in the application will share the "app" prefix.
// --
// NOTE: This shared prefix is here just to demonstrate that a non-empty ancestor
// path is not sufficient for getting named-outlets to work. It's a direct-parent
// segment kind of constraint.
path: "app",
children: [
{
// CAUTION: In order for the NAMED OUTLET child route to work (chat),
// its parent segment must contain a non-empty path. As such, we're
// using "main" for this wrapper component in order to ensure a non-
// empty value.
path: "main",
component: LayoutWrapperComponent,
children: [
{
path: "layout-a",
component: LayoutAComponent
},
{
path: "layout-b",
component: LayoutBComponent
},
{
outlet: "chat", // <--- Named outlet.
path: "open",
component: ChatComponent
}
]
}
]
},
{
path: "**",
redirectTo: "/app/main/layout-a"
}
];
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@NgModule({
bootstrap: [
AppComponent
],
imports: [
BrowserModule,
RouterModule.forRoot(
routes,
{
// Tell the router to use the HashLocationStrategy.
useHash: true
}
)
],
declarations: [
AppComponent,
ChatComponent,
LayoutAComponent,
LayoutBComponent,
LayoutWrapperComponent
],
providers: [
// CAUTION: We don't need to specify the LocationStrategy because we are setting
// the "useHash" property in the Router module above.
// --
// {
// provide: LocationStrategy,
// useClass: HashLocationStrategy
// }
]
})
export class AppModule {
// ...
}
The wrapper component, associated with the "main" segment, then defines both the primary router-outlet and the secondary router-outlet named "chat":
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "layout-wrapper",
styleUrls: [ "./layout-wrapper.component.css" ],
template:
`
<p>
<a routerLink="./layout-a">Layout A</a> —
<a routerLink="./layout-b">Layout B</a> —
<strong>Chat:</strong>
<a [routerLink]="[{ outlets: { chat: 'open' } }]">Open</a> /
<a [routerLink]="[{ outlets: { chat: null } }]">Close</a>
</p>
<!-- The PRIMARY outlet. -->
<router-outlet></router-outlet>
<!-- The NAMED outlet for the CHAT widget. -->
<router-outlet name="chat"></router-outlet>
`
})
export class LayoutWrapperComponent {
// ...
}
Now, if we run this application in the browser, navigate to "Layout A", and then click to open the Chat widget, we are taken to the following route:
#/app/main/(layout-a//chat:open)
As you can see, Angular creates two divergent routes under the "main" parent path segment - one for the primary layout route and one for our secondary "chat" route.
Now that we have a working solution, let's go back to our route configuration and replace the "main" segment with an empty string. This will make the direct parent segment of the secondary route an empty path:
// ....
var routes: Routes = [
{
// All routes in the application will share the "app" prefix.
// --
// NOTE: This shared prefix is here just to demonstrate that a non-empty ancestor
// path is not sufficient for getting named-outlets to work. It's a direct-parent
// segment kind of constraint.
path: "app",
children: [
{
// CAUTION: THIS WILL BREAK. In order for secondary, named routes to work
// (such as our Chat outlet), they must have non-empty parent route path
// segments. Since this parent is "", the secondary route will break.
path: "",
component: LayoutWrapperComponent,
children: [
{
path: "layout-a",
component: LayoutAComponent
},
{
path: "layout-b",
component: LayoutBComponent
},
{
outlet: "chat", // <--- Named outlet.
path: "open",
component: ChatComponent
}
]
}
]
},
{
path: "**",
redirectTo: "/app/layout-a"
}
];
// ....
Now, if we try to do the same thing - navigate to "Layout A" and the open the Chat widget, we get the following output:
NOTE: You can see this much better in the video.
As you can see, the Chat widget won't render at all. In fact, when clicking on the open link, the chat segment doesn't even appear in the browser URL.
Secondary routes are a tricky thing to think about because they seem to be, at the same time, both decoupled from the primary layout routes; and yet, still part of the primary route configuration hierarchy. Part of this relationship requires that secondary routes be housed within non-empty parent route segments. Otherwise, it seems that Angular has trouble resolving the secondary routes within the nested router-outlet elements.
Want to use code from this post? Check out the license.
Reader Comments
@All,
Here is the discussion about this issue / behavior in GitHub - https://github.com/angular/angular/issues/14662
Excelent! thanks to taking time to wrote this and share.
@Diego,
My pleasure. And, for what it's worth, this still seems to be a requirement, even in Angular 5. I don't think they can get around this, as I think it may make the router-tree too ambiguous (just my guess). I bring this up only because I ran into this issue again _just this week_ and I'm on the latest Angular.
This issue was so hard to find.
Strange thing is that, it's not mentioned any where in the angular docs.
You know how I found this article, I had to use Augury chrome extension which said, I have emptyOutletComponent only then I got hold of things.
It's great, but I have a little advanced issues. I have two level deep lazy loaded modules with auxilary/named routes. The first level works, on second levels routes are recognized but the outlet remains empty.
@Samiullah,
Unfortunately, I haven't even experimented with lazy-loaded modules yet :( But, I can share your concern over some of the routing complexities. In fact, just the other day, I ran into something really strange that I haven't codified in a demo yet. If I have a secondary route, its open/closed status actually changes the way relative navigation works in the primary route. Meaning, if I have something like this
routerLink="../../"
in my primary outlet, the behavior is different depending on whether or not a sibling secondary route is present.Supporting multiple router outlets is extremely awesome. But, to be sure, there be dragons!
Hi, i am facing a trouble with this issue and created a stackblitz https://stackblitz.com/edit/angular-esjv9e?file=src%2Fapp%2Ffeature%2Ffeature.module.ts could you please provide a workaround to make it work. Currently only /feature/ path is loading component into the panel outlet. I tried almost everything, but couldnot make it work
@Bulat,
Hmm, I've never tried to render the same component into two different outlets at the same time. I am not sure what the correct
routerLink
syntax for that would be. Usually, for the primary outlet, i use the vanillarouterLink
syntax (as you have). But, for secondary outlets, I use the more object-based syntax. I tried switching to that in your demo, but it didn't seem to work either. I am not sure what you can do.