Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/router/src/navigation_transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ export type RestoredState = {
// The `ɵ` prefix is there to reduce the chance of colliding with any existing user properties on
// the history state.
ɵrouterPageId?: number;
// When `browserUrl` is used, the actual route URL is stored here so that popstate events
// can use it for route matching instead of the displayed browser URL.
ɵrouterUrl?: string;
};

/**
Expand Down
11 changes: 10 additions & 1 deletion packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,18 +318,27 @@ export class Router {
// position for the page.
const restoredState = state?.navigationId ? state : null;

// When `browserUrl` was used during the original navigation, the actual route URL
// was stored in history state as `ɵrouterUrl`. Use it for route matching and
// preserve the browser URL as the displayed URL.
const routerUrl = state?.ɵrouterUrl ?? url;
if (state?.ɵrouterUrl) {
extras = {...extras, browserUrl: url};
}

// 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;
delete stateCopy.ɵrouterUrl;
if (Object.keys(stateCopy).length !== 0) {
extras.state = stateCopy;
}
}

const urlTree = this.parseUrl(url);
const urlTree = this.parseUrl(routerUrl);
this.scheduleNavigation(urlTree, source, restoredState, extras).catch((e) => {
if (this.disposed) {
return;
Expand Down
13 changes: 10 additions & 3 deletions packages/router/src/statemanager/navigation_state_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,7 @@ export class NavigationStateManager extends StateManager {
// Prepare the state to be stored in the NavigationHistoryEntry.
const state = {
...transition.extras.state,
// Include router's navigationId for tracking. Required for in-memory scroll restoration
navigationId: transition.id,
...this.generateNgRouterState(transition),
};

const info: NavigationInfo = {ɵrouterInfo: {intercept: true}};
Expand Down Expand Up @@ -489,7 +488,7 @@ export class NavigationStateManager extends StateManager {
: 'push';
const state = {
...transition.extras.state,
navigationId: transition.id,
...this.generateNgRouterState(transition),
};
// this might be a path or an actual URL depending on the baseHref
const pathOrUrl = this.location.prepareExternalUrl(internalPath);
Expand Down Expand Up @@ -544,6 +543,14 @@ export class NavigationStateManager extends StateManager {
return new URL(routerDestination, eventDestination.origin).href === eventDestination.href;
}

private generateNgRouterState(transition: RouterNavigation) {
return {
...this.routerUrlState(transition),
// Include router's navigationId for tracking. Required for in-memory scroll restoration
navigationId: transition.id,
};
}

private deferredCommitSupported(event: NavigateEvent): boolean {
return (
this.precommitHandlerSupported &&
Expand Down
28 changes: 22 additions & 6 deletions packages/router/src/statemanager/state_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ export abstract class StateManager {
return path;
}

protected routerUrlState(navigation?: Navigation): {
ɵrouterUrl?: string;
} {
if (navigation?.targetBrowserUrl === undefined || navigation?.finalUrl === undefined) {
return {};
}
return {ɵrouterUrl: this.urlSerializer.serialize(navigation.finalUrl)};
}

protected commitTransition({targetRouterState, finalUrl, initialUrl}: Navigation): void {
// If we are committing the transition after having a final URL and target state, we're updating
// all pieces of the state. Otherwise, we likely skipped the transition (due to URL handling strategy)
Expand Down Expand Up @@ -226,20 +235,22 @@ export class HistoryStateManager extends StateManager {
}
}

private setBrowserUrl(path: string, {extras, id}: Navigation) {
private setBrowserUrl(path: string, navigation: Navigation) {
const {extras, id} = navigation;
const {replaceUrl, state} = extras;

if (this.location.isCurrentPathEqualTo(path) || !!replaceUrl) {
// replacements do not update the target page
const currentBrowserPageId = this.browserPageId;
const newState = {
...state,
...this.generateNgRouterState(id, currentBrowserPageId),
...this.generateNgRouterState(id, currentBrowserPageId, navigation),
};
this.location.replaceState(path, '', newState);
} else {
const newState = {
...state,
...this.generateNgRouterState(id, this.browserPageId + 1),
...this.generateNgRouterState(id, this.browserPageId + 1, navigation),
};
this.location.go(path, '', newState);
}
Expand Down Expand Up @@ -299,10 +310,15 @@ export class HistoryStateManager extends StateManager {
);
}

private generateNgRouterState(navigationId: number, routerPageId: number) {
private generateNgRouterState(
navigationId: number,
routerPageId: number,
navigation?: Navigation,
) {
if (this.canceledNavigationResolution === 'computed') {
return {navigationId, ɵrouterPageId: routerPageId};
return {navigationId, ɵrouterPageId: routerPageId, ...this.routerUrlState(navigation)};
}
return {navigationId};

return {navigationId, ...this.routerUrlState(navigation)};
}
}
28 changes: 28 additions & 0 deletions packages/router/test/integration/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {eagerUrlUpdateStrategyIntegrationSuite} from './eager_url_update_strateg
import {duplicateInFlightNavigationsIntegrationSuite} from './duplicate_in_flight_navigations.spec';
import {navigationErrorsIntegrationSuite} from './navigation_errors.spec';
import {useAutoTick} from '@angular/private/testing';
import {RouterTestingHarness} from '../../testing';

for (const browserAPI of ['navigation', 'history'] as const) {
describe(`${browserAPI}-based routing`, () => {
Expand Down Expand Up @@ -393,6 +394,33 @@ for (const browserAPI of ['navigation', 'history'] as const) {
expect(event!.restoredState!.navigationId).toEqual(userVictorNavStart.id);
});

it('should restore internal route on popstate when browserUrl is used', async () => {
const router: Router = TestBed.inject(Router);
const location: Location = TestBed.inject(Location);

router.resetConfig([
{path: 'home', component: SimpleCmp},
{path: 'one', component: SimpleCmp},
]);

const harness = await RouterTestingHarness.create('/home');
router.setUpLocationChangeListener();

await router.navigateByUrl('/one', {browserUrl: '/display-one'});
expect(location.path()).toEqual('/display-one');
expect(router.url).toEqual('/one');

location.back();
await advance(harness.fixture);
expect(location.path()).toEqual('/home');
expect(router.url).toEqual('/home');

location.forward();
await advance(harness.fixture);
expect(router.url).toEqual('/one');
expect(location.path()).toEqual('/display-one');
});

it('should navigate to the same url when config changes', async () => {
const router: Router = TestBed.inject(Router);
const location: Location = TestBed.inject(Location);
Expand Down
Loading