Brian Love
Angular + TypeScript Developer in Denver, CO

Angular2 Breadcrumb using Router

Reading time ~10 minutes

A breadcrumb has been and still is a standard in user interface navigation on the web. We’ll be implementing a breadcrumb into our Angular 2 application that uses Angular’s Router.

Demo

Check out my demo plunker: https://plnkr.co/edit/aNEoyZ?p=preview

Router

Each application can have a single Router that manages the Routes in your application. You can read more about Angular 2’s Routing and Navigation. To get started, let’s take a quick look at my example routing configuration in the app.routing.module.ts file:

import { ModuleWithProviders } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { IndexComponent } from "./index/index.component";
import { RootComponent } from "./root/root.component";
import { SigninComponent } from "./signin/signin.component";
import { SignupComponent } from "./signup/signup.component";

const routes: Routes = [
  {
    path: "",
    component: RootComponent,
    children: [
      {
        path: "signin",
        component: SigninComponent,
        data: {
          breadcrumb: "Sign In"
        }
      },
      {
        path: "signup",
        component: SignupComponent,
        data: {
          breadcrumb: "Sign Up"
        }
      },
      {
        path: "",
        component: IndexComponent
      }
    ]
  },
];

export const routing: ModuleWithProviders = RouterModule.forRoot(routes);

First, I import the necessary classes from the @angular/core and the @angular/router package. Then, I import the necessary components that are going to be bound to the paths specified for each Route.

Next, I define a constant named routes that is of type Routes, which is simply an array of Route configuration objects. Each route has a path and the component for that path.

In my example application I have 3 children states for my root route:

  1. / - the IndexComponent will display the index.
  2. /signin - the SigninComponent will display a sign in form for my app.
  3. /signup - the SignupComponent will display a sign up form for my app.

These 3 routes are wrapped in the RootComponent. The /signin and the /signup routes both have custom data with a breadcrumb property, which is a string for the label of the breadcrumb link.

My application’s @NgModule declares all of my components as well as my routing:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from "./app.component";
import { BreadcrumbComponent } from './breadcrumb/breadcrumb.component';
import { RootComponent } from "./root/root.component";
import { IndexComponent } from "./index/index.component";
import { SigninComponent } from "./signin/signin.component";
import { SignupComponent } from "./signup/signup.component";
import { routing } from "./app.routing.module";

@NgModule({
  imports: [
    BrowserModule,
    routing
  ],
  declarations: [
    AppComponent,
    BreadcrumbComponent,
    IndexComponent,
    RootComponent,
    SigninComponent,
    SignupComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

Note that I have also defined a BreadcrumbComponent. We’ll get to that in a minute.

Also note that I have imported the exported the routing function from the app.routing.module.ts file.

AppComponent

Ok, so far we have set up some basic routing for our application. Now, let’s look at the AppComponent. The AppComponent will contain both my <breadcrumb> as well as the <router-outlet>.

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Killer App</h1>
    <breadcrumb></breadcrumb>
    <router-outlet></router-outlet>
  `
})
export class AppComponent implements OnInit {
  constructor() {}
  ngOnInit() {}
}

Note that the <router-outlet> is necessary to tell the Router where to place the content of the activated route.

Now that we have our routing configured and our AppComponent containing the shell of our application we are ready to create our breadcrumb navigation. In this example I am using Bootstrap 3’s CSS for the display of the breadcrumb. You may need to customize the template as necessary for your application. Further, I am putting all of the template HTML inline in the @Component decorator data. You should most likely be specifying the templateUrl property instead.

Let’s take a look at the breadcrumb.component.ts file:

import { Component, OnInit } from "@angular/core";
import { Router, ActivatedRoute, NavigationEnd, Params, PRIMARY_OUTLET } from "@angular/router";
import "rxjs/add/operator/filter";

interface IBreadcrumb {
  label: string;
  params: Params;
  url: string;
}

@Component({
  selector: "breadcrumb",
  template: `
    <ol class="breadcrumb">
      <li><a routerLink="" class="breadcrumb">Home</a></li>
      <li *ngFor="let breadcrumb of breadcrumbs">
        <a [routerLink]="[breadcrumb.url, breadcrumb.params]">{{ breadcrumb.label }}</a>
      </li>
    </ol>
  `
})
export class BreadcrumbComponent implements OnInit {

  public breadcrumbs: IBreadcrumb[];

  /**
   * @class DetailComponent
   * @constructor
   */
  constructor(
    private activatedRoute: ActivatedRoute,
    private router: Router
  ) {
    this.breadcrumbs = [];
  }

  /**
   * Let's go!
   *
   * @class DetailComponent
   * @method ngOnInit
   */
  ngOnInit() {
    const ROUTE_DATA_BREADCRUMB: string = "breadcrumb";

    //subscribe to the NavigationEnd event
    this.router.events.filter(event => event instanceof NavigationEnd).subscribe(event => {
      //set breadcrumbs
      let root: ActivatedRoute = this.activatedRoute.root;
      this.breadcrumbs = this.getBreadcrumbs(root);
    });
  }

  /**
   * Returns array of IBreadcrumb objects that represent the breadcrumb
   *
   * @class DetailComponent
   * @method getBreadcrumbs
   * @param {ActivateRoute} route
   * @param {string} url
   * @param {IBreadcrumb[]} breadcrumbs
   */
  private getBreadcrumbs(route: ActivatedRoute, url: string="", breadcrumbs: IBreadcrumb[]=[]): IBreadcrumb[] {
    const ROUTE_DATA_BREADCRUMB: string = "breadcrumb";

    //get the child routes
    let children: ActivatedRoute[] = route.children;

    //return if there are no more children
    if (children.length === 0) {
      return breadcrumbs;
    }

    //iterate over each children
    for (let child of children) {
      //verify primary route
      if (child.outlet !== PRIMARY_OUTLET) {
        continue;
      }

      //verify the custom data property "breadcrumb" is specified on the route
      if (!child.snapshot.data.hasOwnProperty(ROUTE_DATA_BREADCRUMB)) {
        return this.getBreadcrumbs(child, url, breadcrumbs);
      }

      //get the route's URL segment
      let routeURL: string = child.snapshot.url.map(segment => segment.path).join("/");

      //append route URL to URL
      url += `/${routeURL}`;

      //add breadcrumb
      let breadcrumb: IBreadcrumb = {
        label: child.snapshot.data[ROUTE_DATA_BREADCRUMB],
        params: child.snapshot.params,
        url: url
      };
      breadcrumbs.push(breadcrumb);

      //recursive
      return this.getBreadcrumbs(child, url, breadcrumbs);
    }
  }

}

There is a lot here, so let’s break it down.

Lines 1-3:

import { Component, OnInit } from "@angular/core";
import { Router, ActivatedRoute, NavigationEnd, Params, PRIMARY_OUTLET } from "@angular/router";
import "rxjs/add/operator/filter";
  • The ActivatedRoute contains data about the route for the current Component that is being displayed in the <router-outlet>.
  • The NavigationEnd class represents the event when the navigation from one route to another has completed. We’ll use this to filter the stream of events coming from the router so that we can listen for when the navigation has completed.
  • The PRIMARY_OULET is a constant string that represents the route name. This defaults to the string “primary”. In my example application I am not using multiple named routes. I only have a single outlet, or <router-outlet> element, in my template. If you are using multiple named routes in your application we will check if the route is the primary one, and will use this for the breadcrumb.
  • Finally, I import the filter() operator from the ReactiveX library.

Lines 5-9:

interface IBreadcrumb {
  label: string;
  params?: Params;
  url: string;
}
  • I am defining a new interface called IBreadcrumb.
  • A breadcrumb has two required properties: label and url, both of which are strings. The params property is not required and is of type Params. This is used to pass along params in our breadcrumb links.

Lines 11-21:

@Component({
  selector: "breadcrumb",
  template: `
    <ol class="breadcrumb">
      <li><a routerLink="" class="breadcrumb">Home</a></li>
      <li *ngFor="let breadcrumb of breadcrumbs">
        <a [routerLink]="[breadcrumb.url, breadcrumb.params]">{{ breadcrumb.label }}</a>
      </li>
    </ol>
  `
})
  • My component will use the selector “breadcrumb”. If you recall I had a &lt;breadcrumb&gt; element defined in the template of the AppComponent.
  • My template HTML is defined inline. As this is a short example, this works in this case. But, you should most likely use the templateUrl property to define an external .html file.
  • I like to include a static “Home” link in the breadcrumb. This simply uses the RouterLink directive to go back to the root of my application.
  • I then use the NgFor directive to iterate over the breadcrumb objects. These objects will be of type IBreadcrumb - the interface I declared previously.
  • Inside our NgFor loop I output the link to the route for each breadcrumb using the values stored in the IBreadcrumb object. We’ll populate these objects in our controller’s ngOnInit() method.

Lines 24-31:

public breadcrumbs: IBreadcrumb[];

constructor(
  private activatedRoute: ActivatedRoute,
  private router: Router
) {
  this.breadcrumbs = [];
}
  • First, I define a public variable named breadcrumbs that is an array of IBreadcrumb objects.
  • My constructor() function uses dependency injection (DI) to get instances of the ActivatedRoute and the Router.
  • Inside my constructor function I set the breadcrumbs value to an empty array.

Lines 33-43:

ngOnInit() {
  //subscribe to the NavigationEnd event
  this.router.events.filter(event => event instanceof NavigationEnd).subscribe(event => {
    //set breadcrumbs
    let root: ActivatedRoute = this.activatedRoute.root;
    this.breadcrumbs = this.getBreadcrumbs(root);
  });
}
  • In the ngOnInit method we are going subscribe to an Observable for when the navigation has ended in our Router.
  • The Router class has a property named events. The documentation indicates that this “Returns an observable of route events”. These events include: starting the navigation, canceling the navigation, and the end of the navigation (and possibly others).
  • We’ll use the Filter operator from rxjs to filter out any events that are not an instance of the NavigationEnd class. We are only concerned about updating the application’s breadcrumb when the navigation has completed.
  • We then subscribe() to the stream and use the fat-arrow syntax for defining a function that will be invoked whenever the NavigationEnd event occurs.
  • Inside the anonymous function (whose this scope is bound to the BreadcrumbComponent instance) we get the root route for the currently activated route. The root route is the top of the route tree, or the first route if you like to think about it that way.
  • In the getBreadcrumbs() method we will use this root route to recursively determine the primary routes for the currently activate route.

Lines 45-87:

private getBreadcrumbs(route: ActivatedRoute, url: string="", breadcrumbs: IBreadcrumb[]=[]): IBreadcrumb[] {
  const ROUTE_DATA_BREADCRUMB: string = "breadcrumb";

  //get the child routes
  let children: ActivatedRoute[] = route.children;

  //return if there are no more children
  if (children.length === 0) {
    return breadcrumbs;
  }

  //iterate over each children
  for (let child of children) {
    //verify primary route
    if (child.outlet !== PRIMARY_OUTLET) {
      continue;
    }

    //verify the custom data property "breadcrumb" is specified on the route
    if (!child.snapshot.data.hasOwnProperty(ROUTE_DATA_BREADCRUMB)) {
      return this.getBreadcrumbs(child, url, breadcrumbs);
    }

    //get the route's URL segment
    let routeURL: string = child.snapshot.url.map(segment => segment.path).join("/");

    //append route URL to URL
    url += `/${routeURL}`;

    //add breadcrumb
    let breadcrumb: IBreadcrumb = {
      label: child.snapshot.data[ROUTE_DATA_BREADCRUMB],
      params: child.snapshot.params,
      url: url
    };
    breadcrumbs.push(breadcrumb);

    //recursive
    return this.getBreadcrumbs(child, url, breadcrumbs);
  }

  return breadcumbs;
}
  • Here is the bulk of the implementation - where we will recursively go through the routing tree to determine the primary path, obtaining the necessary breadcrumb information at each step in order to return an array of IBreadcrumb object.
  • Note that the url and breadcrumbs arguments are optional, and have default values defined.
  • First I define a constant string named ROUTE_DATA_BREADCRUMB. The value for this is simply “breadcrumb”. Back when I defined our routes in the app.routing.module.ts file I included custom data for most routes. The custom data is an object that has a property named breadcrumb. We can access that custom data to set the label value for each IBreadcrumb object that we will create and push onto our breadcrumbs array.
  • Next, I get the array of children for the route passed into the method. Initially, that is our root route.
  • If the route has no children, then we have reached the end of the routing tree. So, we simply return the array of breadcrumbs that we have created.
  • Now we are ready to iterate over each of the child routes.
  • For each child route, we are only interested in the primary route.
  • And, we also want to ensure that the route’s custom data includes the “breadcrumb” property. If not, continue to build the breadcrumb using it’s child routes.
  • Now that we know that we have the primary route, we need to build the URL for the route. There are two ways to do this: using observables or using the snapshot of the activated route at this time. I chose to use the snapshot approach as it’s a bit simpler. And, I don’t believe using the observable is necessary. The snapshot property is an instance of Angular’s ActivatedRouteSnapshot class. In that class we have the property named url that is an array of UrlSegment objects. I use the map() function to get each segment’s path property value, and then join these using the forward-slash as the separator “/”.
  • I then prefix my route’s URL with a forward-slash so that the routerLink values are absolute URLs.
  • Finally, I create a new object that is of type IBreadcrumb and assign the values for each property:
    • label,
    • params, and
    • url.
  • I then push this new object onto my array of breadcrumbs.
  • Once we have finished this primary route, then we are no longer concerned with the rest of the sibling children. So, we can just break out of the for-of loop.
  • Finally, we recurively invoke the getBreadcrumbs() method, passing in the primary child route, along with the url and array of breadcumbs that we have built so far.

Plunker

Here is an example of the code on Plunker:

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).