Search code examples
angularangular-routingangular-router

Nested Routing in Angular


This maybe a common question and if there are better answers please point me towards it. That being said, here's the issue:

On the top level, the angular app I am developing exposes a login path and paths to 4 separate dashboards depending on who logs in. This part is working as expected.

For each dashboard, I have a side navigation that is more or less same (some options are only shown for certain types of user). In the dashboard, I have a nested router outlet. Whenever I try to load a module inside the nested outlet, Angular can't match the path. Here's what I have so far:

app-routing.module.ts

const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'login' },
  { path: 'login', loadChildren: () => import('./modules/auth/auth.module').then(m => m.AuthModule) },
  //{ path: 'dashboard', loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule) }
  { path: 'admin', loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule) },
];

admin-routing.module.ts


const routes: Routes = [
  { path: '', pathMatch: 'full', component: AdminComponent, children: [
    { path: 'employee', loadChildren: () => import('./../employee/employee.module').then(m => m.EmployeeModule) },
    { path: 'attendance', loadChildren: () => import('./../attendance/attendance.module').then(m => m.AttendanceModule) },
    { path: 'customer', loadChildren: () => import('./../customer/customer.module').then(m => m.CustomerModule) },
    { path: 'order', loadChildren: () => import('./../order/order.module').then(m => m.OrderModule) },
    { path: 'expense', loadChildren: () => import('./../expense/expense.module').then(m => m.ExpenseModule) },
  ]},
];

app.component.html

<router-outlet></router-outlet>

admin.component.html

<mat-drawer-container mode="side" id="dashboard-drawer-container" hasBackdrop="false">
  <mat-drawer #drawer id="sidenav-container">
    <app-sidenav></app-sidenav>
  </mat-drawer>
  <div id="dashboard-container">
    <router-outlet></router-outlet>
  </div>
</mat-drawer-container>

Now the expected behavior is as follows:

  1. When navigated to /admin, the AdminComponent will be rendered and the sidenav will be visible
  2. When a link is clicked on the sidenav, the content should be rendered in the nested router in the admin component (for example, admin/employee)
  3. When other routes are accessed inside the module loaded in (2), it should be rendered inside an outlet in that module (for example, admin/employee/:id) for employee detail page where, employee module has a nested router

I tried with named outlets and it kept throwing error. If I move the children out of admin routes and make them independent routes, it kind of works but, the content is rendered on the outermost (app) router outlet and the sidenav is not rendered.

Any help or suggestion will be greatly appreciated.


Solution

  • Let's break the problem into a small one:

    app.module.ts

    const routes: Routes = [
      {
        path: '',
        // pathMatch: 'full',
        children: [
          {
            path: 'foo',
            component: FooComponent
          },
        ],
      },
      {
        path: '**',
        component: NotFoundComponent,
      }
    ];
    

    app.component.html

    <router-outlet></router-outlet>
    
    <button routerLink="/foo">go to foo</button>
    

    ng-run demo.


    If we click on the button, the Angular Router will schedule a route transition. This involves a quite interesting process which is composed of multiple phases.

    One of these phases is the one called Apply Redirects and it's where the redirects are resolved and where NoMatch errors come from. This is also where we can find more about pathMatch: 'full'.
    In this phase it will go through each configuration object and will try to find the first one that matches with the issued url(e.g /foo).

    It will first encounter this one(it happens in matchSegmentAgainstRoute):

    {
      path: '',
      // pathMatch: 'full',
      children: [
        {
          path: 'foo',
          component: FooComponent
        },
      ],
    },
    

    then, the match function will be invoked:

    const {matched, consumedSegments, lastChild} = match(rawSegmentGroup, route, segments);
    

    here, we stop at route.path === '':

    if (route.path === '') {
      if ((route.pathMatch === 'full') && (segmentGroup.hasChildren() || segments.length > 0)) {
        return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
      }
    
      return {matched: true, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
    }
    

    So, here is one case where pathMatch option makes the difference. With the current configuration(pathMatch not set),

    return {matched: true, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
    

    will be reached and then it will proceed to go through the children array. So, in this case, the FooComponent will successfully be displayed.

    But, if we have pathMatch: 'full', then the expression

    if ((route.pathMatch === 'full') && (segmentGroup.hasChildren() || segments.length > 0)) { }
    

    will be true, because segments.length > 0, in this case segments is ['foo']. So, we'd get matched: false, which means the FooComponent won't appear in the view.