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:
- / - the IndexComponent will display the index.
- /signin - the SigninComponent will display a sign in form for my app.
- /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.
BreadcrumbComponent
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
ActivatedRoutecontains data about the route for the current Component that is being displayed in the<router-outlet>. - The
NavigationEndclass 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:
labelandurl, both of which are strings. Theparamsproperty is not required and is of typeParams. 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
<breadcrumb>element defined in the template of theAppComponent. - My template HTML is defined inline. As this is a short example, this works in this case. But, you should most likely use the
templateUrlproperty 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
NgFordirective to iterate over thebreadcrumbobjects. These objects will be of typeIBreadcrumb- the interface I declared previously. - Inside our
NgForloop I output the link to the route for each breadcrumb using the values stored in theIBreadcrumbobject. We’ll populate these objects in our controller’sngOnInit()method.
Lines 24-31:
public breadcrumbs: IBreadcrumb[];
constructor(
private activatedRoute: ActivatedRoute,
private router: Router
) {
this.breadcrumbs = [];
}
- First, I define a public variable named
breadcrumbsthat is an array ofIBreadcrumbobjects. - My
constructor()function uses dependency injection (DI) to get instances of theActivatedRouteand theRouter. - Inside my constructor function I set the
breadcrumbsvalue 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
ngOnInitmethod we are going subscribe to anObservablefor when the navigation has ended in our Router. - The
Routerclass has a property namedevents. 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
NavigationEndclass. 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 theNavigationEndevent occurs. - Inside the anonymous function (whose
thisscope 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 thisrootroute 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
urlandbreadcrumbsarguments 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 namedbreadcrumb. We can access that custom data to set thelabelvalue for eachIBreadcrumbobject that we will create and push onto ourbreadcrumbsarray. - Next, I get the array of
childrenfor 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
ActivatedRouteSnapshotclass. In that class we have the property named url that is an array of UrlSegment objects. I use themap()function to get each segment’spathproperty 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
IBreadcrumband assign the values for each property:label,params, andurl.
- 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 primarychildroute, along with theurland array ofbreadcumbsthat we have built so far.
Plunker
Here is an example of the code on Plunker: