Search code examples
angularangular-ui-routerlazy-loadingbreadcrumbs

Angular Router Breadcrumbs with Lazy Loaded Modules


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 {}

Solution

  • 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!