refactor(router): Introduce StateManager interface (#52171)

Move existing logic into `HistoryStateManager`

PR Close #52171
This commit is contained in:
Thomas Wilkinson
2023-10-11 15:21:36 -05:00
committed by Pawel Kozlowski
parent 6fd863d6e1
commit 030a68b3df
3 changed files with 104 additions and 61 deletions
@@ -278,6 +278,9 @@
{
"name": "HAS_TRANSPLANTED_VIEWS"
},
{
"name": "HistoryStateManager"
},
{
"name": "INITIAL_NAVIGATION"
},
@@ -2141,6 +2144,9 @@
{
"name": "ɵɵelementStart"
},
{
"name": "ɵɵgetInheritedFactory"
},
{
"name": "ɵɵinject"
},
+6 -6
View File
@@ -20,7 +20,7 @@ import {RouteReuseStrategy} from './route_reuse_strategy';
import {ROUTER_CONFIGURATION} from './router_config';
import {ROUTES} from './router_config_loader';
import {Params} from './shared';
import {StateManager} from './state_manager';
import {StateManager} from './statemanager/state_manager';
import {UrlHandlingStrategy} from './url_handling_strategy';
import {containsTree, IsActiveMatchOptions, isUrlTree, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
import {standardizeConfig, validateConfig} from './utils/config';
@@ -69,10 +69,10 @@ export const subsetMatchOptions: IsActiveMatchOptions = {
@Injectable({providedIn: 'root'})
export class Router {
private get currentUrlTree() {
return this.stateManager.currentUrlTree;
return this.stateManager.getCurrentUrlTree();
}
private get rawUrlTree() {
return this.stateManager.rawUrlTree;
return this.stateManager.getRawUrlTree();
}
private disposed = false;
private nonRouterCurrentEntryChangeSubscription?: SubscriptionLike;
@@ -108,7 +108,7 @@ export class Router {
* The current state of routing in this NgModule.
*/
get routerState() {
return this.stateManager.routerState;
return this.stateManager.getRouterState();
}
/**
@@ -177,7 +177,7 @@ export class Router {
const currentTransition = this.navigationTransitions.currentTransition;
const currentNavigation = this.navigationTransitions.currentNavigation;
if (currentTransition !== null && currentNavigation !== null) {
this.stateManager.handleNavigationEvent(e, currentNavigation);
this.stateManager.handleRouterEvent(e, currentNavigation);
if (e instanceof NavigationCancel && e.code !== NavigationCancellationCode.Redirect &&
e.code !== NavigationCancellationCode.SupersededByNewNavigation) {
// It seems weird that `navigated` is set to `true` when the navigation is rejected,
@@ -249,7 +249,7 @@ export class Router {
// run into ngZone
if (!this.nonRouterCurrentEntryChangeSubscription) {
this.nonRouterCurrentEntryChangeSubscription =
this.stateManager.nonRouterCurrentEntryChange((url, state) => {
this.stateManager.registerNonRouterCurrentEntryChangeListener((url, state) => {
// The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
// hybrid apps.
setTimeout(() => {
@@ -10,63 +10,97 @@ import {Location} from '@angular/common';
import {inject, Injectable} from '@angular/core';
import {SubscriptionLike} from 'rxjs';
import {BeforeActivateRoutes, Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationStart, PrivateRouterEvents, RoutesRecognized} from './events';
import {isBrowserTriggeredNavigation, Navigation, RestoredState} from './navigation_transition';
import {ROUTER_CONFIGURATION} from './router_config';
import {createEmptyState, RouterState} from './router_state';
import {UrlHandlingStrategy} from './url_handling_strategy';
import {UrlSerializer, UrlTree} from './url_tree';
import {BeforeActivateRoutes, Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationStart, PrivateRouterEvents, RoutesRecognized,} from '../events';
import {Navigation, RestoredState} from '../navigation_transition';
import {ROUTER_CONFIGURATION} from '../router_config';
import {createEmptyState, RouterState} from '../router_state';
import {UrlHandlingStrategy} from '../url_handling_strategy';
import {UrlSerializer, UrlTree} from '../url_tree';
@Injectable({providedIn: 'root', useFactory: () => inject(HistoryStateManager)})
export abstract class StateManager {
/**
* Returns the currently activated `UrlTree`.
*
* This `UrlTree` shows only URLs that the `Router` is configured to handle (through
* `UrlHandlingStrategy`).
*
* The value is set after finding the route config tree to activate but before activating the
* route.
*/
abstract getCurrentUrlTree(): UrlTree;
/**
* Returns a `UrlTree` that is represents what the browser is actually showing.
*
* In the life of a navigation transition:
* 1. When a navigation begins, the raw `UrlTree` is updated to the full URL that's being
* navigated to.
* 2. During a navigation, redirects are applied, which might only apply to _part_ of the URL (due
* to `UrlHandlingStrategy`).
* 3. Just before activation, the raw `UrlTree` is updated to include the redirects on top of the
* original raw URL.
*
* Note that this is _only_ here to support `UrlHandlingStrategy.extract` and
* `UrlHandlingStrategy.shouldProcessUrl`. Without those APIs, the current `UrlTree` would not
* deviated from the raw `UrlTree`.
*
* For `extract`, a raw `UrlTree` is needed because `extract` may only return part
* of the navigation URL. Thus, the current `UrlTree` may only represent _part_ of the browser
* URL. When a navigation gets cancelled and the router needs to reset the URL or a new navigation
* occurs, it needs to know the _whole_ browser URL, not just the part handled by
* `UrlHandlingStrategy`.
* For `shouldProcessUrl`, when the return is `false`, the router ignores the navigation but
* still updates the raw `UrlTree` with the assumption that the navigation was caused by the
* location change listener due to a URL update by the AngularJS router. In this case, the router
* still need to know what the browser's URL is for future navigations.
*/
abstract getRawUrlTree(): UrlTree;
/** Returns the current state stored by the browser for the current history entry. */
abstract restoredState(): RestoredState|null|undefined;
/** Returns the current RouterState. */
abstract getRouterState(): RouterState;
/**
* Registers a listener that is called whenever the current history entry changes by some API
* outside the Router. This includes user-activated changes like back buttons and link clicks, but
* also includes programmatic APIs called by non-Router JavaScript.
*/
abstract registerNonRouterCurrentEntryChangeListener(
listener: (url: string, state: RestoredState|null|undefined) => void): SubscriptionLike;
/**
* Handles a navigation event sent from the Router. These are typically events that indicate a
* navigation has started, progressed, been cancelled, or finished.
*/
abstract handleRouterEvent(e: Event|PrivateRouterEvents, currentTransition: Navigation): void;
}
@Injectable({providedIn: 'root'})
export class StateManager {
export class HistoryStateManager extends StateManager {
private readonly location = inject(Location);
private readonly urlSerializer = inject(UrlSerializer);
private readonly options = inject(ROUTER_CONFIGURATION, {optional: true}) || {};
private readonly canceledNavigationResolution =
this.options.canceledNavigationResolution || 'replace';
// These are currently writable via the Router public API but are deprecated and should be made
// `private readonly` in the future.
urlHandlingStrategy = inject(UrlHandlingStrategy);
urlUpdateStrategy = this.options.urlUpdateStrategy || 'deferred';
private urlHandlingStrategy = inject(UrlHandlingStrategy);
private urlUpdateStrategy = this.options.urlUpdateStrategy || 'deferred';
private currentUrlTree = new UrlTree();
override getCurrentUrlTree() {
return this.currentUrlTree;
}
private rawUrlTree = this.currentUrlTree;
override getRawUrlTree() {
return this.rawUrlTree;
}
/**
* Represents the activated `UrlTree` that the `Router` is configured to handle (through
* `UrlHandlingStrategy`). That is, after we find the route config tree that we're going to
* activate, run guards, and are just about to activate the route, we set the currentUrlTree.
* @internal
*/
currentUrlTree = new UrlTree();
/**
* Meant to represent the entire browser url after a successful navigation. In the life of a
* navigation transition:
* 1. The rawUrl represents the full URL that's being navigated to
* 2. We apply redirects, which might only apply to _part_ of the URL (due to
* `UrlHandlingStrategy`).
* 3. Right before activation (because we assume activation will succeed), we update the
* rawUrlTree to be a combination of the urlAfterRedirects (again, this might only apply to part
* of the initial url) and the rawUrl of the transition (which was the original navigation url in
* its full form).
* @internal
*
* Note that this is _only_ here to support `UrlHandlingStrategy.extract` and
* `UrlHandlingStrategy.shouldProcessUrl`. If those didn't exist, we could get by with
* `currentUrlTree` alone. If a new Router were to be provided (i.e. one that works with the
* browser navigation API), we should think about whether this complexity should be carried over.
*
* - extract: `rawUrlTree` is needed because `extract` may only return part
* of the navigation URL. Thus, `currentUrlTree` may only represent _part_ of the browser URL.
* When a navigation gets cancelled and we need to reset the URL or a new navigation occurs, we
* need to know the _whole_ browser URL, not just the part handled by UrlHandlingStrategy.
* - shouldProcessUrl: When this returns `false`, the router just ignores the navigation but still
* updates the `rawUrlTree` with the assumption that the navigation was caused by the location
* change listener due to a URL update by the AngularJS router. In this case, we still need to
* know what the browser's URL is for future navigations.
*
*/
rawUrlTree = this.currentUrlTree;
/**
* The id of the currently active page in the router.
* Updated to the transition's target id on a successful navigation.
@@ -76,10 +110,9 @@ export class StateManager {
* page.
*/
private currentPageId: number = 0;
lastSuccessfulId: number = -1;
private lastSuccessfulId: number = -1;
/** Returns the current state from the browser. */
restoredState(): RestoredState|null|undefined {
override restoredState(): RestoredState|null|undefined {
return this.location.getState() as RestoredState | null | undefined;
}
@@ -95,7 +128,12 @@ export class StateManager {
return this.restoredState()?.ɵrouterPageId ?? this.currentPageId;
}
routerState = createEmptyState(this.currentUrlTree, null);
private routerState = createEmptyState(this.currentUrlTree, null);
override getRouterState() {
return this.routerState;
}
private stateMemento = this.createStateMemento();
private createStateMemento() {
@@ -106,8 +144,8 @@ export class StateManager {
};
}
nonRouterCurrentEntryChange(listener: (url: string, state: RestoredState|null|undefined) => void):
SubscriptionLike {
override registerNonRouterCurrentEntryChangeListener(
listener: (url: string, state: RestoredState|null|undefined) => void): SubscriptionLike {
return this.location.subscribe(event => {
if (event['type'] === 'popstate') {
listener(event['url']!, event.state as RestoredState | null | undefined);
@@ -115,7 +153,7 @@ export class StateManager {
});
}
handleNavigationEvent(e: Event|PrivateRouterEvents, currentTransition: Navigation) {
override handleRouterEvent(e: Event|PrivateRouterEvents, currentTransition: Navigation) {
if (e instanceof NavigationStart) {
this.stateMemento = this.createStateMemento();
} else if (e instanceof NavigationSkipped) {
@@ -173,9 +211,8 @@ export class StateManager {
/**
* Performs the necessary rollback action to restore the browser URL to the
* state before the transition.
* @internal
*/
restoreHistory(navigation: Navigation, restoringFromCaughtError = false) {
private restoreHistory(navigation: Navigation, restoringFromCaughtError = false) {
if (this.canceledNavigationResolution === 'computed') {
const currentBrowserPageId = this.browserPageId;
const targetPagePosition = this.currentPageId - currentBrowserPageId;