From 77d991f033546c4214babab92b7e41fb83e06fde Mon Sep 17 00:00:00 2001 From: griest024 Date: Wed, 15 May 2024 09:59:59 -0400 Subject: [PATCH] feat(router): add service for observing route data (#2778) --- .../src/data/helpers/collect-data.spec.ts | 68 +++++++++++++++++++ libs/router/src/data/helpers/collect-data.ts | 24 +++++++ libs/router/src/data/helpers/public_api.ts | 1 + libs/router/src/data/public_api.ts | 2 + libs/router/src/data/service/data.service.ts | 52 ++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 libs/router/src/data/helpers/collect-data.spec.ts create mode 100644 libs/router/src/data/helpers/collect-data.ts create mode 100644 libs/router/src/data/helpers/public_api.ts create mode 100644 libs/router/src/data/public_api.ts create mode 100644 libs/router/src/data/service/data.service.ts diff --git a/libs/router/src/data/helpers/collect-data.spec.ts b/libs/router/src/data/helpers/collect-data.spec.ts new file mode 100644 index 0000000000..255c83af82 --- /dev/null +++ b/libs/router/src/data/helpers/collect-data.spec.ts @@ -0,0 +1,68 @@ +import { ActivatedRouteSnapshot } from '@angular/router'; + +import { daffRouterDataCollect } from './collect-data'; + +describe('@daffodil/router | daffRouterDataCollect', () => { + let route: ActivatedRouteSnapshot; + let result: any; + + beforeEach(() => { + route = { + data: { + root: 'TestClass', + overwrite: 'TestClass', + }, + children: [ + { + data: { + '00': 'TestClass', + }, + children: [ + { + data: { + 10: 'TestClass', + }, + children: [], + }, + { + data: { + 11: 'TestClass', + overwrite: 'TestClass1', + }, + children: [ + { + data: { + 20: 'TestClass', + }, + children: [], + }, + ], + }, + ], + }, + { + data: { + '01': 'TestClass', + }, + children: [], + }, + ], + }; + + result = daffRouterDataCollect(route); + }); + + it('should collect all the named views and combine them into a single dict', () => { + expect(result['root']).toBeDefined(); + expect(result['00']).toBeDefined(); + expect(result['01']).toBeDefined(); + expect(result[10]).toBeDefined(); + expect(result[11]).toBeDefined(); + expect(result[20]).toBeDefined(); + expect(result['overwrite']).toBeDefined(); + }); + + it('should give precedence to more deeply nested routes when there is a collision', () => { + expect(result['overwrite']).toEqual('TestClass1'); + }); +}); diff --git a/libs/router/src/data/helpers/collect-data.ts b/libs/router/src/data/helpers/collect-data.ts new file mode 100644 index 0000000000..8d9112d62d --- /dev/null +++ b/libs/router/src/data/helpers/collect-data.ts @@ -0,0 +1,24 @@ +import { + ActivatedRouteSnapshot, + Route, +} from '@angular/router'; + +import { collect } from '@daffodil/core'; + +/** + * Collects data defined in the entire tree of routes. + * Shallow merges data, preferring fields of more deeply nested routes. + */ +export const daffRouterDataCollect = (route: ActivatedRouteSnapshot): Route['data'] => { + const ary = collect(route, (r) => r.children); + const ret = ary.reduce( + (acc, r) => r.data + ? { + ...acc, + ...r.data, + } + : acc, + {}, + ); + return ret; +}; diff --git a/libs/router/src/data/helpers/public_api.ts b/libs/router/src/data/helpers/public_api.ts new file mode 100644 index 0000000000..bdd503e8cb --- /dev/null +++ b/libs/router/src/data/helpers/public_api.ts @@ -0,0 +1 @@ +export { daffRouterNamedViewsCollect } from './collect-data'; diff --git a/libs/router/src/data/public_api.ts b/libs/router/src/data/public_api.ts new file mode 100644 index 0000000000..e04c890d5c --- /dev/null +++ b/libs/router/src/data/public_api.ts @@ -0,0 +1,2 @@ +export * from './helpers/public_api'; +export * from './service/data.service'; diff --git a/libs/router/src/data/service/data.service.ts b/libs/router/src/data/service/data.service.ts new file mode 100644 index 0000000000..81e9d83b4a --- /dev/null +++ b/libs/router/src/data/service/data.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRoute, + NavigationEnd, + Route, + Router, +} from '@angular/router'; +import { + Observable, + filter, + map, + merge, +} from 'rxjs'; + +import { daffRouterDataCollect } from '../helpers/collect-data'; + +@Injectable({ + providedIn: 'root', +}) +export class DaffRouterDataService { + /** + * A collection of all the route data defined in any part of the currently activated route's tree. + * Child route's data takes precendence over parent data. + */ + public data$: Observable; + + constructor( + private route: ActivatedRoute, + private router: Router, + ) { + /** + * Because data won't reemit for route changes and + * the top-level data probably won't have named views + * anyway, use `url` and router events to listen for route changes + * and pull named views from nested data in the snapshot. + * + * On first page load, this directive will likely not be instantiated + * in time to catch router events so route.url emits for this case. + * On subsequent route changes, `route.url` will not change (why????) + * so we use router events instead. + */ + this.data$ = merge( + this.router.events.pipe( + filter((e) => e instanceof NavigationEnd), + ), + this.route.url, + ).pipe( + map(() => this.route.snapshot), + map(daffRouterDataCollect), + ); + } +}