DEV Community

Zhiyue Yi
Zhiyue Yi

Posted on • Updated on

Create a Simple Breadcrumb in Angular

NOTE: This article has been archived since it was written in 2018. Now this solution may not work with the latest Angular version. You may continue reading if you would like to see the idea behind it, but may not want to follow the implementation as it is already outdated. Thanks!

Visit my Blog for the original post: Create a Simple Breadcrumb in Angular

Recently, I am building an enterprise resource planning (ERP) platform for my company. The system is required to be flexible to hold different individual modules. In this platform, user navigation should be clear and concise so that the users would conveniently know what location they are at while performing tasks on the platforms.

For example, a hierarchy like Dashboard -> IT HelpDesk -> Issue Log -> New can be provided as a reference of locations. And most importantly, users can navigate back to different level of pages conveniently. So I built a breadcrumb component to cater that need.

Demo for static link:
Imgur

Demo for dynamic link (123 is a dynamic ID):
Imgur

Configure the Routes

Af first, you need to configure your route correctly.

Take Dashboard -> IT HelpDesk -> Issue Log -> New as an example. Below code snippet shows a basic route structure.

{
    path: '',
    component: LoginComponent,
}, {
    path: 'dashboard',
    component: DashboardComponent,
    children: [
        {
            path: 'it-helpdesk',
            component: ItHelpdeskComponent,
            children: [
                {
                    path: 'issue-log',
                    children: [
                        {
                            path: '',
                            component: IssueLogListComponent
                        },
                        {
                            path: 'new',
                            component: IssueLogDetailComponent
                        },
                        {
                            path: ':id',
                            component: IssueLogDetailComponent
                        }
                    ]
                }
            ]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

In order to use breadcrumb, we need to get their names from this route configuration, as in issue-log route is represented as Issue Log in the breadcrumb. Then we use data attribute in Route to store its display names. Hence, we modify the route configuration as below.

{
    path: '',
    component: LoginComponent,
}, {
    path: 'dashboard',
    component: DashboardComponent,
    data: {
        breadcrumb: 'Dashboard',
    },
    children: [
        {
            path: 'it-helpdesk',
            component: ItHelpdeskComponent,
            data: {
                breadcrumb: 'IT Helpdesk'
            },
            children: [
                {
                    path: 'issue-log',
                    data: {
                        breadcrumb: 'Issue Log'
                    },
                    children: [
                        {
                            path: '',
                            component: IssueLogListComponent
                        },
                        {
                            path: 'new',
                            component: IssueLogDetailComponent,
                            data: {
                                breadcrumb: 'New'
                            }
                        },
                        {
                            path: ':id',
                            component: IssueLogDetailComponent,
                            data: {
                                breadcrumb: ''
                            }
                        }
                    ]
                },
            ]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Notice that the route issue-log/:id has no breadcrumb data yet. That is because this route contains dynamic parameters. We will automate the display text later when building the breadcrumb.

Breadcrumb Component

HTML

The HTML part is rather simple. Just use ol and li to list out all the breadcrumbs with *ngFor

breadcrumb.component.html

<ol class="breadcrumb">
  <li *ngFor="let breadcrumb of breadcrumbs">
    <span [routerLink]="breadcrumb.url" routerLinkActive="router-link-active">
      {{ breadcrumb.label }}
    </span>
  </li>
</ol>
Enter fullscreen mode Exit fullscreen mode

SCSS

The CSS is not complicated either. Take note that when a breadcrumb is hovered, it should be dimmed.

breadcrumb.component.scss

.breadcrumb {
  background: none;
  font-size: 0.8em;
  margin: 0;
  a,
  span {
    color: darkgrey;
  }
  a:hover,
  span:hover {
    color: dimgrey;
    text-decoration: none;
  }
  li {
    list-style: none;
    float: left;
    margin: 5px;
  }
  li:last-child {
    margin-right: 20px;
  }
  li::after {
    content: "->";
    color: darkgrey;
  }
  li:last-child::after {
    content: "";
  }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript

The most important part is the TypeScript part.

Interface

The first thing to do is to create an interface to standardize the data structure of a breadcrumb.

breadcrumb.interface.ts

export interface IBreadCrumb {
  label: string;
  url: string;
}
Enter fullscreen mode Exit fullscreen mode

Component

Then we can start to build our breadcrumb component. The basic code structures are as below.

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, NavigationEnd } from '@angular/router';
import { IBreadCrumb } from '../../../interfaces/breadcrumb.interface';
import { filter, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'app-breadcrumb',
  templateUrl: './breadcrumb.component.html',
  styleUrls: ['./breadcrumb.component.scss']
})
export class BreadcrumbComponent implements OnInit {
  public breadcrumbs: IBreadCrumb[]

  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
  ) {
    this.breadcrumbs = this.buildBreadCrumb(this.activatedRoute.root);
  }

  ngOnInit() {
    // ... implementation of ngOnInit
  }

  /**
   * Recursively build breadcrumb according to activated route.
   * @param route
   * @param url
   * @param breadcrumbs
   */
  buildBreadCrumb(route: ActivatedRoute, url: string = '', breadcrumbs: IBreadCrumb[] = []): IBreadCrumb[] {
    // ... implementation of buildBreadCrumb
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we have 2 functions need to be implemented.

ngOnInit() is the function triggered right when the component is created. In this function, we will get the current route and start to build breadcrumb from its root.

buildBreadCrumb() is the function we actually build a breadcrumb. It's a recursive function to recursively loop the child of route object from the root to leaf, such as Dashboard all the way to Issue Log.

buildBreadCrumb()

  1. Label and Path First, let's get the label and path of a single breadcrumb. Note that routeConfig could be null if the current route is on the root. Therefore, it must be checked before assign route.routeConfig.data.breadcrumb and route.routeConfig.path to variables, otherwise, exceptions will be thrown.
let label =
  route.routeConfig && route.routeConfig.data
    ? route.routeConfig.data.breadcrumb
    : "";
let path =
  route.routeConfig && route.routeConfig.data ? route.routeConfig.path : "";
Enter fullscreen mode Exit fullscreen mode
  1. Handling Dynamic Parameters Second, we need to handle dynamic route such as :id. Take a look at this route.
{
    path: 'issue-log/:id',
    component: IssueLogDetailComponent
    data: {
        breadcrumb: ''
    }
}
Enter fullscreen mode Exit fullscreen mode

The breadcrumb is previously left blank because the route is dynamic. I can only know the ID at runtime.

The activated route contains the actual ID. Hence, we shall dynamically attach the actual ID to the breadcrumb by taking the last route part and checking if it starts with :. If so, it is a dynamic route, then we get the actual ID from route.snapshot.params with its parameter name paramName.

const lastRoutePart = path.split("/").pop();
const isDynamicRoute = lastRoutePart.startsWith(":");
if (isDynamicRoute && !!route.snapshot) {
  const paramName = lastRoutePart.split(":")[1];
  path = path.replace(lastRoutePart, route.snapshot.params[paramName]);
  label = route.snapshot.params[paramName];
}
Enter fullscreen mode Exit fullscreen mode
  1. Generate Next URL

In every recursive loop of route, the path is fragment and a complete path is not available, such as issue-log instead of dashboard/it-helpdesk/issue-log. Therefore, a complete path needs to be re-build and attach to the breadcrumb in the current level.

const nextUrl = path ? `${url}/${path}` : url;

const breadcrumb: IBreadCrumb = {
  label: label,
  url: nextUrl
};
Enter fullscreen mode Exit fullscreen mode
  1. Add Route with Non-empty Label and Recursive Calls

In your application, there may be some routes which does not have breadcrumb set and these routes should be ignored by the builder.

Next, if the current route has children, that means that this route is not the leaf route yet and we need to continue to make a recursive call the build next-level route.

const newBreadcrumbs = breadcrumb.label
  ? [...breadcrumbs, breadcrumb]
  : [...breadcrumbs];
if (route.firstChild) {
  //If we are not on our current path yet,
  //there will be more children to look after, to build our breadcumb
  return this.buildBreadCrumb(route.firstChild, nextUrl, newBreadcrumbs);
}
return newBreadcrumbs;
Enter fullscreen mode Exit fullscreen mode
  1. Full Picture of buildBreadCrumb()
/**
 * Recursively build breadcrumb according to activated route.
 * @param route
 * @param url
 * @param breadcrumbs
 */
buildBreadCrumb(route: ActivatedRoute, url: string = '', breadcrumbs: IBreadCrumb[] = []): IBreadCrumb[] {
    //If no routeConfig is avalailable we are on the root path
    let label = route.routeConfig && route.routeConfig.data ? route.routeConfig.data.breadcrumb : '';
    let path = route.routeConfig && route.routeConfig.data ? route.routeConfig.path : '';

    // If the route is dynamic route such as ':id', remove it
    const lastRoutePart = path.split('/').pop();
    const isDynamicRoute = lastRoutePart.startsWith(':');
    if(isDynamicRoute && !!route.snapshot) {
      const paramName = lastRoutePart.split(':')[1];
      path = path.replace(lastRoutePart, route.snapshot.params[paramName]);
      label = route.snapshot.params[paramName];
    }

    //In the routeConfig the complete path is not available,
    //so we rebuild it each time
    const nextUrl = path ? `${url}/${path}` : url;

    const breadcrumb: IBreadCrumb = {
        label: label,
        url: nextUrl,
    };
    // Only adding route with non-empty label
    const newBreadcrumbs = breadcrumb.label ? [ ...breadcrumbs, breadcrumb ] : [ ...breadcrumbs];
    if (route.firstChild) {
        //If we are not on our current path yet,
        //there will be more children to look after, to build our breadcumb
        return this.buildBreadCrumb(route.firstChild, nextUrl, newBreadcrumbs);
    }
    return newBreadcrumbs;
}
Enter fullscreen mode Exit fullscreen mode

ngOnInit()

Finally, we need to implement ngOnInit() to trigger to start building the breadcrumbs.

Breadcrumb build should start when a router change event is detected. To detect it, we use RxJs to observe the changes.

ngOnInit() {
    this.router.events.pipe(
        filter((event: Event) => event instanceof NavigationEnd),
        distinctUntilChanged(),
    ).subscribe(() => {
        this.breadcrumbs = this.buildBreadCrumb(this.activatedRoute.root);
    })
}
Enter fullscreen mode Exit fullscreen mode

The above code snippet indicates that the router events are observed with a filter on the event type to be NavigationEnd and a distinct change.

That means if the route is changing and the new value is different from the previous value, then the breadcrumb will start to build. The results of recursive function will be stored in this.breadcrumb, which will be an array as below.

[
  {
    label: "Dashboard",
    url: "/dashboard"
  },
  {
    label: "IT Helpdesk",
    url: "/dashboard/it-helpdesk"
  },
  {
    label: "Issue Log",
    url: "/dashboard/it-helpdesk/issue-log"
  },
  {
    label: "plfOR05NXxQ1",
    url: "/dashboard/it-helpdesk/issue-log/plfOR05NXxQ1"
  }
];
Enter fullscreen mode Exit fullscreen mode

Conclusion

Breadcrumbs implement a rather simple algorithm, but I think what makes it confusing is its configurations. As developers, you need to know where the configurations should be done and the features Angular provide. With good understanding of Angular, you can implement some components easily as most of the tools you need have been provided by Angular.

You may refer to the full code here: GitHub

Thanks for reading~

Discussion (20)

Collapse
hachirsara2020 profile image
hachirsara2020

thank you for this tuto .
but I getting thir error and I don't know how resolve it:
Types of parameters 'source' and 'source' are incompatible.
Type 'import("C:/Users/asus/breadcrumb/node_modules/rxjs/internal/Observable").Observable' is not assignable to
type 'import("C:/Users/asus/breadcrumb/node_modules/rxjs/internal/Observable").Observable'.
Type 'import("C:/Users/asus/breadcrumb/node_modules/@angular/router/router").Event' is not assignable to type 'Event'.
Type 'ActivationEnd' is missing the following properties from type 'Event': bubbles, cancelBubble, cancelable, composed, and 18 more.

24         filter((event: Event) => event instanceof NavigationEnd),
Enter fullscreen mode Exit fullscreen mode
Collapse
larsvonqualen profile image
Lars von Qualen

Either import the generic event from @angular/router:

import { Router, ActivatedRoute, NavigationEnd, Event } from '@angular/router';
Enter fullscreen mode Exit fullscreen mode

or change the filter to infer the type automatically:

filter(event => event instanceof NavigationEnd),
Enter fullscreen mode Exit fullscreen mode
Collapse
paco_ita profile image
Francesco Leardini • Edited on

Using RxJS (6.6.3) and ESLint (7.10.0 with default rules) the code above fails:

Collapse
zhiyueyi profile image
Zhiyue Yi Author

It could be different rxjs version. When I wrote this article, I used rxjs 5.5.6 version.

Collapse
gtbhopale2412 profile image
gtbhopale2412

Hi I have tried the above code in my application. Right now i have home component which having link change password which will redirect to change password page. So i have added bewlo route configuration like:
{
path:'home',component:HomeComponent,children: [{
path: 'changepassword',
component:ChangePasswordComponent,
data:{
breadcrumb: 'Change Password'
}
}

],
canActivate:[AuthGuard],
data: { breadcrumb: 'Home' },

},

So my purpose is when i click the change password link the breacrumb should be Home/Change Password. But currently is is not behaving as per my need. Can you help me how i can achieve? Or am i missing anything? Because if i redirects from home to change password the this.breadcrumbs array only holds single element.

Collapse
zhiyueyi profile image
Zhiyue Yi Author

It's easier for us to help you if you could provide a github repo so that we can replicate the issue. :)

Collapse
lionchi profile image
lionchi

Your breadcrumbs are not displayed correctly or the component is not drawn ?

Collapse
gtbhopale2412 profile image
gtbhopale2412

see when I click on the change password link from my home component , only Change Password link is getting shown into the breadcrumb,not link like Home/Change Password.

I wanted the breadcrumb-like: Home/Change Password

Thread Thread
lionchi profile image
lionchi

Have you changed the breadcrumb component code ? You can show the html and the component code itself as a screenshot

Thread Thread
gtbhopale2412 profile image
gtbhopale2412 • Edited on

No,i did not change any single line of code for the breadcrumb component. Even I did not change HTML code also. I have only copy-pasted the same things. I also created the interface. Whatever the route configuration I did for the children is it right? which i have added above.

I am following each and every step given here.

Collapse
jennycoca profile image
jennycoca

I tried your breacrumbs and works fine, but I am trying to get the param id from the route (this.activatedRoute.snapshot.params.id;) in other component, but it's always undefined. In your example, it will be in the issue-log-detail.component.ts.

Collapse
lionchi profile image
lionchi

Why is the call to buildBreadCrumb called from the constructor and component lifecycle method? If you remove from the designer, then the breadcrumbs are not displayed? It’s not clear to everyone why this is happening.

Collapse
zhiyueyi profile image
Zhiyue Yi Author

The buildBreadCrumb in the constructor is the initial build for breadcrumb when the application is loaded.

The buildBreadCrumb in onInit is actually in a subscription to a router change. It means that the breadcrumb is always re-built when there is a router change. Because if some modules are lazy loaded, you wouldn't have their routes in the beginning.

Hope that clarifies :)

Collapse
johanmatevosyan profile image
Hovhannes Matevosyan

Thanks for interesting post. Could you give any hint how to implement breadcrumbs within lazy loaded module?
For example if my module loads child components like below then the route gets nullified because of router-outlet directive which I have in UserComponent (root component of a lazy loaded module).
After that the first child points to the root of a entire project but not a module.
const routes: Routes = [
{
path: '', // route gets nullified here
component: UserComponent,
data: {
breadcrumb: 'User',
},
children: [
{
path: 'account',
component: AccountComponent,
data: {
breadcrumb: 'Account info',
},
}
]

}
]

Collapse
zhiyueyi profile image
Zhiyue Yi Author

It should be able to handle it, since the breadcrumb is always re-built when there is a router change.

So when you load the modules in, at the same time your router should have changed. Then the subscription is triggered, and lastly the breadcrumb is re-built.

Hope it helps :)

Collapse
gauravdarkslayer profile image
Gaurav bhojwani

Its not working correctly in lazy loaded components or child components.

Collapse
jcmelchorp profile image
Ju1i0 M31ch0r

Great post! very usefull. How complicated would be include icons like "home" and so..?

Collapse
jeromeburce11 profile image
JeromeBurce • Edited on

thanks for the info.
I have this issue in my .subscribe in ngOnInit

Collapse
jeromeburce11 profile image
JeromeBurce

solve already

Collapse
javims95 profile image
javims95

Thank you very much, it works perfectly. The only thing you have to import the event to not get failure.