Skip to content

Commit 43c5b63

Browse files
vsavkinchuckjaz
authored andcommitted
fix(router): navigating to the current location works (angular#19463)
Closes angular#13340 PR Close angular#19463
1 parent c3a5269 commit 43c5b63

File tree

3 files changed

+120
-95
lines changed

3 files changed

+120
-95
lines changed

packages/common/testing/src/location_mock.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export class SpyLocation implements Location {
4141
return currPath == givenPath + (query.length > 0 ? ('?' + query) : '');
4242
}
4343

44-
simulateUrlPop(pathname: string) { this._subject.emit({'url': pathname, 'pop': true}); }
44+
simulateUrlPop(pathname: string) {
45+
this._subject.emit({'url': pathname, 'pop': true, 'type': 'popstate'});
46+
}
4547

4648
simulateHashChange(pathname: string) {
4749
// Because we don't prevent the native event, the browser will independently update the path

packages/router/src/router.ts

+77-70
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ declare let Zone: any;
4343
*/
4444
export interface NavigationExtras {
4545
/**
46-
* Enables relative navigation from the current ActivatedRoute.
47-
*
48-
* Configuration:
49-
*
50-
* ```
51-
* [{
46+
* Enables relative navigation from the current ActivatedRoute.
47+
*
48+
* Configuration:
49+
*
50+
* ```
51+
* [{
5252
* path: 'parent',
5353
* component: ParentComponent,
5454
* children: [{
@@ -59,92 +59,92 @@ export interface NavigationExtras {
5959
* component: ChildComponent
6060
* }]
6161
* }]
62-
* ```
63-
*
64-
* Navigate to list route from child route:
65-
*
66-
* ```
67-
* @Component({...})
68-
* class ChildComponent {
62+
* ```
63+
*
64+
* Navigate to list route from child route:
65+
*
66+
* ```
67+
* @Component({...})
68+
* class ChildComponent {
6969
* constructor(private router: Router, private route: ActivatedRoute) {}
7070
*
7171
* go() {
7272
* this.router.navigate(['../list'], { relativeTo: this.route });
7373
* }
7474
* }
75-
* ```
76-
*/
75+
* ```
76+
*/
7777
relativeTo?: ActivatedRoute|null;
7878

7979
/**
80-
* Sets query parameters to the URL.
81-
*
82-
* ```
83-
* // Navigate to /results?page=1
84-
* this.router.navigate(['/results'], { queryParams: { page: 1 } });
85-
* ```
86-
*/
80+
* Sets query parameters to the URL.
81+
*
82+
* ```
83+
* // Navigate to /results?page=1
84+
* this.router.navigate(['/results'], { queryParams: { page: 1 } });
85+
* ```
86+
*/
8787
queryParams?: Params|null;
8888

8989
/**
90-
* Sets the hash fragment for the URL.
91-
*
92-
* ```
93-
* // Navigate to /results#top
94-
* this.router.navigate(['/results'], { fragment: 'top' });
95-
* ```
96-
*/
90+
* Sets the hash fragment for the URL.
91+
*
92+
* ```
93+
* // Navigate to /results#top
94+
* this.router.navigate(['/results'], { fragment: 'top' });
95+
* ```
96+
*/
9797
fragment?: string;
9898

9999
/**
100-
* Preserves the query parameters for the next navigation.
101-
*
102-
* deprecated, use `queryParamsHandling` instead
103-
*
104-
* ```
105-
* // Preserve query params from /results?page=1 to /view?page=1
106-
* this.router.navigate(['/view'], { preserveQueryParams: true });
107-
* ```
108-
*
109-
* @deprecated since v4
110-
*/
100+
* Preserves the query parameters for the next navigation.
101+
*
102+
* deprecated, use `queryParamsHandling` instead
103+
*
104+
* ```
105+
* // Preserve query params from /results?page=1 to /view?page=1
106+
* this.router.navigate(['/view'], { preserveQueryParams: true });
107+
* ```
108+
*
109+
* @deprecated since v4
110+
*/
111111
preserveQueryParams?: boolean;
112112

113113
/**
114-
* config strategy to handle the query parameters for the next navigation.
115-
*
116-
* ```
117-
* // from /results?page=1 to /view?page=1&page=2
118-
* this.router.navigate(['/view'], { queryParams: { page: 2 }, queryParamsHandling: "merge" });
119-
* ```
120-
*/
114+
* config strategy to handle the query parameters for the next navigation.
115+
*
116+
* ```
117+
* // from /results?page=1 to /view?page=1&page=2
118+
* this.router.navigate(['/view'], { queryParams: { page: 2 }, queryParamsHandling: "merge" });
119+
* ```
120+
*/
121121
queryParamsHandling?: QueryParamsHandling|null;
122122
/**
123-
* Preserves the fragment for the next navigation
124-
*
125-
* ```
126-
* // Preserve fragment from /results#top to /view#top
127-
* this.router.navigate(['/view'], { preserveFragment: true });
128-
* ```
129-
*/
123+
* Preserves the fragment for the next navigation
124+
*
125+
* ```
126+
* // Preserve fragment from /results#top to /view#top
127+
* this.router.navigate(['/view'], { preserveFragment: true });
128+
* ```
129+
*/
130130
preserveFragment?: boolean;
131131
/**
132-
* Navigates without pushing a new state into history.
133-
*
134-
* ```
135-
* // Navigate silently to /view
136-
* this.router.navigate(['/view'], { skipLocationChange: true });
137-
* ```
138-
*/
132+
* Navigates without pushing a new state into history.
133+
*
134+
* ```
135+
* // Navigate silently to /view
136+
* this.router.navigate(['/view'], { skipLocationChange: true });
137+
* ```
138+
*/
139139
skipLocationChange?: boolean;
140140
/**
141-
* Navigates while replacing the current state in history.
142-
*
143-
* ```
144-
* // Navigate to /view
145-
* this.router.navigate(['/view'], { replaceUrl: true });
146-
* ```
147-
*/
141+
* Navigates while replacing the current state in history.
142+
*
143+
* ```
144+
* // Navigate to /view
145+
* this.router.navigate(['/view'], { replaceUrl: true });
146+
* ```
147+
*/
148148
replaceUrl?: boolean;
149149
}
150150

@@ -518,11 +518,18 @@ export class Router {
518518

519519
// Because of a bug in IE and Edge, the location class fires two events (popstate and
520520
// hashchange) every single time. The second one should be ignored. Otherwise, the URL will
521-
// flicker.
521+
// flicker. Handles the case when a popstate was emitted first.
522522
if (lastNavigation && source == 'hashchange' && lastNavigation.source === 'popstate' &&
523523
lastNavigation.rawUrl.toString() === rawUrl.toString()) {
524524
return Promise.resolve(true); // return value is not used
525525
}
526+
// Because of a bug in IE and Edge, the location class fires two events (popstate and
527+
// hashchange) every single time. The second one should be ignored. Otherwise, the URL will
528+
// flicker. Handles the case when a hashchange was emitted first.
529+
if (lastNavigation && source == 'popstate' && lastNavigation.source === 'hashchange' &&
530+
lastNavigation.rawUrl.toString() === rawUrl.toString()) {
531+
return Promise.resolve(true); // return value is not used
532+
}
526533

527534
let resolve: any = null;
528535
let reject: any = null;
@@ -545,7 +552,7 @@ export class Router {
545552
const url = this.urlHandlingStrategy.extract(rawUrl);
546553
const urlTransition = !this.navigated || url.toString() !== this.currentUrlTree.toString();
547554

548-
if (urlTransition && this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) {
555+
if (this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) {
549556
(this.events as Subject<Event>).next(new NavigationStart(id, this.serializeUrl(url)));
550557
Promise.resolve()
551558
.then(

packages/router/test/integration.spec.ts

+40-24
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ describe('Integration', () => {
5050
expect(fixture.nativeElement).toHaveText('route');
5151
})));
5252

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+
]);
59+
60+
const fixture = createRoot(router, RootCmp);
61+
const events: Event[] = [];
62+
router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e));
63+
64+
router.navigateByUrl('/simple');
65+
tick();
66+
67+
router.navigateByUrl('/simple');
68+
tick();
69+
70+
expectEvents(events, [
71+
[NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'],
72+
[NavigationEnd, '/simple']
73+
]);
74+
})));
75+
5376
describe('should execute navigations serially', () => {
5477
let log: any[] = [];
5578

@@ -428,7 +451,7 @@ describe('Integration', () => {
428451
}]);
429452

430453
const recordedEvents: any[] = [];
431-
router.events.forEach(e => e instanceof RouterEvent && recordedEvents.push(e));
454+
router.events.forEach(e => onlyNavigationStartAndEnd(e) && recordedEvents.push(e));
432455

433456
router.navigateByUrl('/team/22/user/victor');
434457
advance(fixture);
@@ -442,15 +465,8 @@ describe('Integration', () => {
442465
expect(fixture.nativeElement).toHaveText('team 22 [ user fedor, right: ]');
443466

444467
expectEvents(recordedEvents, [
445-
[NavigationStart, '/team/22/user/victor'], [RoutesRecognized, '/team/22/user/victor'],
446-
[GuardsCheckStart, '/team/22/user/victor'], [GuardsCheckEnd, '/team/22/user/victor'],
447-
[ResolveStart, '/team/22/user/victor'], [ResolveEnd, '/team/22/user/victor'],
448-
[NavigationEnd, '/team/22/user/victor'],
449-
450-
[NavigationStart, '/team/22/user/fedor'], [RoutesRecognized, '/team/22/user/fedor'],
451-
[GuardsCheckStart, '/team/22/user/fedor'], [GuardsCheckEnd, '/team/22/user/fedor'],
452-
[ResolveStart, '/team/22/user/fedor'], [ResolveEnd, '/team/22/user/fedor'],
453-
[NavigationEnd, '/team/22/user/fedor']
468+
[NavigationStart, '/team/22/user/victor'], [NavigationEnd, '/team/22/user/victor'],
469+
[NavigationStart, '/team/22/user/fedor'], [NavigationEnd, '/team/22/user/fedor']
454470
]);
455471
})));
456472

@@ -3489,34 +3505,30 @@ describe('Integration', () => {
34893505
}]);
34903506

34913507
const events: any[] = [];
3492-
router.events.subscribe(e => e instanceof RouterEvent && events.push(e));
3508+
router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e));
34933509

34943510
location.go('/include/user/kate(aux:excluded)');
34953511
advance(fixture);
34963512

34973513
expect(location.path()).toEqual('/include/user/kate(aux:excluded)');
3498-
expectEvents(events, [
3499-
[NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'],
3500-
[GuardsCheckStart, '/include/user/kate'], [GuardsCheckEnd, '/include/user/kate'],
3501-
[ResolveStart, '/include/user/kate'], [ResolveEnd, '/include/user/kate'],
3502-
[NavigationEnd, '/include/user/kate']
3503-
]);
3514+
expectEvents(
3515+
events,
3516+
[[NavigationStart, '/include/user/kate'], [NavigationEnd, '/include/user/kate']]);
35043517
events.splice(0);
35053518

35063519
location.go('/include/user/kate(aux:excluded2)');
35073520
advance(fixture);
3508-
expectEvents(events, []);
3521+
expectEvents(
3522+
events,
3523+
[[NavigationStart, '/include/user/kate'], [NavigationEnd, '/include/user/kate']]);
3524+
events.splice(0);
35093525

35103526
router.navigateByUrl('/include/simple');
35113527
advance(fixture);
35123528

35133529
expect(location.path()).toEqual('/include/simple(aux:excluded2)');
3514-
expectEvents(events, [
3515-
[NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'],
3516-
[GuardsCheckStart, '/include/simple'], [GuardsCheckEnd, '/include/simple'],
3517-
[ResolveStart, '/include/simple'], [ResolveEnd, '/include/simple'],
3518-
[NavigationEnd, '/include/simple']
3519-
]);
3530+
expectEvents(
3531+
events, [[NavigationStart, '/include/simple'], [NavigationEnd, '/include/simple']]);
35203532
})));
35213533
});
35223534
});
@@ -3635,6 +3647,10 @@ function expectEvents(events: Event[], pairs: any[]) {
36353647
}
36363648
}
36373649

3650+
function onlyNavigationStartAndEnd(e: Event): boolean {
3651+
return e instanceof NavigationStart || e instanceof NavigationEnd;
3652+
}
3653+
36383654
@Component(
36393655
{selector: 'link-cmp', template: `<a routerLink="/team/33/simple" [target]="'_self'">link</a>`})
36403656
class StringLinkCmp {

0 commit comments

Comments
 (0)