diff --git a/packages/common/testing/src/location_mock.ts b/packages/common/testing/src/location_mock.ts index f52fcc4ecb..b2acc613ca 100644 --- a/packages/common/testing/src/location_mock.ts +++ b/packages/common/testing/src/location_mock.ts @@ -41,7 +41,9 @@ export class SpyLocation implements Location { return currPath == givenPath + (query.length > 0 ? ('?' + query) : ''); } - simulateUrlPop(pathname: string) { this._subject.emit({'url': pathname, 'pop': true}); } + simulateUrlPop(pathname: string) { + this._subject.emit({'url': pathname, 'pop': true, 'type': 'popstate'}); + } simulateHashChange(pathname: string) { // Because we don't prevent the native event, the browser will independently update the path diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index cd2d68c35d..920715c1c4 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -43,12 +43,12 @@ declare let Zone: any; */ export interface NavigationExtras { /** - * Enables relative navigation from the current ActivatedRoute. - * - * Configuration: - * - * ``` - * [{ + * Enables relative navigation from the current ActivatedRoute. + * + * Configuration: + * + * ``` + * [{ * path: 'parent', * component: ParentComponent, * children: [{ @@ -59,92 +59,92 @@ export interface NavigationExtras { * component: ChildComponent * }] * }] - * ``` - * - * Navigate to list route from child route: - * - * ``` - * @Component({...}) - * class ChildComponent { + * ``` + * + * Navigate to list route from child route: + * + * ``` + * @Component({...}) + * class ChildComponent { * constructor(private router: Router, private route: ActivatedRoute) {} * * go() { * this.router.navigate(['../list'], { relativeTo: this.route }); * } * } - * ``` - */ + * ``` + */ relativeTo?: ActivatedRoute|null; /** - * Sets query parameters to the URL. - * - * ``` - * // Navigate to /results?page=1 - * this.router.navigate(['/results'], { queryParams: { page: 1 } }); - * ``` - */ + * Sets query parameters to the URL. + * + * ``` + * // Navigate to /results?page=1 + * this.router.navigate(['/results'], { queryParams: { page: 1 } }); + * ``` + */ queryParams?: Params|null; /** - * Sets the hash fragment for the URL. - * - * ``` - * // Navigate to /results#top - * this.router.navigate(['/results'], { fragment: 'top' }); - * ``` - */ + * Sets the hash fragment for the URL. + * + * ``` + * // Navigate to /results#top + * this.router.navigate(['/results'], { fragment: 'top' }); + * ``` + */ fragment?: string; /** - * Preserves the query parameters for the next navigation. - * - * deprecated, use `queryParamsHandling` instead - * - * ``` - * // Preserve query params from /results?page=1 to /view?page=1 - * this.router.navigate(['/view'], { preserveQueryParams: true }); - * ``` - * - * @deprecated since v4 - */ + * Preserves the query parameters for the next navigation. + * + * deprecated, use `queryParamsHandling` instead + * + * ``` + * // Preserve query params from /results?page=1 to /view?page=1 + * this.router.navigate(['/view'], { preserveQueryParams: true }); + * ``` + * + * @deprecated since v4 + */ preserveQueryParams?: boolean; /** - * config strategy to handle the query parameters for the next navigation. - * - * ``` - * // from /results?page=1 to /view?page=1&page=2 - * this.router.navigate(['/view'], { queryParams: { page: 2 }, queryParamsHandling: "merge" }); - * ``` - */ + * config strategy to handle the query parameters for the next navigation. + * + * ``` + * // from /results?page=1 to /view?page=1&page=2 + * this.router.navigate(['/view'], { queryParams: { page: 2 }, queryParamsHandling: "merge" }); + * ``` + */ queryParamsHandling?: QueryParamsHandling|null; /** - * Preserves the fragment for the next navigation - * - * ``` - * // Preserve fragment from /results#top to /view#top - * this.router.navigate(['/view'], { preserveFragment: true }); - * ``` - */ + * Preserves the fragment for the next navigation + * + * ``` + * // Preserve fragment from /results#top to /view#top + * this.router.navigate(['/view'], { preserveFragment: true }); + * ``` + */ preserveFragment?: boolean; /** - * Navigates without pushing a new state into history. - * - * ``` - * // Navigate silently to /view - * this.router.navigate(['/view'], { skipLocationChange: true }); - * ``` - */ + * Navigates without pushing a new state into history. + * + * ``` + * // Navigate silently to /view + * this.router.navigate(['/view'], { skipLocationChange: true }); + * ``` + */ skipLocationChange?: boolean; /** - * Navigates while replacing the current state in history. - * - * ``` - * // Navigate to /view - * this.router.navigate(['/view'], { replaceUrl: true }); - * ``` - */ + * Navigates while replacing the current state in history. + * + * ``` + * // Navigate to /view + * this.router.navigate(['/view'], { replaceUrl: true }); + * ``` + */ replaceUrl?: boolean; } @@ -518,11 +518,18 @@ export class Router { // 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. + // flicker. Handles the case when a popstate was emitted first. if (lastNavigation && source == 'hashchange' && lastNavigation.source === 'popstate' && lastNavigation.rawUrl.toString() === rawUrl.toString()) { return Promise.resolve(true); // 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. Handles the case when a hashchange was emitted first. + if (lastNavigation && source == 'popstate' && lastNavigation.source === 'hashchange' && + lastNavigation.rawUrl.toString() === rawUrl.toString()) { + return Promise.resolve(true); // return value is not used + } let resolve: any = null; let reject: any = null; @@ -545,7 +552,7 @@ export class Router { const url = this.urlHandlingStrategy.extract(rawUrl); const urlTransition = !this.navigated || url.toString() !== this.currentUrlTree.toString(); - if (urlTransition && this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) { + if (this.urlHandlingStrategy.shouldProcessUrl(rawUrl)) { (this.events as Subject).next(new NavigationStart(id, this.serializeUrl(url))); Promise.resolve() .then( diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 5c78c3e083..4682d8b93e 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -50,6 +50,29 @@ describe('Integration', () => { expect(fixture.nativeElement).toHaveText('route'); }))); + it('should navigate to the current URL', + fakeAsync(inject([Router, Location], (router: Router) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'simple', component: SimpleCmp}, + ]); + + const fixture = createRoot(router, RootCmp); + const events: Event[] = []; + router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e)); + + router.navigateByUrl('/simple'); + tick(); + + router.navigateByUrl('/simple'); + tick(); + + expectEvents(events, [ + [NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'], + [NavigationEnd, '/simple'] + ]); + }))); + describe('should execute navigations serially', () => { let log: any[] = []; @@ -428,7 +451,7 @@ describe('Integration', () => { }]); const recordedEvents: any[] = []; - router.events.forEach(e => e instanceof RouterEvent && recordedEvents.push(e)); + router.events.forEach(e => onlyNavigationStartAndEnd(e) && recordedEvents.push(e)); router.navigateByUrl('/team/22/user/victor'); advance(fixture); @@ -442,15 +465,8 @@ describe('Integration', () => { expect(fixture.nativeElement).toHaveText('team 22 [ user fedor, right: ]'); expectEvents(recordedEvents, [ - [NavigationStart, '/team/22/user/victor'], [RoutesRecognized, '/team/22/user/victor'], - [GuardsCheckStart, '/team/22/user/victor'], [GuardsCheckEnd, '/team/22/user/victor'], - [ResolveStart, '/team/22/user/victor'], [ResolveEnd, '/team/22/user/victor'], - [NavigationEnd, '/team/22/user/victor'], - - [NavigationStart, '/team/22/user/fedor'], [RoutesRecognized, '/team/22/user/fedor'], - [GuardsCheckStart, '/team/22/user/fedor'], [GuardsCheckEnd, '/team/22/user/fedor'], - [ResolveStart, '/team/22/user/fedor'], [ResolveEnd, '/team/22/user/fedor'], - [NavigationEnd, '/team/22/user/fedor'] + [NavigationStart, '/team/22/user/victor'], [NavigationEnd, '/team/22/user/victor'], + [NavigationStart, '/team/22/user/fedor'], [NavigationEnd, '/team/22/user/fedor'] ]); }))); @@ -3489,34 +3505,30 @@ describe('Integration', () => { }]); const events: any[] = []; - router.events.subscribe(e => e instanceof RouterEvent && events.push(e)); + router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e)); location.go('/include/user/kate(aux:excluded)'); advance(fixture); expect(location.path()).toEqual('/include/user/kate(aux:excluded)'); - expectEvents(events, [ - [NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'], - [GuardsCheckStart, '/include/user/kate'], [GuardsCheckEnd, '/include/user/kate'], - [ResolveStart, '/include/user/kate'], [ResolveEnd, '/include/user/kate'], - [NavigationEnd, '/include/user/kate'] - ]); + expectEvents( + events, + [[NavigationStart, '/include/user/kate'], [NavigationEnd, '/include/user/kate']]); events.splice(0); location.go('/include/user/kate(aux:excluded2)'); advance(fixture); - expectEvents(events, []); + expectEvents( + events, + [[NavigationStart, '/include/user/kate'], [NavigationEnd, '/include/user/kate']]); + events.splice(0); router.navigateByUrl('/include/simple'); advance(fixture); expect(location.path()).toEqual('/include/simple(aux:excluded2)'); - expectEvents(events, [ - [NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'], - [GuardsCheckStart, '/include/simple'], [GuardsCheckEnd, '/include/simple'], - [ResolveStart, '/include/simple'], [ResolveEnd, '/include/simple'], - [NavigationEnd, '/include/simple'] - ]); + expectEvents( + events, [[NavigationStart, '/include/simple'], [NavigationEnd, '/include/simple']]); }))); }); }); @@ -3635,6 +3647,10 @@ function expectEvents(events: Event[], pairs: any[]) { } } +function onlyNavigationStartAndEnd(e: Event): boolean { + return e instanceof NavigationStart || e instanceof NavigationEnd; +} + @Component( {selector: 'link-cmp', template: `link`}) class StringLinkCmp {