Dynamischer Breadcrumb mithilfe des Angular Router — Tutorial

Klemens Kühle
coodoo
Published in
5 min readNov 26, 2021

--

Ein Breadcrumb ist ein simples Werkzeug zur besseren Orientierung für Nutzer einer Web-Anwendung. Es erleichtert das Zurechtfinden und Navigieren im aktuellen Seiten-Kontext. Hier ein Tutorial, wie man ein solches in Angular effizient implementieren kann.

Übersicht

Stackblitz-Demo (Angular v12)️
✅ Komplettes Tutorial in Vanilla Angular
🎛 Features:
• Übersichtlich und leicht wartbar, da Config komplett via Angular Router
• Keine extra Library notwendig
• Dynamische Routen werden menschenleserlich übersetzt

Einführung

Ein Breadcrumb besteht aus einer Link-Liste — von der Startseite an bis zum aktuellen Knotenpunkt, an dem man sich befindet. Anhand dieser kann man sich pro Link zurückerinnern und -navigieren und schon wird klar, woher dieses Tool ihren Namen hat: Vom Märchen “Hänsel und Gretel”.

Die Grund-Idee ist, dass wir aus der aktuellen Route rekursiv jede Parent-Route hochsteigen und jeweils ein Link-Segment in einer Liste speichern, die es dann einfach mit einem Trenner anzuzeigen gilt. Alles, was eine vollständige Breadcrumb-Component braucht, bietet uns ‘@angular/router’.

Das Template

Beim Template angefangen erkennt man die Simplizität und Raffinesse der Komponente. Wir iterieren über alle gefundenen Breadcrumbs und bauen pro Breadcrumb einen Anchor-Tag mit Link und Label. Als Trenner der einzelnen Links bieten sich diverse typische Zeichen an, wie z.B. /, > oder ». Die lokale NgForOf-Hilfsvariable last hilft zu vermeiden, ein Trennzeichen zu viel auszugeben und außerdem z.B. das letzte Segment bold hervorzuheben.

<ng-container *ngFor="let bc of breadcrumbs; let last = last">
<a [routerLink]="bc.link" [ngClass]="{'font-weight-bold': last}">
{{ bc.label }}
</a>
<span *ngIf="!last"> / </span>
</ng-container>

Die Routing-Configuration

Wo wären die einzelnen Breadcrumb-Attribute besser aufgehoben, als in der Routing-Configuration selbst? Einer Angular Route kann man pro Definition willkürliche, zur Route gehörende Daten anhängen. In unserem Fall zum Beispiel ein breadcrumb-Object. Da wir den Link zur Route aus dem path lesen können, brauchen wir erst mal nur ein leserliches Label. Streng genommen könnte man auch hier den path ausgeben.

{ 
path: 'home',
data: {
breadcrumb: {
label: 'Startseite'
}
}
children: [{
path: 'detail',
data: {
breadcrumb: {
label: 'Detailseite'
}
},
children: [{
path: 'even-more-detail',
data: {
breadcrumb: {
label: 'Detailliertere Seite'
}
}
}]
}]
}

Listening for Navigation-Events

Da so ein Breadcrumb ein sehr globales Element der Seite ist, sollte man — je nach Projektstruktur — es so früh wie möglich einbinden, am besten direkt in der app.component. Dort sitzt es nun und hat zur Aufgabe, bei jedem neuen Routing den korrekten Breadcrumb anzuzeigen.

Dafür speichern wir uns die breadcrumbs in einem Array vom Typ Breadcrumb, ein simples Interface mit den zwei Strings label und link, und können auf die Router-Events subscriben. Nach jedem Navigieren, also beim NavigationEnd-Event, löschen wir erst die alten Breadcrumbs und erstellen dann die aktuellen.

export interface Breadcrumb {
label: string,
link: string
}
[...]breadcrumbs: Breadcrumb[] = [];constructor(
private router: Router,
private activatedRoute: ActivatedRoute
) {
this.router.events.subscribe(ev => {
if (ev instanceof NavigationEnd) {
this.breadcrumbs = [];
this.buildBreadcrumb(this.activatedRoute);
}
});
}

buildBreadcrumb()

Das Herz der Sache ist nun die Erzeugung des Breadcrumbs. Je nach Komplexität der Sonderfälle, kann hier die Anzahl Zeilen Code in die Höhe schießen. Der simpelste Fall sieht aber wie folgt aus:

0  buildBreadcrumb(currentAR: ActivatedRoute) {
1 if (currentAR.snapshot.data.breadcrumb) {
2 const lastBCLink = this.breadcrumbs.length !== 0 ?
this.breadcrumbs[this.breadcrumbs.length-1].link : '';
3 this.breadcrumbs.push({
4 label: currentAR.snapshot.data.breadcrumb.label,
5 link: lastBCLink + '/' + currentAR!.routeConfig!.path
6 } as Breadcrumb);
7 }
8
9 if (currentAR.firstChild !== null) {
10 this.buildBreadcrumb(currentAR.firstChild);
11 }
12 }

Initial wird bei einem NavigationEnd-Event diese Funktion mit der aktuellen activatedRoute aufgerufen. Diese fügt einen Breadcrumb ins Array, falls die aktuelle Route im data-Objekt einen Breadcrumb definiert hat (Z. 1) und ruft sich rekursiv auf, solange sie Kinder-Routen hat (Z. 9).

Für einen neuen Breadcrumb brauchen wir nur das aktuell definierte Label (Z. 4) und den Link. Um den Link zusammenzubauen, brauchen wir eine vollständige absolute Route, die wir einfach zusammensetzen können aus dem Link des letzten Breadcrumbs (Z.2) und dem aktuellen (Z.5).

Mit der bisher vorgestellten Implementierung ergibt sich für die gegebene Routing-Config folgenden Breadcrumb, wenn man sich auf /home/detail/even-more-detail befindet:

Startseite / Detailseite / Detailliertere Seite

Dies ist also die minimale Komponente für einen dynamischen Breadcrumb. Das Beispiel umfasst aber nur statische Routen, wie sieht es mit dynamischen aus?

Route-Parameter auflösen

Typischerweise hat eine Angular-Anwendung dynamische Routen, die abhängig sind von zum Beispiel einer ID, wie im folgenden Route-Config-Beispiel:

{ path: 'fairy-tales',
data: {
breadcrumb: {
label: 'Märchen'
}
},
children: [{
path: ':id',
...
}]
}

Wenn es sich bei der Anwendung um z.B. eine interne technische Administrations-Oberfläche handelt, könnte man ja direkt diese ID unverschönt als Label ausgeben. Dafür kann man die buildBreadcrumb-Funktion so erweitern, dass der aktuelle Route-Parameter, der dem im path definierten entspricht, als Label für einen neuen Breadcrumb genutzt wird:

if (Object.keys(currentRoute.snapshot.params).length) {
const routeParam = Object.values(currentRoute.snapshot.params)[0];
this.breadcrumbs.push({
label: routeParam,
link: lastBreadcrumb.link + '/' + routeParam
} as Breadcrumb);
}

Route-Parameter leserlich auflösen

Schöner wäre natürlich ein aufgelöster Wert der ID, wie zur obigen Routing-Config passend Märchen / Hänsel und Gretel.

Wie ins gemachte Nest können wir uns dafür setzen, wenn wir Angulars Resolver-Funktionalität nutzen, über die man sich in meinem anderen Blog-Beitrag einführend informieren kann.

resolve(route: ActivatedRouteSnapshot): Observable<Breadcrumb> {
return this.fairyTaleService.get(route.paramMap.get('fairytale'));
}

Es braucht dazu also nur einen FairyTaleResolver, der den API-Call anhand des Route-Parameters fairytale absetzt (s.o.). Der Service muss sich natürlich darum kümmern, dass ein Breadcrumb-Objekt zurückgegeben wird, wobei das Label der aufgelöste Wert ist und der link einfach der mitgegebene Slug.
Dieses Objekt wird dann als breadcrumb-Objekt ins data-Objekt der Route gesetzt (s.u.).

path: ':fairytale',
component: FairyTaleComponent,
resolve: {
breadcrumb: FairyTaleResolver
}

Nun kann die buildBreadcrumb-Funktion folgendermaßen umgebaut werden: Statt den aktuellen Link direkt zu wissen, prüfen wir erst, ob es sich um eine dynamische Route handelt. Wenn ja, kann das dank dem Resolver aufgelöste link-Attribut ausgelesen werden. Ansonsten wie gewohnt der aktuelle path aus der routeConfig.

 let currentBCLink = '';
if (currentAR?.routeConfig?.path?.startsWith(':')) {
currentBCLink = currentAR.snapshot.data.breadcrumb.link;
} else {
currentBCLink = currentAR?.routeConfig?.path || '';
}

Fazit

Ist die Breadcrumb-Komponente erst mal nach dieser Anleitung implementiert und eingebunden, ist für neue Routen oder Änderungen im Bestands-Routing kein Overhead an Änderungen notwendig, damit der Breadcrumb funktioniert wie er soll. Im Gegenteil: Genau da, wo man die Änderungen bezüglich der neuen Routen macht, erweitert man für die RouteConfig den entsprechend Breadcrumb-Wert und schon hat sich das.

Die komplette Implementierung und Übersicht der Demo kann hier bei Stackblitz angesehen werden.

Trivia

“Wer sind Hänsel und Gretel und was haben die mit Webseiten-Navigation zu tun?!” höre ich da jemanden fragen, woraufhin ich antworten möchte, dass das jenes Gebrüder Grimm Märchen war, in dem die beiden Kinder im Wald Brotkrumen hinter sich liegen ließen, um den Weg zurück zu finden und möchte meine Oma zitierend hinzufügen: “Lernt ihr Kinder heute denn gar keine Märchen mehr in der Schule?!”.

Weiterführende Links

--

--