Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Mark Drew
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Mark Drew@markdrew )

Named-Outlets Require Non-Empty Parent Route-Segment Paths In Angular 4.4.4

By Ben Nadel on

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> &mdash;
  • <a routerLink="./layout-b">Layout B</a> &mdash;
  •  
  • <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.


 
 
 

 
 Secondary routes require a non-empty parent route path segment. 
 
 
 

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:


 
 
 

 
 Secondary routes won't render without a parent path segment. 
 
 
 

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.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

@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.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.