Skip to content

Commit b11d897

Browse files
authored
feat!: use local route for local router store (#309)
This change in implementation will make the local router store more closely match `ActivatedRoute` while the global router store matches NgRx Router Store selectors. Through complex route configurations, the router store implementations are exercised to identify edge case differences between them and any breaking changes introduced to the local router store. ## Features - `LocalRouterStore` matches `ActivatedRoute` more closely - Use `ActivatedRoute` to serialize the router state for the local router store implementation (`LocalRouterStore`) - `LocalRouterStore.currentRoute$` matches `ActivatedRoute.snapshot` ## Testing - Use Spectactular (15.0) feature harnesses - Compare `LocalRouterStore` selectors to `ActivatedRoute` observables - Compare `GlobalRouterStore` selectors to NgRx Router Store selectors and `Router#events` - Cover nested and componentless routes with route data - Cover nested and componentless routes with route parameters - Cover route fragment - Cover current route for nested routes - Cover nested route URLs - Cover nested route titles **BREAKING CHANGES** BEFORE: *(Router Component Store <=0.3.2)* ```typescript // URL: /parent/child/grandchild @component({ /* (...) */ providers: [provideLocalRouterStore()], }) export class ChildComponent implements OnInit { #route = inject(ActivatedRoute); #routerStore = inject(RouterStore); ngOnInit() { const currentRouteSnapshot = this.#route.snapshot; console.log(currentRouteSnapshot.routeConfig.path); // -> "child" console.log(currentRouteSnapshot.url[0].path); // -> "child" firstValueFrom(this.#routerStore.currentRoute$) .then(currentRoute => { console.log(currentRoute.routeConfig.path); // -> "grandchild" console.log(currentRoute.url[0].path); // -> "grandchild" }); } } ``` AFTER: *(Router Component Store >0.3.2)* ```typescript // URL: /parent/child/grandchild @component({ /* (...) */ providers: [provideLocalRouterStore()], }) export class ChildComponent implements OnInit { #route = inject(ActivatedRoute); #routerStore = inject(RouterStore); ngOnInit() { const currentRouteSnapshot = this.#route.snapshot; console.log(currentRouteSnapshot.routeConfig.path); // -> "child" console.log(currentRouteSnapshot.url[0].path); // -> "child" firstValueFrom(this.#routerStore.currentRoute$) .then(currentRoute => { console.log(currentRoute.routeConfig.path); // -> "child" console.log(currentRoute.url[0].path); // -> "child" }); } } ```
2 parents e9d90fa + cc98f5f commit b11d897

27 files changed

Lines changed: 4474 additions & 560 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@angular/compiler-cli": "15.0.4",
4242
"@angular/language-service": "15.0.4",
4343
"@ngrx/eslint-plugin": "15.0.0",
44+
"@ngworker/spectacular": "15.0.0",
4445
"@nrwl/cli": "15.3.3",
4546
"@nrwl/eslint-plugin-nx": "15.3.3",
4647
"@nrwl/jest": "15.3.3",
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { RouterConfigOptions, Routes } from '@angular/router';
2+
import { firstValueFrom } from 'rxjs';
3+
import { MinimalRouteData } from '../minimal-route-data';
4+
import { RouterStore } from '../router-store';
5+
import { GlobalRouterStore } from './global-router-store';
6+
import { globalRouterStoreSetup } from './test-util/global-router-store-setup';
7+
import {
8+
GlobalRouterStoreTestChildComponent,
9+
GlobalRouterStoreTestGrandchildComponent,
10+
GlobalRouterStoreTestParentComponent,
11+
} from './test-util/global-router-store-test-components';
12+
13+
const routes: Routes = [
14+
{
15+
path: '',
16+
data: {
17+
componentlessBeforeParent: 'componentless-route-data-before-parent',
18+
shadowed: 'componentless-route-data-before-parent',
19+
},
20+
children: [
21+
{
22+
path: 'parent',
23+
component: GlobalRouterStoreTestParentComponent,
24+
data: {
25+
parent: 'parent-route-data',
26+
shadowed: 'parent-route-data',
27+
},
28+
children: [
29+
{
30+
path: '',
31+
data: {
32+
componentlessBeforeChild: 'componentless-route-data-before-child',
33+
shadowed: 'componentless-route-data-before-child',
34+
},
35+
children: [
36+
{
37+
path: 'child',
38+
component: GlobalRouterStoreTestChildComponent,
39+
data: {
40+
child: 'child-route-data',
41+
shadowed: 'child-route-data',
42+
},
43+
children: [
44+
{
45+
path: '',
46+
data: {
47+
componentlessBeforeGrandchild:
48+
'componentless-route-data-before-grandchild',
49+
shadowed: 'componentless-route-data-before-grandchild',
50+
},
51+
children: [
52+
{
53+
path: 'grandchild',
54+
component: GlobalRouterStoreTestGrandchildComponent,
55+
data: {
56+
grandchild: 'grandchild-route-data',
57+
shadowed: 'grandchild-route-data',
58+
},
59+
},
60+
],
61+
},
62+
],
63+
},
64+
],
65+
},
66+
],
67+
},
68+
],
69+
},
70+
];
71+
72+
describe(`${GlobalRouterStore.name} componentless nested route data`, () => {
73+
describe(`Given three layers of routes with components and route data
74+
And a componentless route with route data before each of them`, () => {
75+
const paramsInheritanceStrategies: RouterConfigOptions['paramsInheritanceStrategy'][] =
76+
['always', 'emptyOnly'];
77+
78+
describe.each(paramsInheritanceStrategies)(
79+
' And the "%s" route parameter inheritance strategy is used',
80+
(paramsInheritanceStrategy) => {
81+
it.each(
82+
[
83+
GlobalRouterStoreTestParentComponent,
84+
GlobalRouterStoreTestChildComponent,
85+
GlobalRouterStoreTestGrandchildComponent,
86+
].map((RoutedComponent) => ({ RoutedComponent }))
87+
)(
88+
` And ${RouterStore.name} is injected at $RoutedComponent.name
89+
When the ${GlobalRouterStoreTestGrandchildComponent.name} route is activated
90+
Then route data for the ${GlobalRouterStoreTestGrandchildComponent.name} route is emitted
91+
And componentless route data before the ${GlobalRouterStoreTestGrandchildComponent.name} is emitted
92+
And route data for the ${GlobalRouterStoreTestChildComponent.name} route is emitted
93+
And componentless route data before the ${GlobalRouterStoreTestChildComponent.name} is emitted
94+
And route data for the ${GlobalRouterStoreTestParentComponent.name} route is emitted
95+
And componentless route data before the ${GlobalRouterStoreTestParentComponent.name} is emitted`,
96+
async ({ RoutedComponent }) => {
97+
expect.assertions(3);
98+
const { componentStore, ngrxRouterStore, ngrxStore, routerStore } =
99+
await globalRouterStoreSetup({
100+
navigateTo: '/parent/child/grandchild',
101+
paramsInheritanceStrategy,
102+
RoutedComponent,
103+
routes,
104+
});
105+
106+
const expectedRouteData: MinimalRouteData = {
107+
componentlessBeforeParent:
108+
'componentless-route-data-before-parent',
109+
parent: 'parent-route-data',
110+
componentlessBeforeChild: 'componentless-route-data-before-child',
111+
child: 'child-route-data',
112+
componentlessBeforeGrandchild:
113+
'componentless-route-data-before-grandchild',
114+
grandchild: 'grandchild-route-data',
115+
shadowed: 'grandchild-route-data',
116+
};
117+
await expect(
118+
firstValueFrom(routerStore.routeData$)
119+
).resolves.toEqual(expectedRouteData);
120+
await expect(
121+
firstValueFrom(ngrxStore.select(ngrxRouterStore.selectRouteData))
122+
).resolves.toEqual(expectedRouteData);
123+
await expect(
124+
firstValueFrom(
125+
componentStore.select({
126+
componentlessBeforeParent: routerStore.selectRouteData(
127+
'componentlessBeforeParent'
128+
),
129+
parent: routerStore.selectRouteData('parent'),
130+
componentlessBeforeChild: routerStore.selectRouteData(
131+
'componentlessBeforeChild'
132+
),
133+
child: routerStore.selectRouteData('child'),
134+
componentlessBeforeGrandchild: routerStore.selectRouteData(
135+
'componentlessBeforeGrandchild'
136+
),
137+
grandchild: routerStore.selectRouteData('grandchild'),
138+
shadowed: routerStore.selectRouteData('shadowed'),
139+
})
140+
)
141+
).resolves.toEqual(expectedRouteData);
142+
}
143+
);
144+
145+
it.each(
146+
[
147+
GlobalRouterStoreTestParentComponent,
148+
GlobalRouterStoreTestChildComponent,
149+
].map((RoutedComponent) => ({
150+
RoutedComponent,
151+
}))
152+
)(
153+
` And ${RouterStore.name} is injected at $RoutedComponent.name
154+
When the ${GlobalRouterStoreTestChildComponent.name} route is activated
155+
Then componentless route data before the ${GlobalRouterStoreTestGrandchildComponent.name} is emitted
156+
And route data for the ${GlobalRouterStoreTestChildComponent.name} route is emitted
157+
And componentless route data before the ${GlobalRouterStoreTestChildComponent.name} is emitted
158+
And route data for the ${GlobalRouterStoreTestParentComponent.name} route is emitted
159+
And componentless route data before the ${GlobalRouterStoreTestParentComponent.name} is emitted`,
160+
async ({ RoutedComponent }) => {
161+
expect.assertions(3);
162+
const { componentStore, ngrxRouterStore, ngrxStore, routerStore } =
163+
await globalRouterStoreSetup({
164+
navigateTo: '/parent/child',
165+
paramsInheritanceStrategy,
166+
RoutedComponent,
167+
routes,
168+
});
169+
170+
const expectedRouteData: MinimalRouteData = {
171+
componentlessBeforeParent:
172+
'componentless-route-data-before-parent',
173+
parent: 'parent-route-data',
174+
componentlessBeforeChild: 'componentless-route-data-before-child',
175+
child: 'child-route-data',
176+
componentlessBeforeGrandchild:
177+
'componentless-route-data-before-grandchild',
178+
shadowed: 'componentless-route-data-before-grandchild',
179+
};
180+
await expect(
181+
firstValueFrom(routerStore.routeData$)
182+
).resolves.toEqual(expectedRouteData);
183+
await expect(
184+
firstValueFrom(ngrxStore.select(ngrxRouterStore.selectRouteData))
185+
).resolves.toEqual(expectedRouteData);
186+
await expect(
187+
firstValueFrom(
188+
componentStore.select({
189+
componentlessBeforeParent: routerStore.selectRouteData(
190+
'componentlessBeforeParent'
191+
),
192+
parent: routerStore.selectRouteData('parent'),
193+
componentlessBeforeChild: routerStore.selectRouteData(
194+
'componentlessBeforeChild'
195+
),
196+
child: routerStore.selectRouteData('child'),
197+
componentlessBeforeGrandchild: routerStore.selectRouteData(
198+
'componentlessBeforeGrandchild'
199+
),
200+
shadowed: routerStore.selectRouteData('shadowed'),
201+
})
202+
)
203+
).resolves.toEqual(expectedRouteData);
204+
}
205+
);
206+
207+
it(` And ${RouterStore.name} is injected at ${GlobalRouterStoreTestParentComponent.name}
208+
When the ${GlobalRouterStoreTestParentComponent.name} route is activated
209+
Then componentless route data before the ${GlobalRouterStoreTestChildComponent.name} is emitted
210+
And route data for the ${GlobalRouterStoreTestParentComponent.name} route is emitted
211+
And componentless route data before the ${GlobalRouterStoreTestParentComponent.name} is emitted`, async () => {
212+
expect.assertions(3);
213+
const { componentStore, ngrxRouterStore, ngrxStore, routerStore } =
214+
await globalRouterStoreSetup({
215+
navigateTo: '/parent',
216+
paramsInheritanceStrategy,
217+
RoutedComponent: GlobalRouterStoreTestParentComponent,
218+
routes,
219+
});
220+
221+
const expectedRouteData: MinimalRouteData = {
222+
componentlessBeforeParent: 'componentless-route-data-before-parent',
223+
parent: 'parent-route-data',
224+
componentlessBeforeChild: 'componentless-route-data-before-child',
225+
shadowed: 'componentless-route-data-before-child',
226+
};
227+
await expect(firstValueFrom(routerStore.routeData$)).resolves.toEqual(
228+
expectedRouteData
229+
);
230+
await expect(
231+
firstValueFrom(ngrxStore.select(ngrxRouterStore.selectRouteData))
232+
).resolves.toEqual(expectedRouteData);
233+
await expect(
234+
firstValueFrom(
235+
componentStore.select({
236+
componentlessBeforeParent: routerStore.selectRouteData(
237+
'componentlessBeforeParent'
238+
),
239+
parent: routerStore.selectRouteData('parent'),
240+
componentlessBeforeChild: routerStore.selectRouteData(
241+
'componentlessBeforeChild'
242+
),
243+
shadowed: routerStore.selectRouteData('shadowed'),
244+
})
245+
)
246+
).resolves.toEqual(expectedRouteData);
247+
});
248+
}
249+
);
250+
});
251+
});

0 commit comments

Comments
 (0)