1
1
import { inject , Injectable , Provider } from '@angular/core' ;
2
- import {
3
- Navigation ,
4
- NavigationEnd ,
5
- NavigationStart ,
6
- Router ,
7
- } from '@angular/router' ;
2
+ import { NavigationEnd , NavigationStart , Router } from '@angular/router' ;
8
3
import { ComponentStore , provideComponentStore } from '@ngrx/component-store' ;
9
- import { concatMap , filter , Observable , take } from 'rxjs' ;
10
-
11
- interface RouterHistoryRecord {
12
- readonly id : number ;
13
- readonly url : string ;
14
- }
4
+ import { filter , Observable } from 'rxjs' ;
15
5
16
6
interface RouterHistoryState {
17
- readonly currentIndex : number ;
18
- readonly event ?: NavigationStart | NavigationEnd ;
19
- readonly history : readonly RouterHistoryRecord [ ] ;
20
- readonly id : number ;
21
- readonly idToRestore ?: number ;
22
- readonly trigger ?: Navigation [ 'trigger' ] ;
7
+ readonly history : NavigationHistory ;
23
8
}
24
9
10
+ type CompleteNavigation = readonly [ NavigationStart , NavigationEnd ] ;
11
+ type NavigationHistory = Record < number , NavigationSequence > ;
12
+ type NavigationSequence = PendingNavigation | CompleteNavigation ;
13
+ type PendingNavigation = readonly [ NavigationStart ] ;
14
+
25
15
export function provideRouterHistoryStore ( ) : Provider [ ] {
26
16
return [ provideComponentStore ( RouterHistoryStore ) ] ;
27
17
}
@@ -59,11 +49,8 @@ export function provideRouterHistoryStore(): Provider[] {
59
49
export class RouterHistoryStore extends ComponentStore < RouterHistoryState > {
60
50
#router = inject ( Router ) ;
61
51
62
- #currentIndex$: Observable < number > = this . select (
63
- ( state ) => state . currentIndex
64
- ) ;
65
- #history$: Observable < readonly RouterHistoryRecord [ ] > = this . select (
66
- ( state ) => state . history
52
+ #history$ = this . select ( ( state ) => state . history ) . pipe (
53
+ filter ( ( history ) => Object . keys ( history ) . length > 0 )
67
54
) ;
68
55
#navigationEnd$: Observable < NavigationEnd > = this . #router. events . pipe (
69
56
filter ( ( event ) : event is NavigationEnd => event instanceof NavigationEnd )
@@ -73,43 +60,49 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
73
60
( event ) : event is NavigationStart => event instanceof NavigationStart
74
61
)
75
62
) ;
76
- #imperativeNavigationEnd$: Observable < NavigationEnd > =
77
- this . #navigationStart$. pipe (
78
- filter ( ( event ) => event . navigationTrigger === 'imperative' ) ,
79
- concatMap ( ( ) => this . #navigationEnd$. pipe ( take ( 1 ) ) )
80
- ) ;
81
- #popstateNavigationEnd$: Observable < NavigationEnd > =
82
- this . #navigationStart$. pipe (
83
- filter ( ( event ) => event . navigationTrigger === 'popstate' ) ,
84
- concatMap ( ( ) => this . #navigationEnd$. pipe ( take ( 1 ) ) )
85
- ) ;
86
63
87
- currentUrl$ : Observable < string > = this . select (
88
- this . #navigationEnd$. pipe (
89
- concatMap ( ( ) =>
90
- this . select (
91
- this . #currentIndex$,
92
- this . #history$,
93
- ( currentIndex , history ) => [ currentIndex , history ] as const
94
- )
95
- )
96
- ) ,
97
- ( [ currentIndex , history ] ) => history [ currentIndex ] . url ,
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
+ )
71
+ ) ;
72
+ #latestCompletedNavigation$ = this . select (
73
+ this . #maxCompletedNavigationId$,
74
+ this . #history$,
75
+ ( maxCompletedNavigationId , history ) =>
76
+ history [ maxCompletedNavigationId ] as CompleteNavigation ,
98
77
{
99
78
debounce : true ,
100
79
}
101
80
) ;
102
- previousUrl$ : Observable < string | null > = this . select (
103
- this . #navigationEnd$. pipe (
104
- concatMap ( ( ) =>
105
- this . select (
106
- this . #currentIndex$,
107
- this . #history$,
108
- ( currentIndex , history ) => [ currentIndex , history ] as const
109
- )
110
- )
111
- ) ,
112
- ( [ currentIndex , history ] ) => history [ currentIndex - 1 ] ?. url ?? null ,
81
+
82
+ currentUrl$ : Observable < string > = this . select (
83
+ this . #latestCompletedNavigation$,
84
+ ( [ , end ] ) => end . urlAfterRedirects
85
+ ) ;
86
+ previousUrl$ : Observable < string | undefined > = this . select (
87
+ this . #history$,
88
+ this . #maxCompletedNavigationId$,
89
+ ( history , maxCompletedNavigationId ) => {
90
+ if ( maxCompletedNavigationId === 1 ) {
91
+ return undefined ;
92
+ }
93
+
94
+ const [ completedNavigationSourceStart ] = this . #getNavigationSource(
95
+ maxCompletedNavigationId ,
96
+ history
97
+ ) ;
98
+ const previousNavigationId = completedNavigationSourceStart . id - 1 ;
99
+ const [ , previousNavigationSourceEnd ] = this . #getNavigationSource(
100
+ previousNavigationId ,
101
+ history
102
+ ) ;
103
+
104
+ return previousNavigationSourceEnd . urlAfterRedirects ;
105
+ } ,
113
106
{
114
107
debounce : true ,
115
108
}
@@ -118,95 +111,49 @@ export class RouterHistoryStore extends ComponentStore<RouterHistoryState> {
118
111
constructor ( ) {
119
112
super ( initialState ) ;
120
113
121
- this . #updateRouterHistoryOnNavigationStart( this . #navigationStart$) ;
122
- this . #updateRouterHistoryOnImperativeNavigationEnd(
123
- this . #imperativeNavigationEnd$
124
- ) ;
125
- this . #updateRouterHistoryOnPopstateNavigationEnd(
126
- this . #popstateNavigationEnd$
127
- ) ;
114
+ this . #addNavigationStart( this . #navigationStart$) ;
115
+ this . #addNavigationEnd( this . #navigationEnd$) ;
128
116
}
129
117
130
- /**
131
- * Update router history on imperative navigation end (`Router#navigate`,
132
- * `Router#navigateByUrl`, or `RouterLink` click).
133
- */
134
- #updateRouterHistoryOnImperativeNavigationEnd = this . updater < NavigationEnd > (
135
- ( state , event ) : RouterHistoryState => {
136
- let currentIndex = state . currentIndex ;
137
- let history = state . history ;
138
- // remove all events in history that come after the current index
139
- history = [
140
- ...history . slice ( 0 , currentIndex + 1 ) ,
141
- // add the new event to the end of the history
142
- {
143
- id : state . id ,
144
- url : event . urlAfterRedirects ,
145
- } ,
146
- ] ;
147
- // set the new event as our current history index
148
- currentIndex = history . length - 1 ;
149
-
150
- return {
151
- ...state ,
152
- currentIndex,
153
- event,
154
- history,
155
- } ;
156
- }
118
+ #addNavigationEnd = this . updater < NavigationEnd > (
119
+ ( state , event ) : RouterHistoryState => ( {
120
+ ...state ,
121
+ history : {
122
+ ...state . history ,
123
+ [ event . id ] : [ state . history [ event . id ] [ 0 ] , event ] ,
124
+ } ,
125
+ } )
157
126
) ;
158
127
159
- #updateRouterHistoryOnNavigationStart = this . updater < NavigationStart > (
128
+ #addNavigationStart = this . updater < NavigationStart > (
160
129
( state , event ) : RouterHistoryState => ( {
161
130
...state ,
162
- id : event . id ,
163
- idToRestore : event . restoredState ?. navigationId ?? undefined ,
164
- event,
165
- trigger : event . navigationTrigger ,
131
+ history : {
132
+ ... state . history ,
133
+ [ event . id ] : [ event ] ,
134
+ } ,
166
135
} )
167
136
) ;
168
137
169
- /**
170
- * Update router history on browser navigation end (back, forward, and other
171
- * `popstate` or `pushstate` events).
172
- */
173
- #updateRouterHistoryOnPopstateNavigationEnd = this . updater < NavigationEnd > (
174
- ( state , event ) : RouterHistoryState => {
175
- let currentIndex = 0 ;
176
- let { history } = state ;
177
- // get the history item that references the idToRestore
178
- const historyIndexToRestore = history . findIndex (
179
- ( historyRecord ) => historyRecord . id === state . idToRestore
180
- ) ;
181
-
182
- // if found, set the current index to that history item and update the id
183
- if ( historyIndexToRestore > - 1 ) {
184
- currentIndex = historyIndexToRestore ;
185
- history = [
186
- ...history . slice ( 0 , historyIndexToRestore ) ,
187
- {
188
- ...history [ historyIndexToRestore ] ,
189
- id : state . id ,
190
- } ,
191
- ...history . slice ( historyIndexToRestore + 1 ) ,
138
+ #getNavigationSource(
139
+ navigationId : number ,
140
+ history : NavigationHistory
141
+ ) : CompleteNavigation {
142
+ let navigation = history [ navigationId ] ;
143
+
144
+ while ( navigation [ 0 ] . navigationTrigger === 'popstate' ) {
145
+ navigation =
146
+ history [
147
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148
+ navigation [ 0 ] . restoredState ! . navigationId
192
149
] ;
193
- }
194
-
195
- return {
196
- ...state ,
197
- currentIndex,
198
- event,
199
- history,
200
- } ;
150
+ navigationId = navigation [ 0 ] . id ;
201
151
}
202
- ) ;
152
+
153
+ return navigation as CompleteNavigation ;
154
+ }
203
155
}
204
156
205
157
export const initialState : RouterHistoryState = {
206
- currentIndex : 0 ,
207
- event : undefined ,
208
158
history : [ ] ,
209
- id : 0 ,
210
- idToRestore : 0 ,
211
- trigger : undefined ,
212
159
} ;
0 commit comments