[從 0 開始的 Angular 生活]No.56 在 Angular 中結合 Bootstrap4 做個麵包屑

前言

同上篇,這篇也是寫玩具 (side project) 時遇到的需求,麵包屑 (Breadcrumbs) 這個功能亦是網站相當常見的元素,那麼又該如何透過 Angular 的 Router 搭配 Bootstrap 4 建立麵包屑呢?

本文環境

  • Angular CLI: 8.3.20
  • Angular: 8.2.14
  • Bootstrap: 4.3.1

實作

說明

因為這個需求源自於我的玩具 (side project) ,而麵包屑的做法在網路上查完一輪後,因人而異的有各種方式能實作,所以本文的做法僅供參考。

如同上一段提到的,這個專案環境使用懶載入的方式載入模組 (lazy-loading-ngmodules) ,而網路上查到的資料卻很少提到在這個前提下應該怎麼調整,導致照著做是無法順利運行的。

又爬了好一陣的資料,折騰了好久才完成,但完成的版本需要再麵包屑元件的 constructor(){} 內進行 router.events 的訂閱。

但根據查到的資料,constructor(){} 指的是物件實體剛被建立時,此時是不包含在生命週期中的,而 constructor(){} 應該只做依賴注入,不要亂加一些東西。

想想,又對於這個做法不滿意,想辦法改良後最後決定放在這邊記錄下來。

routing.module 設置

在各個父層路由定義 data 物件內容,如:

app-routing.module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const routes: Routes = [
{
path: '',
redirectTo: '/login',
pathMatch: 'full',
},
{
path: 'login',
component: LoginComponent,
},
{
path: 'dashboard',
loadChildren: () => import('./back-ui/back-routing.module').then(m => m.BackRoutingModule),
data: { breadcrumb: '後台' },
},
{
path: '**',
redirectTo: '/login',
},
];

back-routing.module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const routes: Routes = [
{
path: '',
component: DashboardComponent,
children: [
{
path: 'products',
loadChildren: () => import('../module/products/products-routing.module').then(m => m.ProductsRoutingModule),
data: { breadcrumb: '產品列表' },
},
{
path: 'orders',
loadChildren: () => import('../module/orders/orders-routing.module').then(m => m.OrdersRoutingModule),
data: { breadcrumb: '訂單列表' },
},
],
},
];

訂閱 Router events

AppComponent 中訂閱 Router 的事件, 而這些事件相當多,在此只需要針對 NavigationEnd 進行處理即可。

另外要記得在 ngOnDestroy() 階段取消訂閱。

接著還需要一隻服務 BreadcrumbService 將資料存下來,供 BreadcrumbComponent 使用

app.component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router, NavigationEnd } from '@angular/router';
import { BreadcrumbService } from './module/breadcrumb/breadcrumb-service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {

private _routerSubscription: any;

constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
private breadcrumbService: BreadcrumbService,
) { }

ngOnInit(): void {
this._routerSubscription = this.router.events.subscribe((event: NavigationEnd) => {
if (event instanceof NavigationEnd) {
const root: ActivatedRoute = this.activatedRoute.root;
this.breadcrumbService.setActivatedRouteRoot(root);
}
});
}

ngOnDestroy(): void {
this._routerSubscription.unsubscribe();
}

}

eventNavigationEnd 時,透過 setActivatedRouteRoot() 將資料儲存。

新增 Breadcrumb Service

breadcrumb-service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Injectable, EventEmitter } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Injectable()
export class BreadcrumbService {

private root: ActivatedRoute;
public routeEvent = new EventEmitter<ActivatedRoute>();

public setActivatedRouteRoot(root: ActivatedRoute): void {
this.root = root;
this.routeEvent.emit(root);
}

public getActivatedRouteRoot(): ActivatedRoute {
return this.root;
}
}

setActivatedRouteRoot() 被觸發時,將資料 emit 出去,這時 BreadcrumbComponent 只需要訂閱 routeEvent 事件就好了~

建立 BreadcrumbComponent

接著來處理本次的主角:

  • 依賴注入 BreadcrumbService
  • ngOnInit() 內 訂閱BreadcrumbServicerouteEvent 事件,確保資料的取得
    • 由於是在 ngOnInit() 階段訂閱,所以程式首次運行必須跑一次 getActivatedRouteRoot()
  • ngOnDestroy() 階段取消訂閱
  • 運用遞迴的技巧取回顯示在麵包屑上的內容

將取回的資料印出觀察,會發現路由的資料其實是一層層包覆的資料結構

breadcrumb.component.html

1
2
3
4
5
6
7
8
9
10
<nav aria-label="breadcrumb">
<ol class="breadcrumb bg-transparent px-0 mb-0">
<ng-container *ngFor="let breadcrumb of breadcrumbs;let lastRecord = last">
<li class="breadcrumb-item" [ngClass]="{'active': lastRecord}">
<a [routerLink]="[breadcrumb.url]" *ngIf="!lastRecord">{{ breadcrumb.label }}</a>
<ng-container *ngIf="lastRecord">{{ breadcrumb.label }}</ng-container>
</li>
</ng-container>
</ol>
</nav>

breadcrumb.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BreadcrumbDTO } from '../breadcrumb-dto';
import { ActivatedRoute, PRIMARY_OUTLET } from '@angular/router';
import { BreadcrumbService } from '../breadcrumb-service';

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

public breadcrumbs: Array<BreadcrumbDTO> = [];

constructor(
private breadcrumbService: BreadcrumbService,
) {}

ngOnInit(): void {
this.breadcrumbService.routeEvent.subscribe((root: ActivatedRoute) => {
this.breadcrumbs = this.getBreadcrumbs(root);
});
this.breadcrumbs = this.getBreadcrumbs(this.breadcrumbService.getActivatedRouteRoot());
}

ngOnDestroy(): void {
this.breadcrumbService.routeEvent.unsubscribe();
}

private getBreadcrumbs(route: ActivatedRoute, url: string = '', breadcrumbs: Array<BreadcrumbDTO> = []): Array<BreadcrumbDTO> {
const ROUTE_DATA_BREADCRUMB = 'breadcrumb';

const children: ActivatedRoute[] = route.children;
if (children.length === 0) { return breadcrumbs; }

for (const child of children) {

if (child.outlet !== PRIMARY_OUTLET) { continue; }
if (!child.snapshot.data.hasOwnProperty(ROUTE_DATA_BREADCRUMB)) {
return this.getBreadcrumbs(child, url, breadcrumbs);
}

const routeURL: string = child.snapshot.url.map(segment => segment.path).join('/');
if (routeURL) {
url += `/${routeURL}`;
}

const breadcrumb: BreadcrumbDTO = {
label: child.snapshot.data[ROUTE_DATA_BREADCRUMB],
params: child.snapshot.params,
url,
};

if (child.component) {
breadcrumbs.push(breadcrumb);
}
return this.getBreadcrumbs(child, url, breadcrumbs);
}

return breadcrumbs;
}

}

特別要注意如果是使用懶載入模組的方式,則需要判斷 child.component 否則會有麵包屑名稱重複的問題,原因參考資料中有提到,有興趣的不妨看看。

參考資料

這篇文章參考的內容

結論

成果圖:

本文不會有完整程式碼提供下載,僅紀錄相關程式碼片段,如果之後這個玩具有完成,會再考慮要不要公開。

0%