1
- import { inject , Injectable , Provider } from '@angular/core' ;
1
+ import {
2
+ APP_INITIALIZER ,
3
+ FactoryProvider ,
4
+ inject ,
5
+ Injectable ,
6
+ Provider ,
7
+ } from '@angular/core' ;
2
8
import { NavigationEnd , NavigationStart , Router } from '@angular/router' ;
3
9
import { ComponentStore , provideComponentStore } from '@ngrx/component-store' ;
4
10
import { filter , Observable } from 'rxjs' ;
5
11
6
12
interface RouterHistoryState {
13
+ /**
14
+ * The history of all navigations.
15
+ */
7
16
readonly history : NavigationHistory ;
8
17
}
9
18
@@ -12,8 +21,17 @@ type NavigationHistory = Record<number, NavigationSequence>;
12
21
type NavigationSequence = PendingNavigation | CompleteNavigation ;
13
22
type PendingNavigation = readonly [ NavigationStart ] ;
14
23
24
+ /**
25
+ * Provide and initialize the `RouterHistoryStore`.
26
+ *
27
+ * @remarks
28
+ * Must be provided by the root injector to capture all navigation events.
29
+ */
15
30
export function provideRouterHistoryStore ( ) : Provider [ ] {
16
- return [ provideComponentStore ( RouterHistoryStore ) ] ;
31
+ return [
32
+ provideComponentStore ( RouterHistoryStore ) ,
33
+ routerHistoryStoreInitializer ,
34
+ ] ;
17
35
}
18
36
19
37
// TODO(@LayZeeDK): Handle `NavigationCancel` and `NavigationError` events
@@ -49,26 +67,45 @@ export function provideRouterHistoryStore(): Provider[] {
49
67
export class RouterHistoryStore extends ComponentStore < RouterHistoryState > {
50
68
#router = inject ( Router ) ;
51
69
70
+ /**
71
+ * The history of all navigations.
72
+ */
52
73
#history$ = this . select ( ( state ) => state . history ) . pipe (
53
74
filter ( ( history ) => Object . keys ( history ) . length > 0 )
54
75
) ;
76
+ /**
77
+ * All `NavigationEnd` events.
78
+ */
55
79
#navigationEnd$: Observable < NavigationEnd > = this . #router. events . pipe (
56
80
filter ( ( event ) : event is NavigationEnd => event instanceof NavigationEnd )
57
81
) ;
82
+ /**
83
+ * All `NavigationStart` events.
84
+ */
58
85
#navigationStart$: Observable < NavigationStart > = this . #router. events . pipe (
59
86
filter (
60
87
( event ) : event is NavigationStart => event instanceof NavigationStart
61
88
)
62
89
) ;
63
90
64
- #maxCompletedNavigationId$ = this . select ( this . #history$, ( history ) =>
65
- Number (
66
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
67
- Object . entries ( history )
68
- . reverse ( )
69
- . find ( ( [ , navigation ] ) => navigation . length === 2 ) ! [ 0 ]
70
- )
91
+ /**
92
+ * The navigation ID of the most recent completed navigation.
93
+ */
94
+ #maxCompletedNavigationId$ = this . select (
95
+ this . #history$. pipe ( filter ( ( history ) => ( history [ 1 ] ?? [ ] ) . length > 1 ) ) ,
96
+ ( history ) =>
97
+ Number (
98
+ // This callback is only triggered when at least one navigation has
99
+ // completed
100
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
101
+ Object . entries ( history )
102
+ . reverse ( )
103
+ . find ( ( [ , navigation ] ) => navigation . length === 2 ) ! [ 0 ]
104
+ )
71
105
) ;
106
+ /**
107
+ * The most recent completed navigation.
108
+ */
72
109
#latestCompletedNavigation$ = this . select (
73
110
this . #maxCompletedNavigationId$,
74
111
this . #history$,
@@ -79,10 +116,19 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
79
116
}
80
117
) ;
81
118
119
+ /**
120
+ * The current URL.
121
+ */
82
122
currentUrl$ : Observable < string > = this . select (
83
123
this . #latestCompletedNavigation$,
84
124
( [ , end ] ) => end . urlAfterRedirects
85
125
) ;
126
+ /**
127
+ * The previous URL when taking `popstate` events into account.
128
+ *
129
+ * `undefined` is emitted when the current navigation is the first in the
130
+ * navigation history.
131
+ */
86
132
previousUrl$ : Observable < string | undefined > = this . select (
87
133
this . #history$,
88
134
this . #maxCompletedNavigationId$,
@@ -95,6 +141,11 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
95
141
maxCompletedNavigationId ,
96
142
history
97
143
) ;
144
+
145
+ if ( completedNavigationSourceStart . id === 1 ) {
146
+ return undefined ;
147
+ }
148
+
98
149
const previousNavigationId = completedNavigationSourceStart . id - 1 ;
99
150
const [ , previousNavigationSourceEnd ] = this . #getNavigationSource(
100
151
previousNavigationId ,
@@ -115,6 +166,9 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
115
166
this . #addNavigationEnd( this . #navigationEnd$) ;
116
167
}
117
168
169
+ /**
170
+ * Add a `NavigationEnd` event to the navigation history.
171
+ */
118
172
#addNavigationEnd = this . updater < NavigationEnd > (
119
173
( state , event ) : RouterHistoryState => ( {
120
174
...state ,
@@ -125,6 +179,9 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
125
179
} )
126
180
) ;
127
181
182
+ /**
183
+ * Add a `NavigationStart` event to the navigation history.
184
+ */
128
185
#addNavigationStart = this . updater < NavigationStart > (
129
186
( state , event ) : RouterHistoryState => ( {
130
187
...state ,
@@ -135,6 +192,16 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
135
192
} )
136
193
) ;
137
194
195
+ /**
196
+ * Search the specified navigation history to find the source of the
197
+ * specified navigation event.
198
+ *
199
+ * This takes `popstate` navigation events into account.
200
+ *
201
+ * @param navigationId The ID of the navigation to trace.
202
+ * @param history The navigation history to search.
203
+ * @returns The source navigation.
204
+ */
138
205
#getNavigationSource(
139
206
navigationId : number ,
140
207
history : NavigationHistory
@@ -144,16 +211,37 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
144
211
while ( navigation [ 0 ] . navigationTrigger === 'popstate' ) {
145
212
navigation =
146
213
history [
214
+ // Navigation events triggered by `popstate` always have a
215
+ // `restoredState`
147
216
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148
217
navigation [ 0 ] . restoredState ! . navigationId
149
218
] ;
150
- navigationId = navigation [ 0 ] . id ;
151
219
}
152
220
153
221
return navigation as CompleteNavigation ;
154
222
}
155
223
}
156
224
157
- export const initialState : RouterHistoryState = {
225
+ /**
226
+ * The initial internal state of the `RouterHistoryStore`.
227
+ */
228
+ const initialState : RouterHistoryState = {
158
229
history : [ ] ,
159
230
} ;
231
+
232
+ const initializeRouterHistoryStoreFactory =
233
+ // Inject the RouterHistoryStore to eagerly initialize it.
234
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
235
+ ( _initializedRouterHistoryStore : RouterHistoryStore ) => ( ) : void => undefined ;
236
+ /**
237
+ * Eagerly initialize the `RouterHistoryStore` to subscribe to all relevant
238
+ * router navigation events.
239
+ */
240
+ const routerHistoryStoreInitializer : FactoryProvider = {
241
+ provide : APP_INITIALIZER ,
242
+ multi : true ,
243
+ deps : [ RouterHistoryStore ] ,
244
+ useFactory :
245
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
246
+ initializeRouterHistoryStoreFactory ,
247
+ } ;
0 commit comments