Skip to content

Commit 6651541

Browse files
vsavkinchuckjaz
authored andcommitted
fix(router): do not call location.go when skipping a navigation (angular#19463)
Closes angular#18036 PR Close angular#19463
1 parent 43c5b63 commit 6651541

File tree

2 files changed

+84
-26
lines changed

2 files changed

+84
-26
lines changed

packages/router/src/router.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -580,10 +580,9 @@ export class Router {
580580
}
581581

582582
private runNavigate(
583-
url: UrlTree, rawUrl: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
584-
id: number, precreatedState: RouterStateSnapshot|null): Promise<boolean> {
583+
url: UrlTree, rawUrl: UrlTree, skipLocationChange: boolean, replaceUrl: boolean, id: number,
584+
precreatedState: RouterStateSnapshot|null): Promise<boolean> {
585585
if (id !== this.navigationId) {
586-
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
587586
(this.events as Subject<Event>)
588587
.next(new NavigationCancel(
589588
id, this.serializeUrl(url),
@@ -705,9 +704,9 @@ export class Router {
705704

706705
(this as{routerState: RouterState}).routerState = state;
707706

708-
if (!shouldPreventPushState) {
707+
if (!skipLocationChange) {
709708
const path = this.urlSerializer.serialize(this.rawUrlTree);
710-
if (this.location.isCurrentPathEqualTo(path) || shouldReplaceUrl) {
709+
if (this.location.isCurrentPathEqualTo(path) || replaceUrl) {
711710
this.location.replaceState(path);
712711
} else {
713712
this.location.go(path);
@@ -755,14 +754,13 @@ export class Router {
755754
(this as{routerState: RouterState}).routerState = storedState;
756755
this.currentUrlTree = storedUrl;
757756
this.rawUrlTree = this.urlHandlingStrategy.merge(this.currentUrlTree, rawUrl);
758-
this.location.replaceState(this.serializeUrl(this.rawUrlTree));
757+
this.resetUrlToCurrentUrlTree();
759758
});
760759
});
761760
}
762761

763762
private resetUrlToCurrentUrlTree(): void {
764-
const path = this.urlSerializer.serialize(this.rawUrlTree);
765-
this.location.replaceState(path);
763+
this.location.replaceState(this.urlSerializer.serialize(this.rawUrlTree));
766764
}
767765
}
768766

packages/router/test/integration.spec.ts

+78-18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/
1212
import {By} from '@angular/platform-browser/src/dom/debug/by';
1313
import {expect} from '@angular/platform-browser/testing/src/matchers';
1414
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '@angular/router';
15+
import {SpyLocation} from 'common/testing';
1516
import {Observable} from 'rxjs/Observable';
1617
import {Observer} from 'rxjs/Observer';
1718
import {of } from 'rxjs/observable/of';
@@ -50,28 +51,87 @@ describe('Integration', () => {
5051
expect(fixture.nativeElement).toHaveText('route');
5152
})));
5253

53-
it('should navigate to the current URL',
54-
fakeAsync(inject([Router, Location], (router: Router) => {
55-
router.resetConfig([
56-
{path: '', component: SimpleCmp},
57-
{path: 'simple', component: SimpleCmp},
58-
]);
54+
describe('navigation', function() {
55+
it('should navigate to the current URL', fakeAsync(inject([Router], (router: Router) => {
56+
router.resetConfig([
57+
{path: '', component: SimpleCmp},
58+
{path: 'simple', component: SimpleCmp},
59+
]);
5960

60-
const fixture = createRoot(router, RootCmp);
61-
const events: Event[] = [];
62-
router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e));
61+
const fixture = createRoot(router, RootCmp);
62+
const events: Event[] = [];
63+
router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e));
6364

64-
router.navigateByUrl('/simple');
65-
tick();
65+
router.navigateByUrl('/simple');
66+
tick();
6667

67-
router.navigateByUrl('/simple');
68-
tick();
68+
router.navigateByUrl('/simple');
69+
tick();
6970

70-
expectEvents(events, [
71-
[NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'],
72-
[NavigationEnd, '/simple']
73-
]);
74-
})));
71+
expectEvents(events, [
72+
[NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'],
73+
[NavigationEnd, '/simple']
74+
]);
75+
})));
76+
77+
it('should not pollute browser history when replaceUrl is set to true',
78+
fakeAsync(inject([Router, Location], (router: Router, location: SpyLocation) => {
79+
router.resetConfig([
80+
{path: '', component: SimpleCmp}, {path: 'a', component: SimpleCmp},
81+
{path: 'b', component: SimpleCmp}
82+
]);
83+
84+
const fixture = createRoot(router, RootCmp);
85+
86+
router.navigateByUrl('/a', {replaceUrl: true});
87+
router.navigateByUrl('/b', {replaceUrl: true});
88+
tick();
89+
90+
expect(location.urlChanges).toEqual(['replace: /', 'replace: /b']);
91+
})));
92+
93+
it('should skip navigation if another navigation is already scheduled',
94+
fakeAsync(inject([Router, Location], (router: Router, location: SpyLocation) => {
95+
router.resetConfig([
96+
{path: '', component: SimpleCmp}, {path: 'a', component: SimpleCmp},
97+
{path: 'b', component: SimpleCmp}
98+
]);
99+
100+
const fixture = createRoot(router, RootCmp);
101+
102+
router.navigate(
103+
['/a'], {queryParams: {a: true}, queryParamsHandling: 'merge', replaceUrl: true});
104+
router.navigate(
105+
['/b'], {queryParams: {b: true}, queryParamsHandling: 'merge', replaceUrl: true});
106+
tick();
107+
108+
/**
109+
* Why do we have '/b?b=true' and not '/b?a=true&b=true'?
110+
*
111+
* This is because the router has the right to stop a navigation mid-flight if another
112+
* navigation has been already scheduled. This is why we can use a top-level guard
113+
* to perform redirects. Calling `navigate` in such a guard will stop the navigation, and
114+
* the components won't be instantiated.
115+
*
116+
* This is a fundamental property of the router: it only cares about its latest state.
117+
*
118+
* This means that components should only map params to something else, not reduce them.
119+
* In other words, the following component is asking for trouble:
120+
*
121+
* ```
122+
* class MyComponent {
123+
* constructor(a: ActivatedRoute) {
124+
* a.params.scan(...)
125+
* }
126+
* }
127+
* ```
128+
*
129+
* This also means "queryParamsHandling: 'merge'" should only be used to merge with
130+
* long-living query parameters (e.g., debug).
131+
*/
132+
expect(router.url).toEqual('/b?b=true');
133+
})));
134+
});
75135

76136
describe('should execute navigations serially', () => {
77137
let log: any[] = [];

0 commit comments

Comments
 (0)