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
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
andurl
, both of which are strings. Theparams
property 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
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 thebreadcrumb
objects. These objects will be of typeIBreadcrumb
- the interface I declared previously. - Inside our
NgFor
loop I output the link to the route for each breadcrumb using the values stored in theIBreadcrumb
object. 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
breadcrumbs
that is an array ofIBreadcrumb
objects. - My
constructor()
function uses dependency injection (DI) to get instances of theActivatedRoute
and theRouter
. - 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 anObservable
for when the navigation has ended in our Router. - The
Router
class 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
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 theNavigationEnd
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 thisroot
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
andbreadcrumbs
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 namedbreadcrumb
. We can access that custom data to set thelabel
value for eachIBreadcrumb
object that we will create and push onto ourbreadcrumbs
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 themap()
function to get each segment’spath
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
, 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 primarychild
route, along with theurl
and array ofbreadcumbs
that we have built so far.
Plunker
Here is an example of the code on Plunker: