@@ -19,6 +19,7 @@ import {
19
19
NavigationError ,
20
20
NavigationSkipped ,
21
21
NavigationStart ,
22
+ NavigationTrigger ,
22
23
PrivateRouterEvents ,
23
24
RoutesRecognized ,
24
25
} from '../events' ;
@@ -30,6 +31,15 @@ import {UrlSerializer, UrlTree} from '../url_tree';
30
31
31
32
@Injectable ( { providedIn : 'root' , useFactory : ( ) => inject ( HistoryStateManager ) } )
32
33
export abstract class StateManager {
34
+ protected readonly urlSerializer = inject ( UrlSerializer ) ;
35
+ private readonly options = inject ( ROUTER_CONFIGURATION , { optional : true } ) || { } ;
36
+ protected readonly canceledNavigationResolution =
37
+ this . options . canceledNavigationResolution || 'replace' ;
38
+ protected location = inject ( Location ) ;
39
+ protected urlHandlingStrategy = inject ( UrlHandlingStrategy ) ;
40
+ protected urlUpdateStrategy = this . options . urlUpdateStrategy || 'deferred' ;
41
+
42
+ private currentUrlTree = new UrlTree ( ) ;
33
43
/**
34
44
* Returns the currently activated `UrlTree`.
35
45
*
@@ -39,8 +49,11 @@ export abstract class StateManager {
39
49
* The value is set after finding the route config tree to activate but before activating the
40
50
* route.
41
51
*/
42
- abstract getCurrentUrlTree ( ) : UrlTree ;
52
+ getCurrentUrlTree ( ) : UrlTree {
53
+ return this . currentUrlTree ;
54
+ }
43
55
56
+ private rawUrlTree = this . currentUrlTree ;
44
57
/**
45
58
* Returns a `UrlTree` that is represents what the browser is actually showing.
46
59
*
@@ -66,21 +79,80 @@ export abstract class StateManager {
66
79
* location change listener due to a URL update by the AngularJS router. In this case, the router
67
80
* still need to know what the browser's URL is for future navigations.
68
81
*/
69
- abstract getRawUrlTree ( ) : UrlTree ;
82
+ getRawUrlTree ( ) : UrlTree {
83
+ return this . rawUrlTree ;
84
+ }
70
85
71
- /** Returns the current state stored by the browser for the current history entry. */
72
- abstract restoredState ( ) : RestoredState | null | undefined ;
86
+ protected createBrowserPath ( { finalUrl, initialUrl, targetBrowserUrl} : Navigation ) : string {
87
+ const rawUrl =
88
+ finalUrl !== undefined ? this . urlHandlingStrategy . merge ( finalUrl ! , initialUrl ) : initialUrl ;
89
+ const url = targetBrowserUrl ?? rawUrl ;
90
+ const path = url instanceof UrlTree ? this . urlSerializer . serialize ( url ) : url ;
91
+ return path ;
92
+ }
93
+
94
+ protected commitTransition ( { targetRouterState, finalUrl, initialUrl} : Navigation ) {
95
+ // If we are committing the transition after having a final URL and target state, we're updating
96
+ // all pieces of the state. Otherwise, we likely skipped the transition (due to URL handling strategy)
97
+ // and only want to update the rawUrlTree, which represents the browser URL (and doesn't necessarily match router state).
98
+ if ( finalUrl && targetRouterState ) {
99
+ this . currentUrlTree = finalUrl ;
100
+ this . rawUrlTree = this . urlHandlingStrategy . merge ( finalUrl , initialUrl ) ;
101
+ this . routerState = targetRouterState ;
102
+ } else {
103
+ this . rawUrlTree = initialUrl ;
104
+ }
105
+ }
106
+
107
+ private routerState = createEmptyState ( null ) ;
73
108
74
109
/** Returns the current RouterState. */
75
- abstract getRouterState ( ) : RouterState ;
110
+ getRouterState ( ) : RouterState {
111
+ return this . routerState ;
112
+ }
113
+
114
+ private stateMemento = this . createStateMemento ( ) ;
115
+
116
+ protected updateStateMemento ( ) {
117
+ this . stateMemento = this . createStateMemento ( ) ;
118
+ }
119
+
120
+ private createStateMemento ( ) {
121
+ return {
122
+ rawUrlTree : this . rawUrlTree ,
123
+ currentUrlTree : this . currentUrlTree ,
124
+ routerState : this . routerState ,
125
+ } ;
126
+ }
127
+
128
+ protected resetInternalState ( { finalUrl} : Navigation ) : void {
129
+ this . routerState = this . stateMemento . routerState ;
130
+ this . currentUrlTree = this . stateMemento . currentUrlTree ;
131
+ // Note here that we use the urlHandlingStrategy to get the reset `rawUrlTree` because it may be
132
+ // configured to handle only part of the navigation URL. This means we would only want to reset
133
+ // the part of the navigation handled by the Angular router rather than the whole URL. In
134
+ // addition, the URLHandlingStrategy may be configured to specifically preserve parts of the URL
135
+ // when merging, such as the query params so they are not lost on a refresh.
136
+ this . rawUrlTree = this . urlHandlingStrategy . merge (
137
+ this . currentUrlTree ,
138
+ finalUrl ?? this . rawUrlTree ,
139
+ ) ;
140
+ }
141
+
142
+ /** Returns the current state stored by the browser for the current history entry. */
143
+ abstract restoredState ( ) : RestoredState | null | undefined ;
76
144
77
145
/**
78
146
* Registers a listener that is called whenever the current history entry changes by some API
79
147
* outside the Router. This includes user-activated changes like back buttons and link clicks, but
80
148
* also includes programmatic APIs called by non-Router JavaScript.
81
149
*/
82
150
abstract registerNonRouterCurrentEntryChangeListener (
83
- listener : ( url : string , state : RestoredState | null | undefined ) => void ,
151
+ listener : (
152
+ url : string ,
153
+ state : RestoredState | null | undefined ,
154
+ trigger : NavigationTrigger ,
155
+ ) => void ,
84
156
) : SubscriptionLike ;
85
157
86
158
/**
@@ -92,27 +164,6 @@ export abstract class StateManager {
92
164
93
165
@Injectable ( { providedIn : 'root' } )
94
166
export class HistoryStateManager extends StateManager {
95
- private readonly location = inject ( Location ) ;
96
- private readonly urlSerializer = inject ( UrlSerializer ) ;
97
- private readonly options = inject ( ROUTER_CONFIGURATION , { optional : true } ) || { } ;
98
- private readonly canceledNavigationResolution =
99
- this . options . canceledNavigationResolution || 'replace' ;
100
-
101
- private urlHandlingStrategy = inject ( UrlHandlingStrategy ) ;
102
- private urlUpdateStrategy = this . options . urlUpdateStrategy || 'deferred' ;
103
-
104
- private currentUrlTree = new UrlTree ( ) ;
105
-
106
- override getCurrentUrlTree ( ) {
107
- return this . currentUrlTree ;
108
- }
109
-
110
- private rawUrlTree = this . currentUrlTree ;
111
-
112
- override getRawUrlTree ( ) {
113
- return this . rawUrlTree ;
114
- }
115
-
116
167
/**
117
168
* The id of the currently active page in the router.
118
169
* Updated to the transition's target id on a successful navigation.
@@ -140,59 +191,39 @@ export class HistoryStateManager extends StateManager {
140
191
return this . restoredState ( ) ?. ɵrouterPageId ?? this . currentPageId ;
141
192
}
142
193
143
- private routerState = createEmptyState ( null ) ;
144
-
145
- override getRouterState ( ) {
146
- return this . routerState ;
147
- }
148
-
149
- private stateMemento = this . createStateMemento ( ) ;
150
-
151
- private createStateMemento ( ) {
152
- return {
153
- rawUrlTree : this . rawUrlTree ,
154
- currentUrlTree : this . currentUrlTree ,
155
- routerState : this . routerState ,
156
- } ;
157
- }
158
-
159
194
override registerNonRouterCurrentEntryChangeListener (
160
- listener : ( url : string , state : RestoredState | null | undefined ) => void ,
195
+ listener : (
196
+ url : string ,
197
+ state : RestoredState | null | undefined ,
198
+ trigger : NavigationTrigger ,
199
+ ) => void ,
161
200
) : SubscriptionLike {
162
201
return this . location . subscribe ( ( event ) => {
163
202
if ( event [ 'type' ] === 'popstate' ) {
164
- listener ( event [ 'url' ] ! , event . state as RestoredState | null | undefined ) ;
203
+ // The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
204
+ // hybrid apps.
205
+ setTimeout ( ( ) => {
206
+ listener ( event [ 'url' ] ! , event . state as RestoredState | null | undefined , 'popstate' ) ;
207
+ } ) ;
165
208
}
166
209
} ) ;
167
210
}
168
211
169
212
override handleRouterEvent ( e : Event | PrivateRouterEvents , currentTransition : Navigation ) {
170
213
if ( e instanceof NavigationStart ) {
171
- this . stateMemento = this . createStateMemento ( ) ;
214
+ this . updateStateMemento ( ) ;
172
215
} else if ( e instanceof NavigationSkipped ) {
173
- this . rawUrlTree = currentTransition . initialUrl ;
216
+ this . commitTransition ( currentTransition ) ;
174
217
} else if ( e instanceof RoutesRecognized ) {
175
218
if ( this . urlUpdateStrategy === 'eager' ) {
176
219
if ( ! currentTransition . extras . skipLocationChange ) {
177
- const rawUrl = this . urlHandlingStrategy . merge (
178
- currentTransition . finalUrl ! ,
179
- currentTransition . initialUrl ,
180
- ) ;
181
- this . setBrowserUrl ( currentTransition . targetBrowserUrl ?? rawUrl , currentTransition ) ;
220
+ this . setBrowserUrl ( this . createBrowserPath ( currentTransition ) , currentTransition ) ;
182
221
}
183
222
}
184
223
} else if ( e instanceof BeforeActivateRoutes ) {
185
- this . currentUrlTree = currentTransition . finalUrl ! ;
186
- this . rawUrlTree = this . urlHandlingStrategy . merge (
187
- currentTransition . finalUrl ! ,
188
- currentTransition . initialUrl ,
189
- ) ;
190
- this . routerState = currentTransition . targetRouterState ! ;
224
+ this . commitTransition ( currentTransition ) ;
191
225
if ( this . urlUpdateStrategy === 'deferred' && ! currentTransition . extras . skipLocationChange ) {
192
- this . setBrowserUrl (
193
- currentTransition . targetBrowserUrl ?? this . rawUrlTree ,
194
- currentTransition ,
195
- ) ;
226
+ this . setBrowserUrl ( this . createBrowserPath ( currentTransition ) , currentTransition ) ;
196
227
}
197
228
} else if (
198
229
e instanceof NavigationCancel &&
@@ -208,22 +239,22 @@ export class HistoryStateManager extends StateManager {
208
239
}
209
240
}
210
241
211
- private setBrowserUrl ( url : UrlTree | string , transition : Navigation ) {
212
- const path = url instanceof UrlTree ? this . urlSerializer . serialize ( url ) : url ;
213
- if ( this . location . isCurrentPathEqualTo ( path ) || ! ! transition . extras . replaceUrl ) {
242
+ private setBrowserUrl ( path : string , { extras , id } : Navigation ) {
243
+ const { replaceUrl , state } = extras ;
244
+ if ( this . location . isCurrentPathEqualTo ( path ) || ! ! replaceUrl ) {
214
245
// replacements do not update the target page
215
246
const currentBrowserPageId = this . browserPageId ;
216
- const state = {
217
- ...transition . extras . state ,
218
- ...this . generateNgRouterState ( transition . id , currentBrowserPageId ) ,
247
+ const newState = {
248
+ ...state ,
249
+ ...this . generateNgRouterState ( id , currentBrowserPageId ) ,
219
250
} ;
220
- this . location . replaceState ( path , '' , state ) ;
251
+ this . location . replaceState ( path , '' , newState ) ;
221
252
} else {
222
- const state = {
223
- ...transition . extras . state ,
224
- ...this . generateNgRouterState ( transition . id , this . browserPageId + 1 ) ,
253
+ const newState = {
254
+ ...state ,
255
+ ...this . generateNgRouterState ( id , this . browserPageId + 1 ) ,
225
256
} ;
226
- this . location . go ( path , '' , state ) ;
257
+ this . location . go ( path , '' , newState ) ;
227
258
}
228
259
}
229
260
@@ -237,11 +268,11 @@ export class HistoryStateManager extends StateManager {
237
268
const targetPagePosition = this . currentPageId - currentBrowserPageId ;
238
269
if ( targetPagePosition !== 0 ) {
239
270
this . location . historyGo ( targetPagePosition ) ;
240
- } else if ( this . currentUrlTree === navigation . finalUrl && targetPagePosition === 0 ) {
271
+ } else if ( this . getCurrentUrlTree ( ) === navigation . finalUrl && targetPagePosition === 0 ) {
241
272
// We got to the activation stage (where currentUrlTree is set to the navigation's
242
273
// finalUrl), but we weren't moving anywhere in history (skipLocationChange or replaceUrl).
243
274
// We still need to reset the router state back to what it was when the navigation started.
244
- this . resetState ( navigation ) ;
275
+ this . resetInternalState ( navigation ) ;
245
276
this . resetUrlToCurrentUrlTree ( ) ;
246
277
} else {
247
278
// The browser URL and router state was not updated before the navigation cancelled so
@@ -253,29 +284,15 @@ export class HistoryStateManager extends StateManager {
253
284
// reject. For 'eager' navigations, it seems like we also really should reset the state
254
285
// because the navigation was cancelled. Investigate if this can be done by running TGP.
255
286
if ( restoringFromCaughtError ) {
256
- this . resetState ( navigation ) ;
287
+ this . resetInternalState ( navigation ) ;
257
288
}
258
289
this . resetUrlToCurrentUrlTree ( ) ;
259
290
}
260
291
}
261
292
262
- private resetState ( navigation : Navigation ) : void {
263
- this . routerState = this . stateMemento . routerState ;
264
- this . currentUrlTree = this . stateMemento . currentUrlTree ;
265
- // Note here that we use the urlHandlingStrategy to get the reset `rawUrlTree` because it may be
266
- // configured to handle only part of the navigation URL. This means we would only want to reset
267
- // the part of the navigation handled by the Angular router rather than the whole URL. In
268
- // addition, the URLHandlingStrategy may be configured to specifically preserve parts of the URL
269
- // when merging, such as the query params so they are not lost on a refresh.
270
- this . rawUrlTree = this . urlHandlingStrategy . merge (
271
- this . currentUrlTree ,
272
- navigation . finalUrl ?? this . rawUrlTree ,
273
- ) ;
274
- }
275
-
276
293
private resetUrlToCurrentUrlTree ( ) : void {
277
294
this . location . replaceState (
278
- this . urlSerializer . serialize ( this . rawUrlTree ) ,
295
+ this . urlSerializer . serialize ( this . getRawUrlTree ( ) ) ,
279
296
'' ,
280
297
this . generateNgRouterState ( this . lastSuccessfulId , this . currentPageId ) ,
281
298
) ;
0 commit comments