Skip to main content

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

In order to use breadcrumb, we need to get their names from this route configuration, as in issue-log route is represented as Issue Login 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: ''
                            }
                        }
                    ]
                },
            ]
        }
    ]
}

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>

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: "";
  }
}

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

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

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.pathto 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 : "";
  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: ''
    }
}

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];
}
  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
};
  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;
  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;
}

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);
    })
}

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"
  }
];

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.

Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...