angularangular-ui-routerangular-routerrouter-outlet

How can I mix primary and named outlets in nested navigation?


Given the following routes:

export const routes: Routes = [
  { path: "test-1/:name", component: TestComponent, title: "Main 1" },
  { path: "test-2/:name", component: TestComponent, title: "Main 2" },
  { path: "test-aux-1/:name", component: TestComponent, title: "Aux 1", outlet: "aux" },
  { path: "test-aux-2/:name", component: TestComponent, title: "Aux 2", outlet: "aux" },
  { path: "nested", children: [
    { path: "n-test-1/:name", component: TestComponent, title: "Nested Main 1" },
    { path: "n-test-2/:name", component: TestComponent, title: "Nested Main 2" },
    { path: "n-test-aux-1/:name", component: TestComponent, title: "Nested Aux 1", outlet: "aux" },
    { path: "n-test-aux-2/:name", component: TestComponent, title: "Nested Aux 2", outlet: "aux" },
  ]}
];

How can I navigate to the two nested routes that use the 'aux' outlet (Nested Aux 1/2) in the same way that the non-nested routes work?

These router links work for the non-nested parts (each route changes its own part - the generated router links contain the full URL so the unaffected parts do not change):

<a [routerLink]="['test-1', 'Main-1']">Main 1</a>
<a [routerLink]="['test-2', 'Main-2']">Main 2</a>
<a [routerLink]="[{ outlets: { aux: ['test-aux-1', 'Aux-1'] }}]">Aux 1</a>
<a [routerLink]="[{ outlets: { aux: ['test-aux-2', 'Aux-2'] }}]">Aux 2</a>

And I expected these to work for their nested counterparts, but the last two give errors:

<a [routerLink]="['nested', 'n-test-1', 'Nested-Main-1']">Nested Main 1</a>
<a [routerLink]="['nested', 'n-test-2', 'Nested-Main-2']">Nested Main 2</a>

<!-- Cannot match any routes. URL Segment: 'nested/n-test-aux-1/Nested-Aux-1' -->
<a [routerLink]="[{ outlets: { aux: ['nested', 'n-test-aux-1', 'Nested-Aux-1'] }}]">Nested Aux 1</a>

<!-- Cannot match any routes. URL Segment: 'nested/n-test-aux-2/Nested-Aux-2' -->
<a [routerLink]="[{ outlets: { aux: ['nested', 'n-test-aux-2', 'Nested-Aux-2'] }}]">Nested Aux 2</a>

Stackblitz: https://stackblitz.com/edit/stackblitz-starters-yrrm5iul?file=src%2Fapp.routes.ts

Updates

Following on from Ian's answer I found the details in the Angular source. The key is this comment:

// Note that we switch to primary when we have a match because outlet configs look like
// this: {path: 'a', outlet: 'a', children: [
//  {path: 'b', component: B},
//  {path: 'c', component: C},
// ]}
// Notice that the children of the named outlet are configured with the primary outlet

Which shows why Ian's solution is correct. So unfortunately (for me!):

  1. The outlet for nested content must be specified on the top level of the routing tree.
  2. If you load nested routes once they are only available under their parent outlet.
  3. If you load the same nested routes multiple times under different outlets all routes are available in all outlets.
  4. Nested routes must always use the primary outlet - the final outlet is determined from its top-level parent.

I'd tried changing the output on the nested level (1) but not changed the final route outlets, so (4) caught me out.

It does not seem possible to have shared config (like that from a module) which has some primary and some auxiliary outputs.

Background

The actual problem I'm trying to solve is for routes lazy-loaded from a module:

export const routes: Routes = [
  ...
  { path: "nested", loadChildren: () => import("./features/nested.module").then(m => m.NestedModule) },
]

So I don't think I can easily change the structure - I'm trying to add an aux outlet to an existing app and really do not want to refactor all the routes.

Also, I think the lazy-loading, and even the sub-module, is a diversion. I spent a heap of time looking at lazy-loading problems (https://github.com/angular/angular/issues/10981 and similar), but then I came back from a lazy module to an eager module, and then finally to the problem above. I think the core of the problem is why the last two routes above don't work - and even if that is not the actual problem with the module I'd love to understand enough about the router to know why they don't.

The only partial success I've had was from this example (https://github.com/angular/angular/issues/10981#issuecomment-301787482), using a main route with a pathless child:

{ path: "nested-3", children: [
  { path: "n3-test-aux-1/:name", children: [
    { path: "", component: TestComponent, title: "Nested 3 Aux 1", outlet: "aux" },
  ]},
]},

<a [routerLink]="['nested-3', 'n3-test-aux-1', 'Nested-3-Aux-1']">Nested 3 Aux 1</a>

But whilst that does set the aux output correctly, it includes a primary route so clears whatever is in the primary output.

So I'm completely stuck. Seems a simple thing to try to do, so would be great if someone could point the way...

Cheers


Solution

  • The changes I made works quite well (to my surprise), however, it needs module management to separate the primary paths from the aux paths. I found the solution by specifying the outlet of the 'nested' path separately as there is just no way that both a primary outlet child path and an aux outlet child path will match using the same urlMatcher function

    in the app.routes.ts file:

    {
        path: 'nested',
        children: [
          {
            path: 'n-test-1/:name',
            component: TestComponent,
            title: 'Nested Test 1',
            outlet: 'primary',
          },
          {
            path: 'n-test-2/:name',
            component: TestComponent,
            title: 'Nested Test 2',
            outlet: 'primary',
          },
        ],
      },
      {
        path: 'nested',
        outlet: 'aux',
        children: [
          {
            path: 'n-test-aux-1/:name',
            component: TestComponent,
            title: 'Nested Test Aux 1',
          },
          {
            path: 'n-test-aux-2/:name',
            component: TestComponent,
            title: 'Nested Test Aux 2',
          },
        ],
      },
    

    there is an outlet for primary (which is the default outlet) and aux (which is the secondary outlet)

    Stackblitz

    for your main problem, just create separate modules for the child paths (for primary and aux), however, I have not tested this code below so I will leave the exploration up to you

    export const routes: Routes = [
      ...
      { path: "nested", loadChildren: () => import("./features/nestedprimary.module").then(m => m.NestedModule) },
      { path: "nested", outlet: 'aux', loadChildren: () => import("./features/nestedaux.module").then(m => m.NestedModule) },
    ]