Skip to content

Commit d594fa6

Browse files
authored
feat: add RouterStore#selectRouterEvents selector factory (#304)
# Features - Add `RouterStore#selectRouterEvents` selector factory
1 parent 60a918a commit d594fa6

File tree

8 files changed

+497
-84
lines changed

8 files changed

+497
-84
lines changed

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,19 @@ Published with partial Ivy compilation.
2121

2222
A `RouterStore` service has the following public properties.
2323

24-
| API | Description |
25-
| ----------------------------------------------------------------------- | ------------------------------------------ |
26-
| `currentRoute$: Observable<MinimalActivatedRouteSnapshot>` | Select the current route. |
27-
| `fragment$: Observable<string \| null>` | Select the current route fragment. |
28-
| `queryParams$: Observable<Params>` | Select the current route query parameters. |
29-
| `routeData$: Observable<Data>` | Select the current route data. |
30-
| `routeParams$: Observable<Params>` | Select the current route parameters. |
31-
| `title$: Observable<string \| undefined>` | Select the resolved route title. |
32-
| `url$: Observable<string>` | Select the current URL. |
33-
| `selectQueryParam(param: string): Observable<string \| undefined>` | Select the specified query parameter. |
34-
| `selectRouteData<TValue>(key: string): Observable<TValue \| undefined>` | Select the specified route data. |
35-
| `selectRouteParam(param: string): Observable<string \| undefined>` | Select the specified route parameter. |
24+
| API | Description |
25+
| ------------------------------------------------------------------------------------- | --------------------------------------------------------- |
26+
| `currentRoute$: Observable<MinimalActivatedRouteSnapshot>` | Select the current route. |
27+
| `fragment$: Observable<string \| null>` | Select the current route fragment. |
28+
| `queryParams$: Observable<Params>` | Select the current route query parameters. |
29+
| `routeData$: Observable<Data>` | Select the current route data. |
30+
| `routeParams$: Observable<Params>` | Select the current route parameters. |
31+
| `title$: Observable<string \| undefined>` | Select the resolved route title. |
32+
| `url$: Observable<string>` | Select the current URL. |
33+
| `selectQueryParam(param: string): Observable<string \| undefined>` | Select the specified query parameter. |
34+
| `selectRouteData<TValue>(key: string): Observable<TValue \| undefined>` | Select the specified route data. |
35+
| `selectRouteParam(param: string): Observable<string \| undefined>` | Select the specified route parameter. |
36+
| `selectRouterEvents(...acceptedRouterEvents: RouterEvent[]): Observable<RouterEvent>` | Select router events of the specified router event types. |
3637

3738
A `RouterStore` service is provided by using either `provideGlobalRouterStore` or `provideLocalRouterStore`.
3839

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { Component, NgZone } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
import {
4+
NavigationEnd,
5+
NavigationStart,
6+
Router,
7+
RoutesRecognized,
8+
} from '@angular/router';
9+
import { RouterTestingModule } from '@angular/router/testing';
10+
import { firstValueFrom, take, toArray } from 'rxjs';
11+
import { filterRouterEvents } from './filter-router-event.operator';
12+
13+
@Component({
14+
standalone: true,
15+
template: '',
16+
})
17+
class DummyTestComponent {}
18+
19+
const navigationEnd = new NavigationEnd(1, '/', '/');
20+
const navigationStart = new NavigationStart(1, '/');
21+
const routesRecognized = new RoutesRecognized(
22+
1,
23+
'/',
24+
'/',
25+
expect.objectContaining({
26+
url: '/',
27+
})
28+
);
29+
30+
describe(filterRouterEvents.name, () => {
31+
function setup({
32+
assertions = 1,
33+
}: {
34+
readonly assertions?: number;
35+
} = {}) {
36+
expect.assertions(assertions);
37+
38+
TestBed.configureTestingModule({
39+
imports: [
40+
RouterTestingModule.withRoutes([
41+
{
42+
path: 'test',
43+
component: DummyTestComponent,
44+
},
45+
]),
46+
],
47+
});
48+
49+
const ngZone = TestBed.inject(NgZone);
50+
const router = TestBed.inject(Router);
51+
const routerEvents = router.events;
52+
53+
return {
54+
navigateByUrl(url: string) {
55+
return ngZone.run(() => router.navigateByUrl(url));
56+
},
57+
routerEvents,
58+
};
59+
}
60+
61+
it('filters 1 router event type', async () => {
62+
const { navigateByUrl, routerEvents } = setup();
63+
const navigationStart$ = routerEvents.pipe(
64+
filterRouterEvents(NavigationStart)
65+
);
66+
const whenNavigationStart = firstValueFrom(navigationStart$);
67+
68+
await navigateByUrl('/');
69+
70+
await expect(whenNavigationStart).resolves.toEqual(navigationStart);
71+
});
72+
73+
it('filters 2 router event types', async () => {
74+
const { navigateByUrl, routerEvents } = setup();
75+
const navigation$ = routerEvents.pipe(
76+
filterRouterEvents(NavigationStart, NavigationEnd)
77+
);
78+
const whenNavigation = firstValueFrom(navigation$.pipe(take(2), toArray()));
79+
80+
await navigateByUrl('/');
81+
82+
await expect(whenNavigation).resolves.toEqual([
83+
navigationStart,
84+
navigationEnd,
85+
]);
86+
});
87+
88+
it('filters 3 router event types', async () => {
89+
const { navigateByUrl, routerEvents } = setup();
90+
const navigation$ = routerEvents.pipe(
91+
filterRouterEvents(NavigationStart, RoutesRecognized, NavigationEnd)
92+
);
93+
const whenNavigation = firstValueFrom(navigation$.pipe(take(3), toArray()));
94+
95+
await navigateByUrl('/');
96+
97+
await expect(whenNavigation).resolves.toEqual([
98+
navigationStart,
99+
routesRecognized,
100+
navigationEnd,
101+
]);
102+
});
103+
104+
it('filters multiple events of 1 router event type', async () => {
105+
const { navigateByUrl, routerEvents } = setup();
106+
const navigationStart$ = routerEvents.pipe(
107+
filterRouterEvents(NavigationStart)
108+
);
109+
const whenNavigationStart = firstValueFrom(
110+
navigationStart$.pipe(take(2), toArray())
111+
);
112+
113+
await navigateByUrl('/');
114+
await navigateByUrl('/test');
115+
116+
await expect(whenNavigationStart).resolves.toEqual([
117+
navigationStart,
118+
new NavigationStart(2, '/test'),
119+
]);
120+
});
121+
122+
it('filters multiple events of 2 router event types', async () => {
123+
const { navigateByUrl, routerEvents } = setup();
124+
const navigation$ = routerEvents.pipe(
125+
filterRouterEvents(NavigationStart, NavigationEnd)
126+
);
127+
const whenNavigation = firstValueFrom(navigation$.pipe(take(4), toArray()));
128+
129+
await navigateByUrl('/');
130+
await navigateByUrl('/test');
131+
132+
await expect(whenNavigation).resolves.toEqual([
133+
navigationStart,
134+
navigationEnd,
135+
new NavigationStart(2, '/test'),
136+
new NavigationEnd(2, '/test', '/test'),
137+
]);
138+
});
139+
140+
it('filters multiple events of 3 router event types', async () => {
141+
const { navigateByUrl, routerEvents } = setup();
142+
const navigation$ = routerEvents.pipe(
143+
filterRouterEvents(NavigationStart, RoutesRecognized, NavigationEnd)
144+
);
145+
const whenNavigation = firstValueFrom(navigation$.pipe(take(6), toArray()));
146+
147+
await navigateByUrl('/');
148+
await navigateByUrl('/test');
149+
150+
await expect(whenNavigation).resolves.toEqual([
151+
navigationStart,
152+
routesRecognized,
153+
navigationEnd,
154+
new NavigationStart(2, '/test'),
155+
new RoutesRecognized(
156+
2,
157+
'/test',
158+
'/test',
159+
expect.objectContaining({ url: '/test' })
160+
),
161+
new NavigationEnd(2, '/test', '/test'),
162+
]);
163+
});
164+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Type } from '@angular/core';
2+
import { Event as RouterEvent } from '@angular/router';
3+
import { filter, OperatorFunction } from 'rxjs';
4+
5+
/**
6+
* Narrow a stream of router events to the specified router event types.
7+
*
8+
* @param acceptedEventTypes The types of router events to accept.
9+
*
10+
* @example <caption>Usage</caption>
11+
* const navigation$ = router.events.pipe(
12+
* filterRouterEvents(NavigationStart, NavigationEnd),
13+
* );
14+
*/
15+
export function filterRouterEvents<
16+
TAcceptedRouterEvents extends Type<RouterEvent>[]
17+
>(
18+
...acceptedEventTypes: [...TAcceptedRouterEvents]
19+
): OperatorFunction<RouterEvent, InstanceType<TAcceptedRouterEvents[number]>> {
20+
return filter((event: RouterEvent) =>
21+
acceptedEventTypes.some((eventType) => event instanceof eventType)
22+
) as OperatorFunction<
23+
RouterEvent,
24+
InstanceType<TAcceptedRouterEvents[number]>
25+
>;
26+
}

0 commit comments

Comments
 (0)