Implement a breadcrumb in Angular with PrimeNg

Implement a breadcrumb bar in Angular with PrimeNg

A breadcrumb bar is a very important piece of any website. It gives an idea where the user is currently in, from where the user landed on this page and finally allow the user to navigate back to any steps wanted.
I like to call it an “enhanced version of the URL path”. The URL path in itself has the information but it might, at time, not be human readable. That is where the breadcrumb become indispensable.
I showed few features of PrimeNg in my previous posts about building an inline form and about building a tree structure. It turns out that they also provide a Angular friendly breadcrumb component.
Today we will see how we can make use of the breadcrumb component together with the Angular router to provide a breadcrumb bar.
This post is composed by 3 parts:

1. PrimeNg Breadcrumb component
2. Breadcrumb service, parent/child component communication
3. Use the breadcrumb service together with the PrimeNg Breadcrumb component

1. PrimeNg Breadcrumb component

Just like other components from PrimeNg, the breadcrumb component comes from a separate module called BreadcrumbModule and each breadcrumb element is defined as a MenuItem like any other menu. So we need to import those in the module we need to use the breadcrumb.

@NgModule({
  imports: [
    BreadcrumbModule
    ...
  ],
  ...
})
export class MyModule { }

All we have to do is simply to use the breadcrumb:

<p-breadcrumb [model]="crumbs"></p-breadcrumb>

And in our component:

export class PrimeNgComponent implements OnInit {
  crumbs: MenuItem[];
    
  ngOnInit() {
    this.crumbs = [
        { label: 'Home' },
        { label: 'Products' },
        { label: 'Product A' }
    ];
  }
}

model binds to our property crumbs in our component and we can push MenuItem in it on initialization. The result is as followed:

preview

Now that we have a breadcrumb component working, what we have left to do is to use it from our pages and update it accordingly to the current page we navigated to.

2. Breadcrumb service, parent/child component communication

In order to update the breadcrumb, we can directly update the crumbs on ngOnInit of each components. But this would mean having to add the breadcrumb in every pages. What we can do instead is create a parent component, which will hold the breadcrumb and then from each child components, update the breadcrumb depending on which child is active. For this to work, we need a Breadcrumb service to communicate the current position back to the parent.

Here’s a preview of the implementation:

@Injectable()
export class BreadcrumbService {
  private crumbs: Subject<MenuItem[]>;
  crumbs$: Observable<MenuItem[]>;

  constructor() {
    this.crumbs = new Subject<MenuItem[]>();
    this.crumbs$ = this.crumbs.asObservable();
  }

  setCrumbs(items: MenuItem[]) {
    this.crumbs.next(
      (items || []).map(item =>
          Object.assign({}, item, {
            routerLinkActiveOptions: { exact: true }
          })
        )
    );
  }
}

The Breadcrumb service contains a crumbs$ observable which will change based on the current active route.
It has a function setCrumbs which will be used on the active route component to dispatch a new item to the crumbs$ observable.

Notice the routerLinkActiveOptions: { exact: true } option which makes only the exact path active for the router. This is to prevent all the crumbs to be seen as visually active.

Now that we have the service, we can move on to the components implementations.

3. Use the breadcrumb service together with the PrimeNg Breadcrumb component

In a previous post, I explained how the Angular router works and how routes are defined.

Here we want a parent component which will hold the breadcrumb:

@Component({
  template: `
    <div class="m-3">
      <p-breadcrumb [model]="crumbs$ | async"></p-breadcrumb>
    </div>
    <router-outlet></router-outlet>
  `
})
export class ParentComponent implements OnInit {
  crumbs$: Observable<MenuItem[]>;
    
  constructor(private breadcrumb: BreadcrumbService) { }

  ngOnInit() {
    this.crumbs$ = this.breadcrumb.crumbs$;
  }
}

The <router-outlet></router-outlet> defines where the content of the child will be placed; under the tag.
Then we can place it as a parent route for our components:

const routes: Routes = [
  {
    path: '',
    component: ParentComponent,
    children: [{
      path: 'my-route',
      component: MyComponent,
    }],
  }
];

Now we should be able to see the breadcrumb when navigating to /my-route.
Next we need to be able to update the breadcrumb when MyComponent is activated.

@Component({
  template: `
    <p class="m-3">This component is breadcrumb'ed</p>
  `
})
export class MyBreadcrumbedComponent implements OnInit {
  constructor(private breadcrumb: BreadcrumbService) { }

  ngOnInit() {
    setTimeout(() =>
      this.breadcrumb.setCrumbs([{
        label: 'A'
      }, {
        label: 'B'
      }, {
        label: 'C'
      }])
    );
  }
}

You might be wondering, what is the setTimeout about?
The setTimeout is meant to delay the cycle of update for the breadcrumb.
As we saw, the crumbs are directly displayed in the parent. Because of the change cycle ran by Angular, the parent data are evaluated first, at that point, the breadcrumb was empty. Then once the child component is evaluated, it pushes data to update the breadcrumb which has already been evaluated.
This causes the following exception:

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '[object Object]'. Current value: '[object Object],[object Object],[object Object]'.

In order to fix this, we simply delay the update to the end of the change cycle using setTimeout.

And that’s it, we have now a workable breadcrumb. More enhancement can be made, for example, we can make a common base class which will be used by all components needing to update the breadcrumb and we could add the menu items straight into the route data so that no code would be needed on the component. Lastly we could implement a token replacement mechanism in order to allow the route labels to use values from the params of the activated route but I will leave that for you to implement!

The source code is available on my GitHub https://github.com/Kimserey/ng-samples/blob/master/src/app/primeng/parent.component.ts.

Conclusion

Today we saw how to implement a breadcrumb bar to enhance the user experience of a website. In the process we saw how we could create a parent component, child components and how we can bind them via the Angular router. We also saw how we could utilize a service to allow a decoupled two way communication from component to component using observables. Lastly we saw how to solve one particular tricky problem encountered for parent/child communication whereby parent will be updated after being checked. Hope you enjoyed this post as much as I enjoyed writing it. As usual, if you have any question leave it here or hit me on Twitter @Kimserey_Lam. See you next time!

Comments

  1. Nice post. It was really helpful to me. Thank you.

    ReplyDelete
  2. gr8 post! save me a lot of time. THX

    ReplyDelete

Post a Comment

Popular posts from this blog

A complete SignalR with ASP Net Core example with WSS, Authentication, Nginx

Verify dotnet SDK and runtime version installed

SDK-Style project and project.assets.json