Angular Router Breadcrumbs with Lazy Loaded Modules

CodersCreed picture CodersCreed · Sep 7, 2017 · Viewed 7.8k times · Source

I am working with using the Angular router to dynamically add breadcrumbs. I have followed several examples and have gotten them to successfully work.

However, if I attempt incorporate a lazy-loaded module, I get the error:

Root segment cannot have matrix parameters

I have researched that issue and have not been able to find satisfactory information/fixes.

I've created a plunk with what I am trying to accomplish to save from extensive amounts of code on this page: https://plnkr.co/edit/505x2r

How can I continuing utilizing the dynamic breadcrumb creation from the router, but use lazy-loaded routes as well.

import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';

import { RootComponent } from 'src/root/root.component';
import { IndexComponent } from 'src/index/index.component';
import { SignupComponent } from 'src/signup/signup.component';
import { SigninComponent } from 'src/signin/signin.component';

@NgModule({
    imports: [
        RouterModule.forChild([
            {
            path: '',
            component: RootComponent,
            children: [
                {
                    path: 'signin',
                    loadChildren: 'src/signin/signin.module#SigninModule'
                },
                {
                    path: 'signup',
                    component: SignupComponent,
                    data: {
                        breadcrumb: 'Sign Up'
                    }
                },
                {
                    path: '',
                    component: IndexComponent
                }
            ]
        }
    ])
],
exports: [
    RouterModule
]
})
export class RootRoutingModule {}

Answer

CodersCreed picture CodersCreed · Sep 13, 2017

Thank you to Robert, Alex Beugnet, and DAG for their comments and getting me on the right track to a solid solution!

The final plunk is here: https://plnkr.co/edit/iedQjH?p=preview

Here are the condensed issues I ran into:

First, I had lazy loaded routes where I had data on the route in the form of breadcrumb. When running through getBreadcrumbs, the breadcrumb label would show twice. I resolved this by adding the following lines:

  if (
    child.snapshot.url.map(segment => segment.path).length === 0
  ) {
      return this.getBreadcrumbs(child, url, breadcrumbs);
  }

Second, I had routes that were simply a parameter input, but I needed to have it listed on the breadcrumb trail. This was resolved by mapping the child.snapshot.url and assigning the segment to the label.

const routeArray = child.snapshot.url.map(segment => segment.path);
for ( let i = 0; i < routeArray.length; i++ ) {
    label = routeArray[i];
}

Third, I needed to show the parameter as well as the label for next route whether it was loaded immediately or lazy loaded via a module. I also ran into the following paths which needed to be split and labels assigned appropriately:

{
    path: ':id/products',
    loadChildren: 'src/products/products.module#ProductsModule',
    data: {
      breadcrumb: 'Products'
    }
}

and

                {
                  path: 'orders/:id',
                  component: OrderComponent,
                  data: {
                    breadcrumb: 'Orders'
                  }
                },

The solution to both was:

  for ( let i = 0; i < routeArray.length; i++ ) {
    if ( Object.keys(child.snapshot.params).length > 0 ) {
      if ( this.isParam(child.snapshot.params, routeArray[i]) ) {
        label = routeArray[i];
      } else {
        label = child.snapshot.data[ROUTE_DATA_BREADCRUMB];
      }
    } else {
      label = child.snapshot.data[ROUTE_DATA_BREADCRUMB];
    }
    const routeURL = routeArray[i];
    url += `/${routeURL}`;
    const breadcrumb: BreadCrumb = {
      label: label,
      params: child.snapshot.params,
      url: url
    };
    breadcrumbs.push(breadcrumb);
  }

private isParam(params: Params, segment: string) {
  for ( const key of Object.keys(params)) {
    const value = params[key];
    if ( value === segment ) {
      return true;
    }
  }
  return false;
}

With all good things, there one catch: YOU HAVE TO HAVE A ROUTE FOR EACH PARAMETER.

For example, if you have url: #/quotes/123456/products/123456/Generic/Tissue, you will need the following routes:

{
  path: ':id',
  component: ProductComponent,
  data: 'data'
},
{
  path: ':id/:make',
  component: ProductComponent,
  data: 'data'
},
{
  path: ':id/:make/:model',
  component: ProductComponent,
  data: 'data'
}

And you will end up with a clickable breadcrumbs that looks like this:

Home / Quotes / 123456 / Product / 123456 / Generic / Tissue

The project I am working on will only requires me to have one parameter... ever.

In the end, I am able to handle both routes loaded immediately and lazy-loaded routes which was my end-goal. I am sure there are some instances that I have not accounted for, but I feel it's pretty solid.

Regardless, I encourage folks to take what's here and run with it. Shoot me a note with any updates so I can what else this can become! Thx!