diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index 1c1df4b5db..00c418b42a 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -154,6 +154,9 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree'; * When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with * the user component in it. * + * An empty path route inherits its parent's params and data. This is because it cannot have its + * own params, and, as a result, it often uses its parent's params and data as its own. + * * ### Matching Strategy * * By default the router will look at what is left in the url, and check if it starts with @@ -219,7 +222,8 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree'; * has to have the primary and aux outlets defined. * * The router will also merge the `params`, `data`, and `resolve` of the componentless parent into - * the `params`, `data`, and `resolve` of the children. + * the `params`, `data`, and `resolve` of the children. This is done because there is no component + * that can inject the activated route of the componentless parent. * * This is especially useful when child components are defined as follows: * diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 3a85679241..f24a95e993 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -623,7 +623,8 @@ export class Router { Promise.resolve() .then( (_) => this.runNavigate( - url, rawUrl, false, false, id, createEmptyState(url, this.rootComponentType))) + url, rawUrl, false, false, id, + createEmptyState(url, this.rootComponentType).snapshot)) .then(resolve, reject); } else { @@ -634,7 +635,7 @@ export class Router { private runNavigate( url: UrlTree, rawUrl: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean, - id: number, precreatedState: RouterState): Promise { + id: number, precreatedState: RouterStateSnapshot): Promise { if (id !== this.navigationId) { this.location.go(this.urlSerializer.serialize(this.currentUrlTree)); this.routerEvents.next(new NavigationCancel( @@ -644,68 +645,80 @@ export class Router { } return new Promise((resolvePromise, rejectPromise) => { - let state: RouterState; - let navigationIsSuccessful: boolean; - let preActivation: PreActivation; - - let appliedUrl: UrlTree; - - const storedState = this.currentRouterState; - const storedUrl = this.currentUrlTree; - - let routerState$: any; - + // create an observable of the url and route state snapshot + // this operation do not result in any side effects + let urlAndSnapshot$: Observable<{appliedUrl: UrlTree, snapshot: RouterStateSnapshot}>; if (!precreatedState) { const redirectsApplied$ = applyRedirects(this.injector, this.configLoader, url, this.config); - const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => { - appliedUrl = u; - return recognize( - this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)); - }); + urlAndSnapshot$ = mergeMap.call(redirectsApplied$, (appliedUrl: UrlTree) => { + return map.call( + recognize( + this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)), + (snapshot: any) => { - const emitRecognzied$ = - map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => { - this.routerEvents.next(new RoutesRecognized( - id, this.serializeUrl(url), this.serializeUrl(appliedUrl), - newRouterStateSnapshot)); - return newRouterStateSnapshot; - }); + this.routerEvents.next(new RoutesRecognized( + id, this.serializeUrl(url), this.serializeUrl(appliedUrl), snapshot)); - routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => { - return createRouterState(routerStateSnapshot, this.currentRouterState); + return {appliedUrl, snapshot}; + }); }); } else { - appliedUrl = url; - routerState$ = of (precreatedState); + urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState}); } - const preactivation$ = map.call(routerState$, (newState: RouterState) => { - state = newState; + + // run preactivation: guards and data resolvers + let preActivation: PreActivation; + const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => { preActivation = - new PreActivation(state.snapshot, this.currentRouterState.snapshot, this.injector); + new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector); preActivation.traverse(this.outletMap); + return {appliedUrl, snapshot}; }); - const preactivation2$ = mergeMap.call(preactivation$, () => { + const preactivationCheckGuards = + mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => { + if (this.navigationId !== id) return of (false); + + return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => { + return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate}; + }); + }); + + const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => { if (this.navigationId !== id) return of (false); - return preActivation.checkGuards(); - }); - - const resolveData$ = mergeMap.call(preactivation2$, (shouldActivate: boolean) => { - if (this.navigationId !== id) return of (false); - - if (shouldActivate) { - return map.call(preActivation.resolveData(), () => shouldActivate); + if (p.shouldActivate) { + return map.call(preActivation.resolveData(), () => p); } else { - return of (shouldActivate); + return of (p); } }); - resolveData$ - .forEach((shouldActivate: boolean) => { + + // create router state + // this operation has side effects => route state is being affected + const routerState$ = + map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => { + if (shouldActivate) { + const state = createRouterState(snapshot, this.currentRouterState); + return {appliedUrl, state, shouldActivate}; + } else { + return {appliedUrl, state: null, shouldActivate}; + } + }); + + + // applied the new router state + // this operation has side effects + let navigationIsSuccessful: boolean; + const storedState = this.currentRouterState; + const storedUrl = this.currentUrlTree; + + routerState$ + .forEach(({appliedUrl, state, shouldActivate}: any) => { if (!shouldActivate || id !== this.navigationId) { navigationIsSuccessful = false; return; @@ -733,8 +746,8 @@ export class Router { () => { this.navigated = true; if (navigationIsSuccessful) { - this.routerEvents.next( - new NavigationEnd(id, this.serializeUrl(url), this.serializeUrl(appliedUrl))); + this.routerEvents.next(new NavigationEnd( + id, this.serializeUrl(url), this.serializeUrl(this.currentUrlTree))); resolvePromise(true); } else { this.resetUrlToCurrentUrlTree(); diff --git a/modules/@angular/router/test/integration.spec.ts b/modules/@angular/router/test/integration.spec.ts index 13a46ae470..e81c74cc74 100644 --- a/modules/@angular/router/test/integration.spec.ts +++ b/modules/@angular/router/test/integration.spec.ts @@ -1156,8 +1156,6 @@ describe('Integration', () => { advance(fixture); expect(location.path()).toEqual('/initial'); }))); - - // should not break the back button when trigger by initial navigation }); describe('guards', () => { @@ -1380,6 +1378,11 @@ describe('Integration', () => { return true; } }, + { + provide: 'alwaysFalse', + useValue: + (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; } + }, ] }); }); @@ -1504,6 +1507,31 @@ describe('Integration', () => { advance(fixture); expect(location.path()).toEqual('/team/33/user/fedor'); }))); + + it('should not create a route state if navigation is canceled', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{ + path: 'main', + component: TeamCmp, + children: [ + {path: 'component1', component: SimpleCmp, canDeactivate: ['alwaysFalse']}, + {path: 'component2', component: SimpleCmp} + ] + }]); + + router.navigateByUrl('/main/component1'); + advance(fixture); + + router.navigateByUrl('/main/component2'); + advance(fixture); + + const teamCmp = fixture.debugElement.children[1].componentInstance; + expect(teamCmp.route.firstChild.url.value[0].path).toEqual('component1'); + expect(location.path()).toEqual('/main/component1'); + }))); + }); describe('should work when given a class', () => {