fix(router): should reset location if a navigation by location is successful (#13545)
Closes #13491
This commit is contained in:
parent
f49ab56160
commit
eb2ceff4ba
|
@ -266,6 +266,8 @@ function defaultErrorHandler(error: any): any {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NavigationSource = 'imperative' | 'popstate' | 'hashchange';
|
||||||
|
|
||||||
type NavigationParams = {
|
type NavigationParams = {
|
||||||
id: number,
|
id: number,
|
||||||
rawUrl: UrlTree,
|
rawUrl: UrlTree,
|
||||||
|
@ -273,7 +275,7 @@ type NavigationParams = {
|
||||||
resolve: any,
|
resolve: any,
|
||||||
reject: any,
|
reject: any,
|
||||||
promise: Promise<boolean>,
|
promise: Promise<boolean>,
|
||||||
imperative: boolean,
|
source: NavigationSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -374,20 +376,8 @@ export class Router {
|
||||||
if (!this.locationSubscription) {
|
if (!this.locationSubscription) {
|
||||||
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
|
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
|
||||||
const rawUrlTree = this.urlSerializer.parse(change['url']);
|
const rawUrlTree = this.urlSerializer.parse(change['url']);
|
||||||
const lastNavigation = this.navigations.value;
|
const source: NavigationSource = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
|
||||||
|
setTimeout(() => { this.scheduleNavigation(rawUrlTree, source, {replaceUrl: true}); }, 0);
|
||||||
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
|
|
||||||
// and that navigation results in 'replaceState' that leads to the same URL,
|
|
||||||
// we should skip those.
|
|
||||||
if (lastNavigation && lastNavigation.imperative &&
|
|
||||||
lastNavigation.rawUrl.toString() === rawUrlTree.toString()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.scheduleNavigation(
|
|
||||||
rawUrlTree, false, {skipLocationChange: change['pop'], replaceUrl: true});
|
|
||||||
}, 0);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -505,12 +495,12 @@ export class Router {
|
||||||
Promise<boolean> {
|
Promise<boolean> {
|
||||||
if (url instanceof UrlTree) {
|
if (url instanceof UrlTree) {
|
||||||
return this.scheduleNavigation(
|
return this.scheduleNavigation(
|
||||||
this.urlHandlingStrategy.merge(url, this.rawUrlTree), true, extras);
|
this.urlHandlingStrategy.merge(url, this.rawUrlTree), 'imperative', extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlTree = this.urlSerializer.parse(url);
|
const urlTree = this.urlSerializer.parse(url);
|
||||||
return this.scheduleNavigation(
|
return this.scheduleNavigation(
|
||||||
this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree), true, extras);
|
this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree), 'imperative', extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -585,8 +575,26 @@ export class Router {
|
||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleNavigation(rawUrl: UrlTree, imperative: boolean, extras: NavigationExtras):
|
private scheduleNavigation(rawUrl: UrlTree, source: NavigationSource, extras: NavigationExtras):
|
||||||
Promise<boolean> {
|
Promise<boolean> {
|
||||||
|
const lastNavigation = this.navigations.value;
|
||||||
|
|
||||||
|
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
|
||||||
|
// and that navigation results in 'replaceState' that leads to the same URL,
|
||||||
|
// we should skip those.
|
||||||
|
if (lastNavigation && source !== 'imperative' && lastNavigation.source === 'imperative' &&
|
||||||
|
lastNavigation.rawUrl.toString() === rawUrl.toString()) {
|
||||||
|
return null; // return value is not used
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because of a bug in IE and Edge, the location class fires two events (popstate and
|
||||||
|
// hashchange)
|
||||||
|
// every single time. The second one should be ignored. Otherwise, the URL will flicker.
|
||||||
|
if (lastNavigation && source == 'hashchange' && lastNavigation.source === 'popstate' &&
|
||||||
|
lastNavigation.rawUrl.toString() === rawUrl.toString()) {
|
||||||
|
return null; // return value is not used
|
||||||
|
}
|
||||||
|
|
||||||
let resolve: any = null;
|
let resolve: any = null;
|
||||||
let reject: any = null;
|
let reject: any = null;
|
||||||
|
|
||||||
|
@ -596,7 +604,7 @@ export class Router {
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = ++this.navigationId;
|
const id = ++this.navigationId;
|
||||||
this.navigations.next({id, imperative, rawUrl, extras, resolve, reject, promise});
|
this.navigations.next({id, source, rawUrl, extras, resolve, reject, promise});
|
||||||
|
|
||||||
// Make sure that the error is propagated even though `processNavigations` catch
|
// Make sure that the error is propagated even though `processNavigations` catch
|
||||||
// handler does not rethrow
|
// handler does not rethrow
|
||||||
|
|
|
@ -389,6 +389,43 @@ describe('Integration', () => {
|
||||||
expect(cmp.recordedUrls()).toEqual(['one/two', 'three/four']);
|
expect(cmp.recordedUrls()).toEqual(['one/two', 'three/four']);
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
describe('should reset location if a navigation by location is successful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [{
|
||||||
|
provide: 'in1Second',
|
||||||
|
useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
||||||
|
let res: any = null;
|
||||||
|
const p = new Promise(_ => res = _);
|
||||||
|
setTimeout(() => res(true), 1000);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('work', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
router.resetConfig([{path: 'simple', component: SimpleCmp, canActivate: ['in1Second']}]);
|
||||||
|
|
||||||
|
// Trigger two location changes to the same URL.
|
||||||
|
// Because of the guard the order will look as follows:
|
||||||
|
// - location change 'simple'
|
||||||
|
// - start processing the change, start a guard
|
||||||
|
// - location change 'simple'
|
||||||
|
// - the first location change gets canceled, the URL gets reset to '/'
|
||||||
|
// - the second location change gets finished, the URL should be reset to '/simple'
|
||||||
|
(<any>location).simulateUrlPop('/simple');
|
||||||
|
(<any>location).simulateUrlPop('/simple');
|
||||||
|
|
||||||
|
tick(2000);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(location.path()).toEqual('/simple');
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
|
||||||
it('should support secondary routes', fakeAsync(inject([Router], (router: Router) => {
|
it('should support secondary routes', fakeAsync(inject([Router], (router: Router) => {
|
||||||
const fixture = createRoot(router, RootCmp);
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
@ -1416,7 +1453,7 @@ describe('Integration', () => {
|
||||||
log.push('called');
|
log.push('called');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue