fix(router): page refresh should not destroy history state (#48540)

The router's `initialNavigation` causes an imperative navigation using
the `navigateByUrl` method. This, however, results in the history state
being removed on a page refresh. This change calls `scheduleNavigation`
directly from `initialNavigation` to ensure the history state is correctly
retained.

PR Close #48540
This commit is contained in:
Andrew Scott
2022-12-19 12:04:36 -08:00
committed by Alex Rickabaugh
parent 43ced455d6
commit cb6d73a84e
3 changed files with 52 additions and 41 deletions
+1
View File
@@ -19,6 +19,7 @@ import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
* @publicApi
*/
export type NavigationTrigger = 'imperative'|'popstate'|'hashchange';
export const IMPERATIVE_NAVIGATION = 'imperative';
/**
* Identifies the type of a router event.
+10 -9
View File
@@ -11,7 +11,7 @@ import {BehaviorSubject, combineLatest, EMPTY, Observable, of, Subject} from 'rx
import {catchError, defaultIfEmpty, filter, finalize, map, switchMap, take, tap} from 'rxjs/operators';
import {createRouterState} from './create_router_state';
import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {Event, GuardsCheckEnd, GuardsCheckStart, IMPERATIVE_NAVIGATION, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
import {NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
import {isNavigationCancelingError, isRedirectingNavigationCancelingError, redirectingNavigationError} from './navigation_canceling_error';
import {activateRoutes} from './operators/activate_routes';
@@ -335,7 +335,7 @@ export class NavigationTransitions {
resolve: null,
reject: null,
promise: Promise.resolve(true),
source: 'imperative',
source: IMPERATIVE_NAVIGATION,
restoredState: null,
currentSnapshot: router.routerState.snapshot,
targetSnapshot: null,
@@ -726,11 +726,12 @@ export class NavigationTransitions {
isBrowserTriggeredNavigation(overallTransitionState.source)
};
router.scheduleNavigation(mergedTree, 'imperative', null, extras, {
resolve: overallTransitionState.resolve,
reject: overallTransitionState.reject,
promise: overallTransitionState.promise
});
router.scheduleNavigation(
mergedTree, IMPERATIVE_NAVIGATION, null, extras, {
resolve: overallTransitionState.resolve,
reject: overallTransitionState.reject,
promise: overallTransitionState.promise
});
}
/* All other errors should reset to the router's internal URL reference
@@ -764,6 +765,6 @@ export class NavigationTransitions {
}
}
export function isBrowserTriggeredNavigation(source: 'imperative'|'popstate'|'hashchange') {
return source !== 'imperative';
export function isBrowserTriggeredNavigation(source: NavigationTrigger) {
return source !== IMPERATIVE_NAVIGATION;
}
+41 -32
View File
@@ -12,7 +12,7 @@ import {Observable, of, SubscriptionLike} from 'rxjs';
import {CreateUrlTreeStrategy} from './create_url_tree_strategy';
import {RuntimeErrorCode} from './errors';
import {Event, NavigationTrigger} from './events';
import {Event, IMPERATIVE_NAVIGATION, NavigationTrigger} from './events';
import {NavigationBehaviorOptions, OnSameUrlNavigation, Routes} from './models';
import {Navigation, NavigationExtras, NavigationTransition, NavigationTransitions, RestoredState, UrlCreationOptions} from './navigation_transition';
import {TitleStrategy} from './page_title_strategy';
@@ -354,7 +354,8 @@ export class Router {
initialNavigation(): void {
this.setUpLocationChangeListener();
if (!this.navigationTransitions.hasRequestedNavigation) {
this.navigateByUrl(this.location.path(true), {replaceUrl: true});
const state = this.location.getState() as RestoredState;
this.navigateToSyncWithBrowser(this.location.path(true), IMPERATIVE_NAVIGATION, state);
}
}
@@ -374,37 +375,49 @@ export class Router {
// The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
// hybrid apps.
setTimeout(() => {
const extras: NavigationExtras = {replaceUrl: true};
// TODO: restoredState should always include the entire state, regardless
// of navigationId. This requires a breaking change to update the type on
// NavigationStart’s restoredState, which currently requires navigationId
// to always be present. The Router used to only restore history state if
// a navigationId was present.
// The stored navigationId is used by the RouterScroller to retrieve the scroll
// position for the page.
const restoredState = event.state?.navigationId ? event.state : null;
// Separate to NavigationStart.restoredState, we must also restore the state to
// history.state and generate a new navigationId, since it will be overwritten
if (event.state) {
const stateCopy = {...event.state} as Partial<RestoredState>;
delete stateCopy.navigationId;
delete stateCopy.ɵrouterPageId;
if (Object.keys(stateCopy).length !== 0) {
extras.state = stateCopy;
}
}
const urlTree = this.parseUrl(event['url']!);
this.scheduleNavigation(urlTree, source, restoredState, extras);
this.navigateToSyncWithBrowser(event['url']!, source, event.state);
}, 0);
}
});
}
}
/**
* Schedules a router navigation to synchronize Router state with the browser state.
*
* This is done as a response to a popstate event and the initial navigation. These
* two scenarios represent times when the browser URL/state has been updated and
* the Router needs to respond to ensure its internal state matches.
*/
private navigateToSyncWithBrowser(
url: string, source: NavigationTrigger, state: RestoredState|undefined) {
const extras: NavigationExtras = {replaceUrl: true};
// TODO: restoredState should always include the entire state, regardless
// of navigationId. This requires a breaking change to update the type on
// NavigationStart’s restoredState, which currently requires navigationId
// to always be present. The Router used to only restore history state if
// a navigationId was present.
// The stored navigationId is used by the RouterScroller to retrieve the scroll
// position for the page.
const restoredState = state?.navigationId ? state : null;
// Separate to NavigationStart.restoredState, we must also restore the state to
// history.state and generate a new navigationId, since it will be overwritten
if (state) {
const stateCopy = {...state} as Partial<RestoredState>;
delete stateCopy.navigationId;
delete stateCopy.ɵrouterPageId;
if (Object.keys(stateCopy).length !== 0) {
extras.state = stateCopy;
}
}
const urlTree = this.parseUrl(url);
this.scheduleNavigation(urlTree, source, restoredState, extras);
}
/** The current URL. */
get url(): string {
return this.serializeUrl(this.currentUrlTree);
@@ -562,7 +575,7 @@ export class Router {
const urlTree = isUrlTree(url) ? url : this.parseUrl(url);
const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree);
return this.scheduleNavigation(mergedTree, 'imperative', null, extras);
return this.scheduleNavigation(mergedTree, IMPERATIVE_NAVIGATION, null, extras);
}
/**
@@ -687,10 +700,6 @@ export class Router {
let targetPageId: number;
if (this.canceledNavigationResolution === 'computed') {
const isInitialPage = this.currentPageId === 0;
if (isInitialPage) {
restoredState = this.location.getState() as RestoredState | null;
}
// If the `ɵrouterPageId` exist in the state then `targetpageId` should have the value of
// `ɵrouterPageId`. This is the case for something like a page refresh where we assign the
// target id to the previously set value for that page.