fix(router): eagerly update internal state on browser-triggered navigations (#43102)

The management of `browserUrlTree` currently has several problems with
correctly tracking the actual state of the browser.

This change makes the Router eagerly update the `browserUrlTree` when
handling navigations triggered by browser events (i.e., not 'imperative'). This
is because with those types of navigations, the browser URL bar is
_already_ updated. If we do not update the internal tracking of the
`browserUrlTree`, we will be out of sync with the real URL if the
navigation is rejected.

It would be best if we could remove `browserUrlTree` completely, but doing that
would require a lot more investigation and is blocked by #27059 because
the SpyLocation used in tests does not emulate real browser behavior.

fixes #43101

PR Close #43102
This commit is contained in:
Andrew Scott 2021-08-10 10:16:01 -07:00 committed by Dylan Hunn
parent b3bb6ec161
commit 286b2807de
4 changed files with 29 additions and 12 deletions

View file

@ -1526,6 +1526,9 @@
{
"name": "isArrayLike"
},
{
"name": "isBrowserTriggeredNavigation"
},
{
"name": "isCommandWithOutlets"
},

View file

@ -636,6 +636,12 @@ export class Router {
(this.onSameUrlNavigation === 'reload' ? true : urlTransition) &&
this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl);
// If the source of the navigation is from a browser event, the URL is
// already updated. We already need to sync the internal state.
if (isBrowserTriggeredNavigation(t.source)) {
this.browserUrlTree = t.rawUrl;
}
if (processCurrentUrl) {
return of(t).pipe(
// Fire NavigationStart event
@ -954,7 +960,12 @@ export class Router {
this.urlHandlingStrategy.merge(e.url, this.rawUrlTree);
const extras = {
skipLocationChange: t.extras.skipLocationChange,
replaceUrl: this.urlUpdateStrategy === 'eager'
// The URL is already updated at this point if we have 'eager' URL
// updates or if the navigation was triggered by the browser (back
// button, URL bar, etc). We want to replace that item in history if
// the navigation is rejected.
replaceUrl: this.urlUpdateStrategy === 'eager' ||
isBrowserTriggeredNavigation(t.source)
};
this.scheduleNavigation(
@ -1385,8 +1396,8 @@ export class Router {
const lastNavigation = this.getTransition();
// We don't want to skip duplicate successful navs if they're imperative because
// onSameUrlNavigation could be 'reload' (so the duplicate is intended).
const browserNavPrecededByRouterNav =
source !== 'imperative' && lastNavigation?.source === 'imperative';
const browserNavPrecededByRouterNav = isBrowserTriggeredNavigation(source) && lastNavigation &&
!isBrowserTriggeredNavigation(lastNavigation.source);
const lastNavigationSucceeded = this.lastSuccessfulId === lastNavigation.id;
// If the last navigation succeeded or is in flight, we can use the rawUrl as the comparison.
// However, if it failed, we should compare to the final result (urlAfterRedirects).
@ -1549,3 +1560,7 @@ function validateCommands(commands: string[]): void {
}
}
}
function isBrowserTriggeredNavigation(source: 'imperative'|'popstate'|'hashchange') {
return source !== 'imperative';
}

View file

@ -1230,7 +1230,11 @@ describe('Integration', () => {
location.simulateHashChange('/blocked');
advance(fixture);
expectEvents(recordedEvents, [[NavigationStart, '/blocked']]);
expectEvents(recordedEvents, [
[NavigationStart, '/blocked'],
[NavigationStart, '/simple'],
[NavigationEnd, '/simple'],
]);
}));
});

View file

@ -266,14 +266,9 @@ describe('Integration', () => {
location.simulateHashChange('/one');
advance(fixture);
const BASE_ERROR_MESSAGE =
'This asserts current behavior, which is incorrect. When #28561 is fixed, it should be: ';
expect(location.path()).toEqual('/one', BASE_ERROR_MESSAGE + '/');
const urlChanges = ['replace: /', 'hash: /one'];
expect(location.urlChanges)
.toEqual(
urlChanges, BASE_ERROR_MESSAGE + JSON.stringify(urlChanges.concat('replace: /')));
expect(location.path()).toEqual('/');
const urlChanges = ['replace: /', 'hash: /one', 'replace: /'];
expect(location.urlChanges).toEqual(urlChanges);
}));
});
});