8

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.

4
  • 1
    What if you remove pathMatch: 'full' from admin-routing.module.ts ? If you issue admin/employee, because you're using pathMatch:full, it will not pass that condition, so it won't go to employee, Commented Jul 11, 2020 at 15:05
  • @AndreiGatej You sir are a savior. It worked perfectly. Removing pathMatch from admin-routing module worked as expected. If possible, can you please elaborate the difference between using pathMatch and not using it? Thanks a lot Commented Jul 11, 2020 at 15:08
  • 1
    @JohnPeters In your example, you are using the components directly. In my case, I am lazy loading modules. So, your solution will not work Commented Jul 11, 2020 at 15:09
  • 1
    @AbrarHossain I'm glad it worked! Sure, that will be my pleasure. Commented Jul 11, 2020 at 19:51

2 Answers 2

11

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.

Sign up to request clarification or add additional context in comments.

4 Comments

@AndreiGatej Thanks so much for taking the time to explain this with all the details. It is much more clear now how the path matching algorithm works in Angular. Till now, it was all like a black box to me!
@AbrarHossain You’re welcome! If you have other uncertainties regarding the Angular Router, please do share them. I plan on writing on article where I’d explain common problems or why some solutions work the way they do.
@AndreiGatej I have often stumbled upon change detection and had issues regarding that. I was able to get past it by invoking the change detector manually but I think I still lack understanding of context where it is actually helpful and where it is unnecessary and can be avoided altogether by better design approach. If you could explain that, it would be great for me
@AbrarHossain here's a great resource for that.
2

if you need to use different UI for different module you can achieve it by defining parent component in app-routing.module.ts.

app-routing.module.ts.

    const routes: Routes = [
      {
        path: '',
        component: AdminComponent, //you can set different UI like navigation/header 
        children: [
         { path: '',redirectTo: 'dashboard',pathMatch: 'full'}, //set default redirect
         { path: 'dashboard', loadChildren: () =>import('./../dashboard/dashboard.module').then(m => m.DashboardModule) },
         { 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) },
        ]
      },
      {
        path: '',
        component: AuthComponent, //different UI for child components, without navigation
        children: [
         { path: 'maintenance',loadChildren: () => import('./../maintenance/maintenance.module').then(m => m.MaintenanceModule) },
         { path: 'login',loadChildren () => import('./../authentication/authentication.module').then(m => m.AuthenticationModule) },
         { path: 'landing',loadChildren: () => import('./../landing/landing.module').then(m => m.LandingModule) }
        ]
      }

Your sub modules e.g: admin/employee/:id will render UI of parent(e.g : AdminComponent),

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.