8000 refactor(router): Update StateManager base class with common concrete… · angular/angular@bd1a755 · GitHub
[go: up one dir, main page]

Skip to content

Commit bd1a755

Browse files
atscottthePunderWoman
authored andcommitted
refactor(router): Update StateManager base class with common concrete implementations (#60617)
This commit updates the `StateManager` base class to contain common concrete implementations that would be the same regardless of whether the state manager is backed by the browser history API or the Navigation API. PR Close #60617
1 parent 9503722 commit bd1a755

File tree

2 files changed

+110
-97
lines changed

2 files changed

+110
-97
lines changed

packages/router/src/router.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,8 @@ export class Router {
277277
// already patch onPopState, so location change callback will
278278
// run into ngZone
279279
this.nonRouterCurrentEntryChangeSubscription ??=
280-
this.stateManager.registerNonRouterCurrentEntryChangeListener((url, state) => {
281-
// The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
282-
// hybrid apps.
283-
setTimeout(() => {
284-
this.navigateToSyncWithBrowser(url, 'popstate', state);
285-
}, 0);
280+
this.stateManager.registerNonRouterCurrentEntryChangeListener((url, state, source) => {
281+
this.navigateToSyncWithBrowser(url, source, state);
286282
});
287283
}
288284

packages/router/src/statemanager/state_manager.ts

Lines changed: 108 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
NavigationError,
2020
NavigationSkipped,
2121
NavigationStart,
22+
NavigationTrigger,
2223
PrivateRouterEvents,
2324
RoutesRecognized,
2425
} from '../events';
@@ -30,6 +31,15 @@ import {UrlSerializer, UrlTree} from '../url_tree';
3031

3132
@Injectable({providedIn: 'root', useFactory: () => inject(HistoryStateManager)})
3233
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();
3343
/**
3444
* Returns the currently activated `UrlTree`.
3545
*
@@ -39,8 +49,11 @@ export abstract class StateManager {
3949
* The value is set after finding the route config tree to activate but before activating the
4050
* route.
4151
*/
42-
abstract getCurrentUrlTree(): UrlTree;
52+
getCurrentUrlTree(): UrlTree {
53+
return this.currentUrlTree;
54+
}
4355

56+
private rawUrlTree = this.currentUrlTree;
4457
/**
4558
* Returns a `UrlTree` that is represents what the browser is actually showing.
4659
*
@@ -66,21 +79,80 @@ export abstract class StateManager {
6679
* location change listener due to a URL update by the AngularJS router. In this case, the router
6780
* still need to know what the browser's URL is for future navigations.
6881
*/
69-
abstract getRawUrlTree(): UrlTree;
82+
getRawUrlTree(): UrlTree {
83+
return this.rawUrlTree;
84+
}
7085

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);
73108

74109
/** 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;
76144

77145
/**
78146
* Registers a listener that is called whenever the current history entry changes by some API
79147
* outside the Router. This includes user-activated changes like back buttons and link clicks, but
80148
* also includes programmatic APIs called by non-Router JavaScript.
81149
*/
82150
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,
84156
): SubscriptionLike;
85157

86158
/**
@@ -92,27 +164,6 @@ export abstract class StateManager {
92164

93165
@Injectable({providedIn: 'root'})
94166
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-
116167
/**
117168
* The id of the currently active page in the router.
118169
* Updated to the transition's target id on a successful navigation.
@@ -140,59 +191,39 @@ export class HistoryStateManager extends StateManager {
140191
return this.restoredState()?.ɵrouterPageId ?? this.currentPageId;
141192
}
142193

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-
159194
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,
161200
): SubscriptionLike {
162201
return this.location.subscribe((event) => {
163202
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+
});
165208
}
166209
});
167210
}
168211

169212
override handleRouterEvent(e: Event | PrivateRouterEvents, currentTransition: Navigation) {
170213
if (e instanceof NavigationStart) {
171-
this.stateMemento = this.createStateMemento();
214+
this.updateStateMemento();
172215
} else if (e instanceof NavigationSkipped) {
173-
this.rawUrlTree = currentTransition.initialUrl;
216+
this.commitTransition(currentTransition);
174217
} else if (e instanceof RoutesRecognized) {
175218
if (this.urlUpdateStrategy === 'eager') {
176219
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);
182221
}
183222
}
184223
} 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);
191225
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);
196227
}
197228
} else if (
198229
e instanceof NavigationCancel &&
@@ -208,22 +239,22 @@ export class HistoryStateManager extends StateManager {
208239
}
209240
}
210241

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) {
214245
// replacements do not update the target page
215246
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),
219250
};
220-
this.location.replaceState(path, '', state);
251+
this.location.replaceState(path, '', newState);
221252
} 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),
225256
};
226-
this.location.go(path, '', state);
257+
this.location.go(path, '', newState);
227258
}
228259
}
229260

@@ -237,11 +268,11 @@ export class HistoryStateManager extends StateManager {
237268
const targetPagePosition = this.currentPageId - currentBrowserPageId;
238269
if (targetPagePosition !== 0) {
239270
this.location.historyGo(targetPagePosition);
240-
} else if (this.currentUrlTree === navigation.finalUrl && targetPagePosition === 0) {
271+
} else if (this.getCurrentUrlTree() === navigation.finalUrl && targetPagePosition === 0) {
241272
// We got to the activation stage (where currentUrlTree is set to the navigation's
242273
// finalUrl), but we weren't moving anywhere in history (skipLocationChange or replaceUrl).
243274
// 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);
245276
this.resetUrlToCurrentUrlTree();
246277
} else {
247278
// The browser URL and router state was not updated before the navigation cancelled so
@@ -253,29 +284,15 @@ export class HistoryStateManager extends StateManager {
253284
// reject. For 'eager' navigations, it seems like we also really should reset the state
254285
// because the navigation was cancelled. Investigate if this can be done by running TGP.
255286
if (restoringFromCaughtError) {
256-
this.resetState(navigation);
287+
this.resetInternalState(navigation);
257288
}
258289
this.resetUrlToCurrentUrlTree();
259290
}
260291
}
261292

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-
276293
private resetUrlToCurrentUrlTree(): void {
277294
this.location.replaceState(
278-
this.urlSerializer.serialize(this.rawUrlTree),
295+
this.urlSerializer.serialize(this.getRawUrlTree()),
279296
'',
280297
this.generateNgRouterState(this.lastSuccessfulId, this.currentPageId),
281298
);

0 commit comments

Comments
 (0)
0