Skip to content

Commit d46b8de

Browse files
vsavkinalxhub
authored andcommitted
fix(router): runs guards every time when unsuccessfully navigating to the same url over and over again (angular#13209)
1 parent bbb7a39 commit d46b8de

File tree

2 files changed

+98
-58
lines changed

2 files changed

+98
-58
lines changed

modules/@angular/router/src/router.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,11 @@ function defaultErrorHandler(error: any): any {
281281
type NavigationParams = {
282282
id: number,
283283
rawUrl: UrlTree,
284-
prevRawUrl: UrlTree,
285284
extras: NavigationExtras,
286285
resolve: any,
287286
reject: any,
288-
promise: Promise<boolean>
287+
promise: Promise<boolean>,
288+
imperative: boolean
289289
};
290290

291291

@@ -387,8 +387,19 @@ export class Router {
387387
// which does not work properly with zone.js in IE and Safari
388388
this.locationSubscription = <any>this.location.subscribe(Zone.current.wrap((change: any) => {
389389
const rawUrlTree = this.urlSerializer.parse(change['url']);
390+
const lastNavigation = this.navigations.value;
391+
392+
// If the user triggers a navigation imperatively (e.g., by using navigateByUrl),
393+
// and that navigation results in 'replaceState' that leads to the same URL,
394+
// we should skip those.
395+
if (lastNavigation && lastNavigation.imperative &&
396+
lastNavigation.rawUrl.toString() === rawUrlTree.toString()) {
397+
return;
398+
}
399+
390400
setTimeout(() => {
391-
this.scheduleNavigation(rawUrlTree, {skipLocationChange: change['pop'], replaceUrl: true});
401+
this.scheduleNavigation(
402+
rawUrlTree, false, {skipLocationChange: change['pop'], replaceUrl: true});
392403
}, 0);
393404
}));
394405
}
@@ -510,11 +521,12 @@ export class Router {
510521
navigateByUrl(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}):
511522
Promise<boolean> {
512523
if (url instanceof UrlTree) {
513-
return this.scheduleNavigation(this.urlHandlingStrategy.merge(url, this.rawUrlTree), extras);
524+
return this.scheduleNavigation(
525+
this.urlHandlingStrategy.merge(url, this.rawUrlTree), true, extras);
514526
} else {
515527
const urlTree = this.urlSerializer.parse(url);
516528
return this.scheduleNavigation(
517-
this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree), extras);
529+
this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree), true, extras);
518530
}
519531
}
520532

@@ -596,12 +608,8 @@ export class Router {
596608
.subscribe(() => {});
597609
}
598610

599-
private scheduleNavigation(rawUrl: UrlTree, extras: NavigationExtras): Promise<boolean> {
600-
const prevRawUrl = this.navigations.value ? this.navigations.value.rawUrl : null;
601-
if (prevRawUrl && prevRawUrl.toString() === rawUrl.toString()) {
602-
return this.navigations.value.promise;
603-
}
604-
611+
private scheduleNavigation(rawUrl: UrlTree, imperative: boolean, extras: NavigationExtras):
612+
Promise<boolean> {
605613
let resolve: any = null;
606614
let reject: any = null;
607615

@@ -611,18 +619,17 @@ export class Router {
611619
});
612620

613621
const id = ++this.navigationId;
614-
this.navigations.next({id, rawUrl, prevRawUrl, extras, resolve, reject, promise});
622+
this.navigations.next({id, imperative, rawUrl, extras, resolve, reject, promise});
615623

616624
// Make sure that the error is propagated even though `processNavigations` catch
617625
// handler does not rethrow
618626
return promise.catch((e: any) => Promise.reject(e));
619627
}
620628

621-
private executeScheduledNavigation({id, rawUrl, prevRawUrl, extras, resolve,
622-
reject}: NavigationParams): void {
629+
private executeScheduledNavigation({id, rawUrl, extras, resolve, reject}: NavigationParams):
630+
void {
623631
const url = this.urlHandlingStrategy.extract(rawUrl);
624-
const prevUrl = prevRawUrl ? this.urlHandlingStrategy.extract(prevRawUrl) : null;
625-
const urlTransition = !prevUrl || url.toString() !== prevUrl.toString();
632+
const urlTransition = !this.navigated || url.toString() !== this.currentUrlTree.toString();
626633

627634
if (urlTransition && this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) {
628635
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
@@ -635,7 +642,8 @@ export class Router {
635642
// we cannot process the current URL, but we could process the previous one =>
636643
// we need to do some cleanup
637644
} else if (
638-
urlTransition && prevRawUrl && this.urlHandlingStrategy.shouldProcessUrl(prevRawUrl)) {
645+
urlTransition && this.rawUrlTree &&
646+
this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)) {
639647
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
640648
Promise.resolve()
641649
.then(

modules/@angular/router/test/integration.spec.ts

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,13 @@ describe('Integration', () => {
13831383
useValue:
13841384
(c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; }
13851385
},
1386+
{
1387+
provide: 'alwaysFalseAndLogging',
1388+
useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
1389+
log.push('called');
1390+
return false;
1391+
}
1392+
},
13861393
]
13871394
});
13881395
});
@@ -1532,62 +1539,86 @@ describe('Integration', () => {
15321539
expect(location.path()).toEqual('/main/component1');
15331540
})));
15341541

1535-
});
1536-
1537-
describe('should work when given a class', () => {
1538-
class AlwaysTrue implements CanDeactivate<TeamCmp> {
1539-
canDeactivate(
1540-
component: TeamCmp, route: ActivatedRouteSnapshot,
1541-
state: RouterStateSnapshot): boolean {
1542-
return true;
1543-
}
1544-
}
1542+
it('should call guards every time when navigating to the same url over and over again',
1543+
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
1544+
const fixture = createRoot(router, RootCmp);
15451545

1546-
beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); });
1546+
router.resetConfig([
1547+
{path: 'simple', component: SimpleCmp, canDeactivate: ['alwaysFalseAndLogging']},
1548+
{path: 'blank', component: BlankCmp}
15471549

1548-
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
1549-
const fixture = createRoot(router, RootCmp);
1550+
]);
15501551

1551-
router.resetConfig(
1552-
[{path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue]}]);
1552+
router.navigateByUrl('/simple');
1553+
advance(fixture);
15531554

1554-
router.navigateByUrl('/team/22');
1555+
router.navigateByUrl('/blank');
15551556
advance(fixture);
1556-
expect(location.path()).toEqual('/team/22');
1557+
expect(log).toEqual(['called']);
1558+
expect(location.path()).toEqual('/simple');
15571559

1558-
router.navigateByUrl('/team/33');
1560+
router.navigateByUrl('/blank');
15591561
advance(fixture);
1560-
expect(location.path()).toEqual('/team/33');
1562+
expect(log).toEqual(['called', 'called']);
1563+
expect(location.path()).toEqual('/simple');
15611564
})));
1562-
});
15631565

15641566

1565-
describe('should work when returns an observable', () => {
1566-
beforeEach(() => {
1567-
TestBed.configureTestingModule({
1568-
providers: [{
1569-
provide: 'CanDeactivate',
1570-
useValue: (c: TeamCmp, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
1571-
return Observable.create((observer: any) => { observer.next(false); });
1572-
}
1573-
}]
1574-
});
1567+
describe('should work when given a class', () => {
1568+
class AlwaysTrue implements CanDeactivate<TeamCmp> {
1569+
canDeactivate(
1570+
component: TeamCmp, route: ActivatedRouteSnapshot,
1571+
state: RouterStateSnapshot): boolean {
1572+
return true;
1573+
}
1574+
}
1575+
1576+
beforeEach(() => { TestBed.configureTestingModule({providers: [AlwaysTrue]}); });
1577+
1578+
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
1579+
const fixture = createRoot(router, RootCmp);
1580+
1581+
router.resetConfig(
1582+
[{path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue]}]);
1583+
1584+
router.navigateByUrl('/team/22');
1585+
advance(fixture);
1586+
expect(location.path()).toEqual('/team/22');
1587+
1588+
router.navigateByUrl('/team/33');
1589+
advance(fixture);
1590+
expect(location.path()).toEqual('/team/33');
1591+
})));
15751592
});
15761593

1577-
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
1578-
const fixture = createRoot(router, RootCmp);
15791594

1580-
router.resetConfig(
1581-
[{path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate']}]);
1595+
describe('should work when returns an observable', () => {
1596+
beforeEach(() => {
1597+
TestBed.configureTestingModule({
1598+
providers: [{
1599+
provide: 'CanDeactivate',
1600+
useValue: (c: TeamCmp, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
1601+
return Observable.create((observer: any) => { observer.next(false); });
1602+
}
1603+
}]
1604+
});
1605+
});
15821606

1583-
router.navigateByUrl('/team/22');
1584-
advance(fixture);
1585-
expect(location.path()).toEqual('/team/22');
1607+
it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
1608+
const fixture = createRoot(router, RootCmp);
15861609

1587-
router.navigateByUrl('/team/33');
1588-
advance(fixture);
1589-
expect(location.path()).toEqual('/team/22');
1590-
})));
1610+
router.resetConfig(
1611+
[{path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate']}]);
1612+
1613+
router.navigateByUrl('/team/22');
1614+
advance(fixture);
1615+
expect(location.path()).toEqual('/team/22');
1616+
1617+
router.navigateByUrl('/team/33');
1618+
advance(fixture);
1619+
expect(location.path()).toEqual('/team/22');
1620+
})));
1621+
});
15911622
});
15921623
});
15931624

@@ -1826,6 +1857,7 @@ describe('Integration', () => {
18261857

18271858
TestBed.configureTestingModule({declarations: [RootCmpWithLink]});
18281859
const router: Router = TestBed.get(Router);
1860+
const loc: any = TestBed.get(Location);
18291861

18301862
const f = TestBed.createComponent(RootCmpWithLink);
18311863
advance(f);

0 commit comments

Comments
 (0)