Scottish Smile
Scottish Smile

Reputation: 1141

Angular Breadcrumb Navigation

I've been trying to build a breadcrumb navigation system for angular.

The problem I'm having is my breadcrumb.service seems to be listing the Home page twice....

home-page

page-2

Code is on Stackblitz here - Stackblitz Editor

It's something about going through the addBreadcrumb() loop the first time.

(Also, I give credit to https://github.com/ocanzillon/angular-breadcrumb as I started off looking over his project)

My breadcrumb.service.ts :

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Data, NavigationEnd, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { filter } from 'rxjs/operators';

import { Breadcrumb } from '@core/models/breadcrumb';

@Injectable({
  providedIn: 'root'
})

export class BreadcrumbService {

  // Subject emitting the breadcrumb hierarchy
  private readonly _breadcrumbs$ = new BehaviorSubject<Breadcrumb[]>([]);

  // Observable exposing the breadcrumb hierarchy
  readonly breadcrumbs$ = this._breadcrumbs$.asObservable();



  constructor(private router: Router) {
    this.router.events.pipe(
      // Filter the NavigationEnd events as the breadcrumb is updated only when the route reaches its end
      filter((event) => event instanceof NavigationEnd)
    ).subscribe(event => {

      // Get the url "/page-1/page-2"
      const root = this.router.routerState.snapshot.root;

      // Create empty breadcrumb array
      const breadcrumbs: Breadcrumb[] = [];

      // Make first call to addBreadcrumb function
      this.addBreadcrumb(root.firstChild!, [], breadcrumbs);

      // Emit the new hierarchy
      this._breadcrumbs$.next(breadcrumbs);

    });
  }

    buildDepth(iterations: number, constituentFolders: string[]) {
      // This module builds links from the various preceeding folders
      // /page-1
      // /page-1/page-2
      // /page-1/page-2/page-3

      var depthStr="";
    
      for (let i=1; i<=iterations; i++)
      {
      // Add each of the folders to the string
          depthStr=depthStr + '/' + constituentFolders[i];
      }

      return depthStr;
    }


  private addBreadcrumb(route: ActivatedRouteSnapshot, parentUrl: string[], breadcrumbs: Breadcrumb[]) {
    if (route) {


      // Get the route url
      const routeUrl = parentUrl.concat(route.url.map(url => url.path));

      // Break the route into seperate folders /page-1/page-2/ becomes and array of [page-1], [page-2]
      let constituentFolders: string[] = new Array();
      constituentFolders = routeUrl.toString().split(",")

      // I need to add 'home' route at the start to match with the empty '' url.
      constituentFolders.splice(0, 0, 'home');

      // Iterate over the folders, building breadcrumb links for each part of the url.
      // Don't do the last folder as that's the current page.
      for(let i = 0; i < (constituentFolders.length); i++){

        // Add a breadcrumb link
          const breadcrumb = {
            label: this.getLabel(constituentFolders[i]),
            url: this.buildDepth(i, constituentFolders)
          };
          breadcrumbs.push(breadcrumb);

        // Recursive call to next element in the route.
        // pass in the route's first child.
        this.addBreadcrumb(route.firstChild!, routeUrl, breadcrumbs);

      }
    }
    
  }

  getLabel(input: string){
    // get the label name from the folder taken from the route
    // page-1 becomes "page 1"
    return input.replace(/-/g, ' ');
  }


}

The breadcrumb.component.ts :

import { Component } from '@angular/core';
import { Observable } from 'rxjs';

import { Breadcrumb } from '@core/models/breadcrumb';
import { BreadcrumbService } from '@core/services/breadcrumb.service';

@Component({
  selector: 'app-breadcrumb',
  templateUrl: './breadcrumb.component.html',
  styleUrls: ['./breadcrumb.component.scss']
})
export class BreadcrumbComponent {

  breadcrumbs$: Observable<Breadcrumb[]>;

  constructor(breadcrumbService: BreadcrumbService) {
    this.breadcrumbs$ = breadcrumbService.breadcrumbs$;
  }

}

The breadcrumb.component.html

    <ol id="breadcrumbNav" class="breadcrumb-nav">
    <li *ngFor="let breadcrumb of (breadcrumbs$ | async)">
      <a [href]="breadcrumb.url">{{ breadcrumb.label }}</a>
    </li>
  </ol>

And finally the CSS just adds the " > " chevrons after the links, but not the last link.

    .breadcrumb-nav {
    & {
      list-style: none;
      font-weight: bold;
      margin: 0;
      padding: 0;
    }
  
    li {
        display: inline-block;          /* display list in a row */
        padding-left: 5%;
  
      /* Display chevron before each link */
      &:before {
        content: " > ";
        margin: 0 6px;
      }
  
      /* Don't display chevron after the first link */
      &:first-child:before {
        content: "";
        margin: 0;
      }
    }
  }
  

.breadcrumb-nav a {
    color: #45768b;
    text-decoration: none;
}

.breadcrumb-nav a:hover {
    color: white;
}

Thanks

UPDATE:

I think it's because the default url is localhost:4200/home so there's an additional 'Home' in the first array...

enter image description here

enter image description here

Upvotes: 1

Views: 7253

Answers (2)

Scottish Smile
Scottish Smile

Reputation: 1141

An alternative solution is - in breadcrumb.service.ts

  // I need to add 'home' route at the start to match with the empty '' url.
  // The route maybe localhost:4200/home so a 'home' will be in constituentFolders[0]
  // We don't want to add a second 'home' then.
  if (constituentFolders[0] != 'home') {
    constituentFolders.splice(0, 0, 'home');
  }

enter image description here

Upvotes: 0

onrails
onrails

Reputation: 859

Oh I see now! It is the default label + the default path name. The first home label in the welcome screen is the label set by default in this line:

constituentFolders.splice(0, 0, 'home');

in your service file.

And the second home is coming from the redirectTo: 'home' set in the app-routing.module.ts when the path is empty. You have to remove either of them to have an ideal view of the breadcrumb, whereby you will have one home label by default and when you pick page one you will NOT get a duplication of home label.

The first approach (the easiest):

1- Go to app-routing.module.ts 2- Instead of this:

const routes: Routes = [
  {
    path: '',
    redirectTo: 'home', // <-- This from where the second label is coming
    pathMatch: 'full',
  }
]

To ensure that the default URL will be hosting the HomeComponent and you will not duplicate the label you can just do this:

const routes: Routes = [
  {
    path: '',
    component: HomePageComponent
  }
]

3- Results:

enter image description here

Thus, I think you do not need to set the home route in your app-routing.module.ts but make the default empty URL points to this component.

The second approach is to either add a conditional check in the service or rework the method to not have a hardcoded default value called home in your breadcrumb component. That's to say that the route tree will be constructed from index 0 dynamically starting from whatever the initial landing screen component route name is. Let me know if the first one is not suitable and you need to rework the service part.

I think the first one resolves your issue.

Here is your updated stackblitz

Upvotes: 1

Related Questions