Picture of Brian Love wearing black against a dark wall in Portland, OR.

Brian Love

Angular2 Breadcrumb using Router

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

Lines 5-9:

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

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

Lines 24-31:

public breadcrumbs: IBreadcrumb[];

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

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

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

Plunker

Here is an example of the code on Plunker: