fix(router): runs guards every time when unsuccessfully navigating to the same url over and over again (#13209)
This commit is contained in:
parent
bbb7a39414
commit
d46b8deeea
@ -281,11 +281,11 @@ function defaultErrorHandler(error: any): any {
|
|||||||
type NavigationParams = {
|
type NavigationParams = {
|
||||||
id: number,
|
id: number,
|
||||||
rawUrl: UrlTree,
|
rawUrl: UrlTree,
|
||||||
prevRawUrl: UrlTree,
|
|
||||||
extras: NavigationExtras,
|
extras: NavigationExtras,
|
||||||
resolve: any,
|
resolve: any,
|
||||||
reject: any,
|
reject: any,
|
||||||
promise: Promise<boolean>
|
promise: Promise<boolean>,
|
||||||
|
imperative: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -387,8 +387,19 @@ export class Router {
|
|||||||
// which does not work properly with zone.js in IE and Safari
|
// which does not work properly with zone.js in IE and Safari
|
||||||
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;
|
||||||
|
|
||||||
|
// 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(() => {
|
setTimeout(() => {
|
||||||
this.scheduleNavigation(rawUrlTree, {skipLocationChange: change['pop'], replaceUrl: true});
|
this.scheduleNavigation(
|
||||||
|
rawUrlTree, false, {skipLocationChange: change['pop'], replaceUrl: true});
|
||||||
}, 0);
|
}, 0);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -510,11 +521,12 @@ export class Router {
|
|||||||
navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}):
|
navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}):
|
||||||
Promise<boolean> {
|
Promise<boolean> {
|
||||||
if (url instanceof UrlTree) {
|
if (url instanceof UrlTree) {
|
||||||
return this.scheduleNavigation(this.urlHandlingStrategy.merge(url, this.rawUrlTree), extras);
|
return this.scheduleNavigation(
|
||||||
|
this.urlHandlingStrategy.merge(url, this.rawUrlTree), true, extras);
|
||||||
} else {
|
} else {
|
||||||
const urlTree = this.urlSerializer.parse(url);
|
const urlTree = this.urlSerializer.parse(url);
|
||||||
return this.scheduleNavigation(
|
return this.scheduleNavigation(
|
||||||
this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree), extras);
|
this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree), true, extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,12 +608,8 @@ export class Router {
|
|||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleNavigation(rawUrl: UrlTree, extras: NavigationExtras): Promise<boolean> {
|
private scheduleNavigation(rawUrl: UrlTree, imperative: boolean, extras: NavigationExtras):
|
||||||
const prevRawUrl = this.navigations.value ? this.navigations.value.rawUrl : null;
|
Promise<boolean> {
|
||||||
if (prevRawUrl && prevRawUrl.toString() === rawUrl.toString()) {
|
|
||||||
return this.navigations.value.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolve: any = null;
|
let resolve: any = null;
|
||||||
let reject: any = null;
|
let reject: any = null;
|
||||||
|
|
||||||
@ -611,18 +619,17 @@ export class Router {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const id = ++this.navigationId;
|
const id = ++this.navigationId;
|
||||||
this.navigations.next({id, rawUrl, prevRawUrl, extras, resolve, reject, promise});
|
this.navigations.next({id, imperative, 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
|
||||||
return promise.catch((e: any) => Promise.reject(e));
|
return promise.catch((e: any) => Promise.reject(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
private executeScheduledNavigation({id, rawUrl, prevRawUrl, extras, resolve,
|
private executeScheduledNavigation({id, rawUrl, extras, resolve, reject}: NavigationParams):
|
||||||
reject}: NavigationParams): void {
|
void {
|
||||||
const url = this.urlHandlingStrategy.extract(rawUrl);
|
const url = this.urlHandlingStrategy.extract(rawUrl);
|
||||||
const prevUrl = prevRawUrl ? this.urlHandlingStrategy.extract(prevRawUrl) : null;
|
const urlTransition = !this.navigated || url.toString() !== this.currentUrlTree.toString();
|
||||||
const urlTransition = !prevUrl || url.toString() !== prevUrl.toString();
|
|
||||||
|
|
||||||
if (urlTransition && this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) {
|
if (urlTransition && this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) {
|
||||||
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
|
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
|
||||||
@ -635,7 +642,8 @@ export class Router {
|
|||||||
// we cannot process the current URL, but we could process the previous one =>
|
// we cannot process the current URL, but we could process the previous one =>
|
||||||
// we need to do some cleanup
|
// we need to do some cleanup
|
||||||
} else if (
|
} else if (
|
||||||
urlTransition && prevRawUrl && this.urlHandlingStrategy.shouldProcessUrl(prevRawUrl)) {
|
urlTransition && this.rawUrlTree &&
|
||||||
|
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
|
||||||
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
|
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(
|
.then(
|
||||||
|
@ -1383,6 +1383,13 @@ describe('Integration', () => {
|
|||||||
useValue:
|
useValue:
|
||||||
(c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; }
|
(c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: 'alwaysFalseAndLogging',
|
||||||
|
useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
||||||
|
log.push('called');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1532,62 +1539,86 @@ describe('Integration', () => {
|
|||||||
expect(location.path()).toEqual('/main/component1');
|
expect(location.path()).toEqual('/main/component1');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
});
|
it('should call guards every time when navigating to the same url over and over again',
|
||||||
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
describe('should work when given a class', () => {
|
|
||||||
class AlwaysTrue implements CanDeactivate<TeamCmp> {
|
|
||||||
canDeactivate(
|
|
||||||
component: TeamCmp, route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); });
|
|
||||||
|
|
||||||
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
|
||||||
const fixture = createRoot(router, RootCmp);
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
router.resetConfig(
|
router.resetConfig([
|
||||||
[{path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue]}]);
|
{path: 'simple', component: SimpleCmp, canDeactivate: ['alwaysFalseAndLogging']},
|
||||||
|
{path: 'blank', component: BlankCmp}
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
]);
|
||||||
advance(fixture);
|
|
||||||
expect(location.path()).toEqual('/team/22');
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/33');
|
router.navigateByUrl('/simple');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(location.path()).toEqual('/team/33');
|
|
||||||
|
router.navigateByUrl('/blank');
|
||||||
|
advance(fixture);
|
||||||
|
expect(log).toEqual(['called']);
|
||||||
|
expect(location.path()).toEqual('/simple');
|
||||||
|
|
||||||
|
router.navigateByUrl('/blank');
|
||||||
|
advance(fixture);
|
||||||
|
expect(log).toEqual(['called', 'called']);
|
||||||
|
expect(location.path()).toEqual('/simple');
|
||||||
})));
|
})));
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('should work when returns an observable', () => {
|
describe('should work when given a class', () => {
|
||||||
beforeEach(() => {
|
class AlwaysTrue implements CanDeactivate<TeamCmp> {
|
||||||
TestBed.configureTestingModule({
|
canDeactivate(
|
||||||
providers: [{
|
component: TeamCmp, route: ActivatedRouteSnapshot,
|
||||||
provide: 'CanDeactivate',
|
state: RouterStateSnapshot): boolean {
|
||||||
useValue: (c: TeamCmp, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
return true;
|
||||||
return Observable.create((observer: any) => { observer.next(false); });
|
}
|
||||||
}
|
}
|
||||||
}]
|
|
||||||
});
|
beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); });
|
||||||
|
|
||||||
|
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
router.resetConfig(
|
||||||
|
[{path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue]}]);
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/22');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/22');
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/33');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/33');
|
||||||
|
})));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
|
||||||
const fixture = createRoot(router, RootCmp);
|
|
||||||
|
|
||||||
router.resetConfig(
|
describe('should work when returns an observable', () => {
|
||||||
[{path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate']}]);
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [{
|
||||||
|
provide: 'CanDeactivate',
|
||||||
|
useValue: (c: TeamCmp, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
||||||
|
return Observable.create((observer: any) => { observer.next(false); });
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
advance(fixture);
|
const fixture = createRoot(router, RootCmp);
|
||||||
expect(location.path()).toEqual('/team/22');
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/33');
|
router.resetConfig(
|
||||||
advance(fixture);
|
[{path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate']}]);
|
||||||
expect(location.path()).toEqual('/team/22');
|
|
||||||
})));
|
router.navigateByUrl('/team/22');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/22');
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/33');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/team/22');
|
||||||
|
})));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1826,6 +1857,7 @@ describe('Integration', () => {
|
|||||||
|
|
||||||
TestBed.configureTestingModule({declarations: [RootCmpWithLink]});
|
TestBed.configureTestingModule({declarations: [RootCmpWithLink]});
|
||||||
const router: Router = TestBed.get(Router);
|
const router: Router = TestBed.get(Router);
|
||||||
|
const loc: any = TestBed.get(Location);
|
||||||
|
|
||||||
const f = TestBed.createComponent(RootCmpWithLink);
|
const f = TestBed.createComponent(RootCmpWithLink);
|
||||||
advance(f);
|
advance(f);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user