Search code examples
angularrouterangular-routingangular-routerrouter-outlet

Passing parameters in named outlets is not working


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


Solution

  • EDIT

    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.


    TLDR;

    Working demo.


    Detailed explanation

    Why it didn't work in the first place

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

    A solution

    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: