breadcrumb.component.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { Component, OnDestroy } from '@angular/core';
  2. import { ActivatedRoute, Data, NavigationEnd, Params, PRIMARY_OUTLET, Router } from '@angular/router';
  3. import { flatten } from 'lodash';
  4. import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subject } from 'rxjs';
  5. import { filter, map, startWith, switchMap, takeUntil } from 'rxjs/operators';
  6. import { DataService } from '../../../data/providers/data.service';
  7. export type BreadcrumbString = string;
  8. export interface BreadcrumbLabelLinkPair {
  9. label: string;
  10. link: any[];
  11. }
  12. export type BreadcrumbValue = BreadcrumbString | BreadcrumbLabelLinkPair | BreadcrumbLabelLinkPair[];
  13. export type BreadcrumbFunction = (data: Data, params: Params, dataService: DataService) => BreadcrumbValue | Observable<BreadcrumbValue>;
  14. export type BreadcrumbDefinition = BreadcrumbValue | BreadcrumbFunction | Observable<BreadcrumbValue>;
  15. /**
  16. * A breadcrumbs component which reads the route config and any route that has a `data.breadcrumb` property will
  17. * be displayed in the breadcrumb trail.
  18. *
  19. * The `breadcrumb` property can be a string or a function. If a function, it will be passed the route's `data`
  20. * object (which will include all resolved keys) and any route params, and should return a BreadcrumbValue.
  21. *
  22. * See the test config to get an idea of allowable configs for breadcrumbs.
  23. */
  24. @Component({
  25. selector: 'vdr-breadcrumb',
  26. templateUrl: './breadcrumb.component.html',
  27. styleUrls: ['./breadcrumb.component.scss'],
  28. })
  29. export class BreadcrumbComponent implements OnDestroy {
  30. breadcrumbs$: Observable<Array<{ link: string | any[]; label: string; }>>;
  31. private destroy$ = new Subject<void>();
  32. constructor(private router: Router,
  33. private route: ActivatedRoute,
  34. private dataService: DataService) {
  35. this.breadcrumbs$ = this.router.events.pipe(
  36. filter(event => event instanceof NavigationEnd),
  37. takeUntil(this.destroy$),
  38. startWith(true),
  39. switchMap(() => this.generateBreadcrumbs(this.route.root)),
  40. );
  41. }
  42. ngOnDestroy(): void {
  43. this.destroy$.next();
  44. this.destroy$.complete();
  45. }
  46. private generateBreadcrumbs(rootRoute: ActivatedRoute): Observable<Array<{ link: Array<string | any>; label: string; }>> {
  47. const breadcrumbParts = this.assembleBreadcrumbParts(rootRoute);
  48. const breadcrumbObservables$ = breadcrumbParts
  49. .map(({ value$, path }) => {
  50. return value$.pipe(
  51. map(value => {
  52. if (isBreadcrumbLabelLinkPair(value)) {
  53. return {
  54. label: value.label,
  55. link: this.normalizeRelativeLinks(value.link, path),
  56. };
  57. } else if (isBreadcrumbPairArray(value)) {
  58. return value.map(val => ({
  59. label: val.label,
  60. link: this.normalizeRelativeLinks(val.link, path),
  61. }));
  62. } else {
  63. return {
  64. label: value,
  65. link: '/' + path.join('/'),
  66. };
  67. }
  68. }),
  69. ) as Observable<BreadcrumbLabelLinkPair | BreadcrumbLabelLinkPair[]>;
  70. });
  71. return observableCombineLatest(breadcrumbObservables$).pipe(map(links => flatten(links)));
  72. }
  73. /**
  74. * Walks the route definition tree to assemble an array from which the breadcrumbs can be derived.
  75. */
  76. private assembleBreadcrumbParts(rootRoute: ActivatedRoute): Array<{ value$: Observable<BreadcrumbValue>; path: string[] }> {
  77. const breadcrumbParts: Array<{ value$: Observable<BreadcrumbValue>; path: string[] }> = [];
  78. const inferredUrl = '';
  79. const segmentPaths: string[] = [];
  80. let currentRoute: ActivatedRoute | null = rootRoute;
  81. do {
  82. const childRoutes = currentRoute.children;
  83. currentRoute = null;
  84. childRoutes.forEach((route: ActivatedRoute) => {
  85. if (route.outlet === PRIMARY_OUTLET) {
  86. const routeSnapshot = route.snapshot;
  87. let breadcrumbDef: BreadcrumbDefinition | undefined =
  88. route.routeConfig && route.routeConfig.data && route.routeConfig.data['breadcrumb'];
  89. segmentPaths.push(routeSnapshot.url.map(segment => segment.path).join('/'));
  90. if (breadcrumbDef) {
  91. if (isBreadcrumbFunction(breadcrumbDef)) {
  92. breadcrumbDef = breadcrumbDef(routeSnapshot.data, routeSnapshot.params, this.dataService);
  93. }
  94. const observableValue = isObservable(breadcrumbDef) ? breadcrumbDef : observableOf(breadcrumbDef);
  95. breadcrumbParts.push({ value$: observableValue, path: segmentPaths.slice() });
  96. }
  97. currentRoute = route;
  98. }
  99. });
  100. } while (currentRoute);
  101. return breadcrumbParts;
  102. }
  103. /**
  104. * Accounts for relative routes in the link array, i.e. arrays whose first element is either:
  105. * * `./` - this appends the rest of the link segments to the current active route
  106. * * `../` - this removes the last segment of the current active route, and appends the link segments
  107. * to the parent route.
  108. */
  109. private normalizeRelativeLinks(link: any[], segmentPaths: string[]): any[] {
  110. const clone = link.slice();
  111. if (clone[0] === './') {
  112. clone[0] = segmentPaths.join('/');
  113. }
  114. if (clone[0] === '../') {
  115. clone[0] = segmentPaths.slice(0, -1).join('/');
  116. }
  117. return clone;
  118. }
  119. }
  120. function isBreadcrumbFunction(value: BreadcrumbDefinition): value is BreadcrumbFunction {
  121. return typeof value === 'function';
  122. }
  123. function isObservable(value: BreadcrumbDefinition): value is Observable<BreadcrumbValue> {
  124. return value instanceof Observable;
  125. }
  126. function isBreadcrumbLabelLinkPair(value: BreadcrumbValue): value is BreadcrumbLabelLinkPair {
  127. return value.hasOwnProperty('label') && value.hasOwnProperty('link');
  128. }
  129. function isBreadcrumbPairArray(value: BreadcrumbValue): value is BreadcrumbLabelLinkPair[] {
  130. return Array.isArray(value) && isBreadcrumbLabelLinkPair(value[0]);
  131. }