Breadcrumbs with dynamic async naming in Angular

228 Views Asked by At

I have been working on implementing breadcrumbs in my Angular application. To do this, I get the name from the routing data. Lets take for example a path shop/products. The breadcrumbs will show "the store > products" because this is specified in the data.breadcrumb of the route (see code below). The problem is when there is a path with an id. Lets say we want to have a specific product from the shop. The path will be shop/products/1. I want the breadcrumb to show the name of the product and not the id. So it should show for example "the shop > products > sneaker" instead of "the shop > products > 1". I have been looking online for ways to get breadcrumbs dynamically, and the only good example I found was from Olivier Canzillon on Github (https://github.com/ocanzillon/angular-breadcrumb). In his example he uses a resolver to fetch the data before the route is loaded and show it in the breadcrumb.

This solution does work and gives a great structure to work with, but it does not load the data async. Because the resolver method is used, the page is not loaded until the resolve is finished. This is something which I do not want in my application. I want the page to be shown to the user, and fill in with the data when it is loaded.

In my project, I use stores to cache data which is retrieved instead of making a call to the api everytime. I would like to find a dynamic way of showing the data in my breadcrumbs using the observers from these stores. The page may already open en the right name will fill in dynamically when it is loaded. Including every store in the constructor of the breadcrumbs will not be possible. Is there a way of passing in the stores (or the obeservable objects from the stores) with the routing, like with the resolvers in the above approach? Or another good approach to achieve this result?

from app-routing.module.ts:

const routes: Routes = [   
  {
     path: 'store',
     data:{
           breadcrumb: 'the store'
     },
     children: [
       {
         path: '',
         data:{
           breadcrumb: null
         },
         component: StoreComponent,
       },
       {
         path: 'products',
         data:{
           breadcrumb: 'products
         },
         children: [
           {
             path: '',
             data:{
               breadcrumb: null
             },
             component: ProductsComponent,
           },
           {
             path: ':id',
             data:{
               breadcrumb: (data: any) =\>`${data?.product?.name}\`
             },
             resolve:{
               product: ProductResolver
             },
             component: ProductDetailComponent,
           }
         ]
       },
     ],
  },
];

breadcrumb.componet.ts:


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

interface Breadcrumb {
label: string;
url?: string;
}

@Component({
selector: 'app-breadcrumb',
templateUrl: './breadcrumb.component.html'
})
export class AppBreadcrumbComponent {

    private readonly _breadcrumbs$ = new BehaviorSubject<Breadcrumb[]>([]);
    
    readonly breadcrumbs$ = this._breadcrumbs$.asObservable();
    
    constructor(private router: Router,
        private activatedRoute: ActivatedRoute) {
        this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(() => {
            var baseUrl = this.getBaseUrl(this.activatedRoute);
            var menuItems = this.createBreadcrumbs(this.activatedRoute, baseUrl)
    
            this._breadcrumbs$.next(menuItems);
        });
    }
    
    private getBaseUrl(route: ActivatedRoute){
        var url = "";
        if(route.parent){
            url = this.getBaseUrl(route.parent);
            url += route.parent.snapshot.url.map(segment => segment.path).join('/');
        }
        return url;
    }
    
    private createBreadcrumbs(route: ActivatedRoute, url: string = '#', breadcrumbs: Breadcrumb[] = []) {
        const children: ActivatedRoute[] = route.children;
    
        if (children.length === 0) {
            return breadcrumbs;
        }
    
        for (const child of children) {
            const routeURL: string = child.snapshot.url.map(segment => segment.path).join('/');
            if (routeURL !== '') {
                url += `/${routeURL}`;
                console.log(url);
            }
      
            var label = this.getLabel(child.snapshot.data);
            if (label) {
                breadcrumbs.push({label, url});
            }
      
            return this.createBreadcrumbs(child, url, breadcrumbs);
        }
          
    }
    
    private getLabel(data: Data) {
        return typeof data.breadcrumb === 'function' ? data.breadcrumb(data) : data.breadcrumb;
    }

}

breadcrumb.component.html:

<nav class="layout-breadcrumb">
    <ol>
        <ng-template ngFor let-item let-last="last" [ngForOf]="breadcrumbs$ | async">
            <li *ngIf="!last"><a [href]="item.url">{{item.label}}</a></li>
            <li *ngIf="last">{{item.label}}</li>
            <li *ngIf="!last" class="layout-breadcrumb-chevron"> / </li>
        </ng-template>
    </ol>
</nav>

product.resolver.ts:


import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from "@angular/router";
import { Product } from "../\_models/domain/product";
import { Observable, of, switchMap } from "rxjs";
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";

@Injectable({
providedIn: 'root'
})
export class ProductResolver implements Resolve\<Product\> {
private url = 'www.example.com/api/product';

    constructor(private readonly http: HttpClient){
    
    }
    
    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Product> {
        return this.http.get<Product[]>(this.url).pipe(
            switchMap(product => 
                {
                    return of(product.find(p => p.id == route.params?.productId));
                })
            );
    }

}
0

There are 0 best solutions below