Skip to content

Commit cb6d73a

Browse files
atscottalxhub
authored andcommitted
fix(router): page refresh should not destroy history state (angular#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 angular#48540
1 parent 43ced45 commit cb6d73a

File tree

3 files changed

+52
-41
lines changed

3 files changed

+52
-41
lines changed

packages/router/src/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
1919
* @publicApi
2020
*/
2121
export type NavigationTrigger = 'imperative'|'popstate'|'hashchange';
22+
export const IMPERATIVE_NAVIGATION = 'imperative';
2223

2324
/**
2425
* Identifies the type of a router event.

packages/router/src/navigation_transition.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {BehaviorSubject, combineLatest, EMPTY, Observable, of, Subject} from 'rx
1111
import {catchError, defaultIfEmpty, filter, finalize, map, switchMap, take, tap} from 'rxjs/operators';
1212

1313
import {createRouterState} from './create_router_state';
14-
import {Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
14+
import {Event, GuardsCheckEnd, GuardsCheckStart, IMPERATIVE_NAVIGATION, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
1515
import {NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models';
1616
import {isNavigationCancelingError, isRedirectingNavigationCancelingError, redirectingNavigationError} from './navigation_canceling_error';
1717
import {activateRoutes} from './operators/activate_routes';
@@ -335,7 +335,7 @@ export class NavigationTransitions {
335335
resolve: null,
336336
reject: null,
337337
promise: Promise.resolve(true),
338-
source: 'imperative',
338+
source: IMPERATIVE_NAVIGATION,
339339
restoredState: null,
340340
currentSnapshot: router.routerState.snapshot,
341341
targetSnapshot: null,
@@ -726,11 +726,12 @@ export class NavigationTransitions {
726726
isBrowserTriggeredNavigation(overallTransitionState.source)
727727
};
728728

729-
router.scheduleNavigation(mergedTree, 'imperative', null, extras, {
730-
resolve: overallTransitionState.resolve,
731-
reject: overallTransitionState.reject,
732-
promise: overallTransitionState.promise
733-
});
729+
router.scheduleNavigation(
730+
mergedTree, IMPERATIVE_NAVIGATION, null, extras, {
731+
resolve: overallTransitionState.resolve,
732+
reject: overallTransitionState.reject,
733+
promise: overallTransitionState.promise
734+
});
734735
}
735736

736737
/* All other errors should reset to the router's internal URL reference
@@ -764,6 +765,6 @@ export class NavigationTransitions {
764765
}
765766
}
766767

767-
export function isBrowserTriggeredNavigation(source: 'imperative'|'popstate'|'hashchange') {
768-
return source !== 'imperative';
768+
export function isBrowserTriggeredNavigation(source: NavigationTrigger) {
769+
return source !== IMPERATIVE_NAVIGATION;
769770
}

packages/router/src/router.ts

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {Observable, of, SubscriptionLike} from 'rxjs';
1212

1313
import {CreateUrlTreeStrategy} from './create_url_tree_strategy';
1414
import {RuntimeErrorCode} from './errors';
15-
import {Event, NavigationTrigger} from './events';
15+
import {Event, IMPERATIVE_NAVIGATION, NavigationTrigger} from './events';
1616
import {NavigationBehaviorOptions, OnSameUrlNavigation, Routes} from './models';
1717
import {Navigation, NavigationExtras, NavigationTransition, NavigationTransitions, RestoredState, UrlCreationOptions} from './navigation_transition';
1818
import {TitleStrategy} from './page_title_strategy';
@@ -354,7 +354,8 @@ export class Router {
354354
initialNavigation(): void {
355355
this.setUpLocationChangeListener();
356356
if (!this.navigationTransitions.hasRequestedNavigation) {
357-
this.navigateByUrl(this.location.path(true), {replaceUrl: true});
357+
const state = this.location.getState() as RestoredState;
358+
this.navigateToSyncWithBrowser(this.location.path(true), IMPERATIVE_NAVIGATION, state);
358359
}
359360
}
360361

@@ -374,37 +375,49 @@ export class Router {
374375
// The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS
375376
// hybrid apps.
376377
setTimeout(() => {
377-
const extras: NavigationExtras = {replaceUrl: true};
378-
379-
// TODO: restoredState should always include the entire state, regardless
380-
// of navigationId. This requires a breaking change to update the type on
381-
// NavigationStart’s restoredState, which currently requires navigationId
382-
// to always be present. The Router used to only restore history state if
383-
// a navigationId was present.
384-
385-
// The stored navigationId is used by the RouterScroller to retrieve the scroll
386-
// position for the page.
387-
const restoredState = event.state?.navigationId ? event.state : null;
388-
389-
// Separate to NavigationStart.restoredState, we must also restore the state to
390-
// history.state and generate a new navigationId, since it will be overwritten
391-
if (event.state) {
392-
const stateCopy = {...event.state} as Partial<RestoredState>;
393-
delete stateCopy.navigationId;
394-
delete stateCopy.ɵrouterPageId;
395-
if (Object.keys(stateCopy).length !== 0) {
396-
extras.state = stateCopy;
397-
}
398-
}
399-
400-
const urlTree = this.parseUrl(event['url']!);
401-
this.scheduleNavigation(urlTree, source, restoredState, extras);
378+
this.navigateToSyncWithBrowser(event['url']!, source, event.state);
402379
}, 0);
403380
}
404381
});
405382
}
406383
}
407384

385+
/**
386+
* Schedules a router navigation to synchronize Router state with the browser state.
387+
*
388+
* This is done as a response to a popstate event and the initial navigation. These
389+
* two scenarios represent times when the browser URL/state has been updated and
390+
* the Router needs to respond to ensure its internal state matches.
391+
*/
392+
private navigateToSyncWithBrowser(
393+
url: string, source: NavigationTrigger, state: RestoredState|undefined) {
394+
const extras: NavigationExtras = {replaceUrl: true};
395+
396+
// TODO: restoredState should always include the entire state, regardless
397+
// of navigationId. This requires a breaking change to update the type on
398+
// NavigationStart’s restoredState, which currently requires navigationId
399+
// to always be present. The Router used to only restore history state if
400+
// a navigationId was present.
401+
402+
// The stored navigationId is used by the RouterScroller to retrieve the scroll
403+
// position for the page.
404+
const restoredState = state?.navigationId ? state : null;
405+
406+
// Separate to NavigationStart.restoredState, we must also restore the state to
407+
// history.state and generate a new navigationId, since it will be overwritten
408+
if (state) {
409+
const stateCopy = {...state} as Partial<RestoredState>;
410+
delete stateCopy.navigationId;
411+
delete stateCopy.ɵrouterPageId;
412+
if (Object.keys(stateCopy).length !== 0) {
413+
extras.state = stateCopy;
414+
}
415+
}
416+
417+
const urlTree = this.parseUrl(url);
418+
this.scheduleNavigation(urlTree, source, restoredState, extras);
419+
}
420+
408421
/** The current URL. */
409422
get url(): string {
410423
return this.serializeUrl(this.currentUrlTree);
@@ -562,7 +575,7 @@ export class Router {
562575
const urlTree = isUrlTree(url) ? url : this.parseUrl(url);
563576
const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree);
564577

565-
return this.scheduleNavigation(mergedTree, 'imperative', null, extras);
578+
return this.scheduleNavigation(mergedTree, IMPERATIVE_NAVIGATION, null, extras);
566579
}
567580

568581
/**
@@ -687,10 +700,6 @@ export class Router {
687700

688701
let targetPageId: number;
689702
if (this.canceledNavigationResolution === 'computed') {
690-
const isInitialPage = this.currentPageId === 0;
691-
if (isInitialPage) {
692-
restoredState = this.location.getState() as RestoredState | null;
693-
}
694703
// If the `ɵrouterPageId` exist in the state then `targetpageId` should have the value of
695704
// `ɵrouterPageId`. This is the case for something like a page refresh where we assign the
696705
// target id to the previously set value for that page.

0 commit comments

Comments
 (0)