77643: WIP: Refactor BreadcrumbsComponent into service

This commit is contained in:
Bruno Roemers
2021-03-12 15:36:00 +01:00
parent 7b455c47c8
commit c2ca35c522
5 changed files with 222 additions and 79 deletions

View File

@@ -1,5 +1,5 @@
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
<nav *ngIf="(showBreadcrumbs$ | async)" aria-label="breadcrumb">
<ol class="breadcrumb">
<ng-container
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'home.breadcrumbs', url: '/'}"></ng-container>

View File

@@ -12,7 +12,6 @@ import { Breadcrumb } from './breadcrumb/breadcrumb.model';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { VarDirective } from '../shared/utils/var.directive';
import { getTestScheduler } from 'jasmine-marbles';
class TestBreadcrumbsService implements BreadcrumbsProviderService<string> {
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
@@ -92,24 +91,24 @@ describe('BreadcrumbsComponent', () => {
expect(component).toBeTruthy();
});
describe('ngOnInit', () => {
beforeEach(() => {
spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]));
});
it('should call resolveBreadcrumb on init', () => {
router.events = observableOf(new NavigationEnd(0, '', ''));
component.ngOnInit();
fixture.detectChanges();
expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
});
});
describe('resolveBreadcrumbs', () => {
it('should return the correct breadcrumbs', () => {
const breadcrumbs = component.resolveBreadcrumbs(route.root);
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedBreadcrumbs });
});
});
// describe('ngOnInit', () => {
// beforeEach(() => {
// spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]));
// });
//
// it('should call resolveBreadcrumb on init', () => {
// router.events = observableOf(new NavigationEnd(0, '', ''));
// component.ngOnInit();
// fixture.detectChanges();
//
// expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
// });
// });
//
// describe('resolveBreadcrumbs', () => {
// it('should return the correct breadcrumbs', () => {
// const breadcrumbs = component.resolveBreadcrumbs(route.root);
// getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedBreadcrumbs });
// });
// });
});

View File

@@ -1,9 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
import { hasNoValue, hasValue, isUndefined } from '../shared/empty.util';
import { filter, map, switchMap, tap } from 'rxjs/operators';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { BreadcrumbsService } from './breadcrumbs.service';
import { Observable } from 'rxjs/internal/Observable';
/**
* Component representing the breadcrumbs of a page
@@ -13,7 +12,8 @@ import { combineLatest, Observable, of as observableOf } from 'rxjs';
templateUrl: './breadcrumbs.component.html',
styleUrls: ['./breadcrumbs.component.scss']
})
export class BreadcrumbsComponent implements OnInit {
export class BreadcrumbsComponent {
/**
* Observable of the list of breadcrumbs for this page
*/
@@ -22,61 +22,15 @@ export class BreadcrumbsComponent implements OnInit {
/**
* Whether or not to show breadcrumbs on this page
*/
showBreadcrumbs: boolean;
showBreadcrumbs$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
private router: Router
private router: Router,
private breadcrumbsService: BreadcrumbsService,
) {
this.breadcrumbs$ = breadcrumbsService.breadcrumbs$;
this.showBreadcrumbs$ = breadcrumbsService.showBreadcrumbs$;
}
/**
* Sets the breadcrumbs on init for this page
*/
ngOnInit(): void {
this.breadcrumbs$ = this.router.events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
tap(() => this.reset()),
switchMap(() => this.resolveBreadcrumbs(this.route.root)),
);
}
/**
* Method that recursively resolves breadcrumbs
* @param route The route to get the breadcrumb from
*/
resolveBreadcrumbs(route: ActivatedRoute): Observable<Breadcrumb[]> {
const data = route.snapshot.data;
const routeConfig = route.snapshot.routeConfig;
const last: boolean = hasNoValue(route.firstChild);
if (last) {
if (hasValue(data.showBreadcrumbs)) {
this.showBreadcrumbs = data.showBreadcrumbs;
} else if (isUndefined(data.breadcrumb)) {
this.showBreadcrumbs = false;
}
}
if (
hasValue(data) && hasValue(data.breadcrumb) &&
hasValue(routeConfig) && hasValue(routeConfig.resolve) && hasValue(routeConfig.resolve.breadcrumb)
) {
const { provider, key, url } = data.breadcrumb;
if (!last) {
return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumbs(route.firstChild))
.pipe(map((crumbs) => [].concat.apply([], crumbs)));
} else {
return provider.getBreadcrumbs(key, url);
}
}
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
}
/**
* Resets the state of the breadcrumbs
*/
reset() {
this.showBreadcrumbs = true;
}
}

View File

@@ -0,0 +1,116 @@
/* tslint:disable:max-classes-per-file */
import { TestBed } from '@angular/core/testing';
import { BreadcrumbsService } from './breadcrumbs.service';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRouteSnapshot, Resolve, Route, Router } from '@angular/router';
import { Component } from '@angular/core';
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
import { BreadcrumbsProviderService } from '../core/breadcrumbs/breadcrumbsProviderService';
import { Observable, of as observableOf } from 'rxjs';
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
/**
* Create the breadcrumbs
*/
class TestBreadcrumbsProviderService implements BreadcrumbsProviderService<string> {
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
return observableOf([new Breadcrumb(key, url)]);
}
}
/**
* Empty component used for every route
*/
@Component({ template: '' })
class DummyComponent {}
/**
* {@link BreadcrumbsService#resolveBreadcrumbs} requires that a breadcrumb resolver is present,
* or data.breadcrumb will be ignored.
* This class satisfies the requirement and sets data.breadcrumb.
*/
class TestBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
resolve(route: ActivatedRouteSnapshot): BreadcrumbConfig<string> {
console.log('route:', route);
return route.data.returnValueForTestBreadcrumbResolver;
}
}
describe('BreadcrumbsService', () => {
let service: BreadcrumbsService;
let router: any;
let breadcrumbProvider;
let breadcrumbConfigA: BreadcrumbConfig<string>;
let breadcrumbConfigB: BreadcrumbConfig<string>;
const initRoute = (path: string, showBreadcrumbs: boolean, breadcrumbConfig: BreadcrumbConfig<string>): Route => ({
path: path,
component: DummyComponent,
data: {
showBreadcrumbs: showBreadcrumbs,
returnValueForTestBreadcrumbResolver: breadcrumbConfig,
},
resolve: {
breadcrumb: TestBreadcrumbResolver,
}
});
const initBreadcrumbs = () => {
breadcrumbProvider = new TestBreadcrumbsProviderService();
breadcrumbConfigA = { provider: breadcrumbProvider, key: 'example.path', url: 'example.com' };
breadcrumbConfigB = { provider: breadcrumbProvider, key: 'another.path', url: 'another.com' };
};
beforeEach(() => {
initBreadcrumbs();
TestBed.configureTestingModule({
providers: [
TestBreadcrumbResolver,
],
imports: [
RouterTestingModule.withRoutes([
initRoute('route-1', undefined, undefined),
initRoute('route-2', false, breadcrumbConfigA),
initRoute('route-3', true, breadcrumbConfigB),
]),
],
});
router = TestBed.inject(Router);
service = TestBed.inject(BreadcrumbsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('breadcrumbs$', () => {
it('should return a breadcrumb corresponding to the current route', () => {
// TODO
service.breadcrumbs$.subscribe((value) => {
console.log('TEST');
console.log(value);
});
router.navigate(['route-3']);
});
it('should change when the route changes', () => {
// TODO
});
});
describe('showBreadcrumbs$', () => {
describe('when the last part of the route has showBreadcrumbs in its data', () => {
it('should return that value', () => {
// TODO
});
});
describe('when the last part of the route has no breadcrumb in its data', () => {
it('should return false', () => {
// TODO
});
});
});
});

View File

@@ -0,0 +1,74 @@
import { Injectable } from '@angular/core';
import {combineLatest, Observable, of as observableOf, ReplaySubject} from 'rxjs';
import {Breadcrumb} from './breadcrumb/breadcrumb.model';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {filter, map, switchMap, tap} from 'rxjs/operators';
import {hasNoValue, hasValue, isUndefined} from '../shared/empty.util';
@Injectable({
providedIn: 'root'
})
export class BreadcrumbsService {
/**
* Observable of the list of breadcrumbs for this page
*/
breadcrumbs$: ReplaySubject<Breadcrumb[]> = new ReplaySubject(1);
/**
* Whether or not to show breadcrumbs on this page
*/
showBreadcrumbs$: ReplaySubject<boolean> = new ReplaySubject(1);
constructor(
private route: ActivatedRoute,
private router: Router,
) {
// supply events to this.breadcrumbs$
this.router.events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
tap(() => this.reset()),
switchMap(() => this.resolveBreadcrumbs(this.route.root)),
).subscribe(this.breadcrumbs$);
}
/**
* Method that recursively resolves breadcrumbs
* @param route The route to get the breadcrumb from
*/
private resolveBreadcrumbs(route: ActivatedRoute): Observable<Breadcrumb[]> {
const data = route.snapshot.data;
const routeConfig = route.snapshot.routeConfig;
const last: boolean = hasNoValue(route.firstChild);
if (last) {
if (hasValue(data.showBreadcrumbs)) {
this.showBreadcrumbs$.next(data.showBreadcrumbs);
} else if (isUndefined(data.breadcrumb)) {
this.showBreadcrumbs$.next(false);
}
}
if (
hasValue(data) && hasValue(data.breadcrumb) &&
hasValue(routeConfig) && hasValue(routeConfig.resolve) && hasValue(routeConfig.resolve.breadcrumb)
) {
const { provider, key, url } = data.breadcrumb;
if (!last) {
return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumbs(route.firstChild))
.pipe(map((crumbs) => [].concat.apply([], crumbs)));
} else {
return provider.getBreadcrumbs(key, url);
}
}
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
}
/**
* Resets the state of the breadcrumbs
*/
private reset() {
this.showBreadcrumbs$.next(true);
}
}