From 92d8bf96194eb06c3023c0fbdf7d64f8c4e7de0a Mon Sep 17 00:00:00 2001 From: vsavkin Date: Sun, 19 Jun 2016 14:44:20 -0700 Subject: [PATCH] feat(router): add support for componentless routes --- .../@angular/router/src/apply_redirects.ts | 8 +- modules/@angular/router/src/config.ts | 7 +- .../@angular/router/src/create_url_tree.ts | 4 +- modules/@angular/router/src/recognize.ts | 55 ++++----- modules/@angular/router/src/router.ts | 102 +++++++++++++---- .../router/test/apply_redirects.spec.ts | 15 +++ modules/@angular/router/test/config.spec.ts | 8 ++ .../router/test/create_router_state.spec.ts | 26 +++++ .../@angular/router/test/recognize.spec.ts | 106 ++++++++++++++++++ modules/@angular/router/test/router.spec.ts | 106 ++++++++++++++++-- modules/@angular/router/tsconfig.json | 1 + 11 files changed, 380 insertions(+), 58 deletions(-) diff --git a/modules/@angular/router/src/apply_redirects.ts b/modules/@angular/router/src/apply_redirects.ts index 69b3ca64b1..392ff54c79 100644 --- a/modules/@angular/router/src/apply_redirects.ts +++ b/modules/@angular/router/src/apply_redirects.ts @@ -35,7 +35,7 @@ function createUrlTree(urlTree: UrlTree, root: UrlSegment): Observable } function expandSegment(routes: Route[], segment: UrlSegment, outlet: string): UrlSegment { - if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) { + if (segment.pathsWithParams.length === 0 && segment.hasChildren()) { return new UrlSegment([], expandSegmentChildren(routes, segment)); } else { return expandPathsWithParams(segment, routes, segment.pathsWithParams, outlet, true); @@ -119,7 +119,7 @@ function matchPathsWithParamsAgainstRoute( return new UrlSegment(consumedPaths, {}); // TODO: check that the right segment is present - } else if (slicedPath.length === 0 && Object.keys(segment.children).length > 0) { + } else if (slicedPath.length === 0 && segment.hasChildren()) { const children = expandSegmentChildren(childConfig, segment); return new UrlSegment(consumedPaths, children); @@ -136,7 +136,7 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): { positionalParamSegments: {[k: string]: UrlPathWithParams} } { if (route.path === '') { - if (route.terminal && (Object.keys(segment.children).length > 0 || paths.length > 0)) { + if (route.terminal && (segment.hasChildren() || paths.length > 0)) { throw new NoMatch(); } else { return {consumedPaths: [], lastChild: 0, positionalParamSegments: {}}; @@ -165,7 +165,7 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): { currentIndex++; } - if (route.terminal && (Object.keys(segment.children).length > 0 || currentIndex < paths.length)) { + if (route.terminal && (segment.hasChildren() || currentIndex < paths.length)) { throw new NoMatch(); } diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index f677fc5474..cf5892c210 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -26,10 +26,15 @@ function validateNode(route: Route): void { throw new Error( `Invalid configuration of route '${route.path}': redirectTo and component cannot be used together`); } + if (route.redirectTo === undefined && !route.component && !route.children) { + throw new Error( + `Invalid configuration of route '${route.path}': component, redirectTo, children must be provided`); + } if (route.path === undefined) { throw new Error(`Invalid route configuration: routes must have path specified`); } if (route.path.startsWith('/')) { - throw new Error(`Invalid route configuration of route '${route.path}': path cannot start with a slash`); + throw new Error( + `Invalid route configuration of route '${route.path}': path cannot start with a slash`); } } \ No newline at end of file diff --git a/modules/@angular/router/src/create_url_tree.ts b/modules/@angular/router/src/create_url_tree.ts index 2c0ad768af..436837bb07 100644 --- a/modules/@angular/router/src/create_url_tree.ts +++ b/modules/@angular/router/src/create_url_tree.ts @@ -139,7 +139,7 @@ function updateSegment(segment: UrlSegment, startIndex: number, commands: any[]) if (!segment) { segment = new UrlSegment([], {}); } - if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) { + if (segment.pathsWithParams.length === 0 && segment.hasChildren()) { return updateSegmentChildren(segment, startIndex, commands); } const m = prefixedWith(segment, startIndex, commands); @@ -147,7 +147,7 @@ function updateSegment(segment: UrlSegment, startIndex: number, commands: any[]) if (m.match && slicedCommands.length === 0) { return new UrlSegment(segment.pathsWithParams, {}); - } else if (m.match && Object.keys(segment.children).length === 0) { + } else if (m.match && !segment.hasChildren()) { return createNewSegment(segment, startIndex, commands); } else if (m.match) { return updateSegmentChildren(segment, 0, slicedCommands); diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index 1b11fcb34b..8189245335 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -5,7 +5,7 @@ import {of } from 'rxjs/observable/of'; import {Route, RouterConfig} from './config'; import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state'; -import {PRIMARY_OUTLET} from './shared'; +import {PRIMARY_OUTLET, Params} from './shared'; import {UrlPathWithParams, UrlSegment, UrlTree, mapChildrenIntoArray} from './url_tree'; import {last, merge} from './utils/collection'; import {TreeNode} from './utils/tree'; @@ -18,7 +18,7 @@ export function recognize( rootComponentType: Type, config: RouterConfig, urlTree: UrlTree, url: string): Observable { try { - const children = processSegment(config, urlTree.root, PRIMARY_OUTLET); + const children = processSegment(config, urlTree.root, {}, PRIMARY_OUTLET); const root = new ActivatedRouteSnapshot( [], {}, PRIMARY_OUTLET, rootComponentType, null, urlTree.root, -1); const rootNode = new TreeNode(root, children); @@ -35,19 +35,20 @@ export function recognize( } } -function processSegment( - config: Route[], segment: UrlSegment, outlet: string): TreeNode[] { - if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) { - return processSegmentChildren(config, segment); +function processSegment(config: Route[], segment: UrlSegment, extraParams: Params, outlet: string): + TreeNode[] { + if (segment.pathsWithParams.length === 0 && segment.hasChildren()) { + return processSegmentChildren(config, segment, extraParams); } else { - return [processPathsWithParams(config, segment, 0, segment.pathsWithParams, outlet)]; + return [processPathsWithParams( + config, segment, 0, segment.pathsWithParams, extraParams, outlet)]; } } function processSegmentChildren( - config: Route[], segment: UrlSegment): TreeNode[] { + config: Route[], segment: UrlSegment, extraParams: Params): TreeNode[] { const children = mapChildrenIntoArray( - segment, (child, childOutlet) => processSegment(config, child, childOutlet)); + segment, (child, childOutlet) => processSegment(config, child, extraParams, childOutlet)); checkOutletNameUniqueness(children); sortActivatedRouteSnapshots(children); return children; @@ -63,10 +64,10 @@ function sortActivatedRouteSnapshots(nodes: TreeNode[]): function processPathsWithParams( config: Route[], segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], - outlet: string): TreeNode { + extraParams: Params, outlet: string): TreeNode { for (let r of config) { try { - return processPathsWithParamsAgainstRoute(r, segment, pathIndex, paths, outlet); + return processPathsWithParamsAgainstRoute(r, segment, pathIndex, paths, extraParams, outlet); } catch (e) { if (!(e instanceof NoMatch)) throw e; } @@ -76,19 +77,20 @@ function processPathsWithParams( function processPathsWithParamsAgainstRoute( route: Route, segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], - outlet: string): TreeNode { + parentExtraParams: Params, outlet: string): TreeNode { if (route.redirectTo) throw new NoMatch(); + if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch(); if (route.path === '**') { const params = paths.length > 0 ? last(paths).parameters : {}; - const snapshot = - new ActivatedRouteSnapshot(paths, params, outlet, route.component, route, segment, -1); + const snapshot = new ActivatedRouteSnapshot( + paths, merge(parentExtraParams, params), outlet, route.component, route, segment, -1); return new TreeNode(snapshot, []); } - const {consumedPaths, parameters, lastChild} = match(segment, route, paths); - + const {consumedPaths, parameters, extraParams, lastChild} = + match(segment, route, paths, parentExtraParams); const snapshot = new ActivatedRouteSnapshot( consumedPaths, parameters, outlet, route.component, route, segment, pathIndex + lastChild - 1); @@ -99,23 +101,24 @@ function processPathsWithParamsAgainstRoute( return new TreeNode(snapshot, []); // TODO: check that the right segment is present - } else if (slicedPath.length === 0 && Object.keys(segment.children).length > 0) { - const children = processSegmentChildren(childConfig, segment); + } else if (slicedPath.length === 0 && segment.hasChildren()) { + const children = processSegmentChildren(childConfig, segment, extraParams); return new TreeNode(snapshot, children); } else { const child = processPathsWithParams( - childConfig, segment, pathIndex + lastChild, slicedPath, PRIMARY_OUTLET); + childConfig, segment, pathIndex + lastChild, slicedPath, extraParams, PRIMARY_OUTLET); return new TreeNode(snapshot, [child]); } } -function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) { +function match( + segment: UrlSegment, route: Route, paths: UrlPathWithParams[], parentExtraParams: Params) { if (route.path === '') { - if (route.terminal && (Object.keys(segment.children).length > 0 || paths.length > 0)) { + if (route.terminal && (segment.hasChildren() || paths.length > 0)) { throw new NoMatch(); } else { - return {consumedPaths: [], lastChild: 0, parameters: {}}; + return {consumedPaths: [], lastChild: 0, parameters: {}, extraParams: {}}; } } @@ -141,12 +144,14 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) { currentIndex++; } - if (route.terminal && (Object.keys(segment.children).length > 0 || currentIndex < paths.length)) { + if (route.terminal && (segment.hasChildren() || currentIndex < paths.length)) { throw new NoMatch(); } - const parameters = merge(posParameters, consumedPaths[consumedPaths.length - 1].parameters); - return {consumedPaths, lastChild: currentIndex, parameters}; + const parameters = merge( + parentExtraParams, merge(posParameters, consumedPaths[consumedPaths.length - 1].parameters)); + const extraParams = route.component ? {} : parameters; + return {consumedPaths, lastChild: currentIndex, parameters, extraParams}; } function checkOutletNameUniqueness(nodes: TreeNode[]): void { diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index f6ba9ba277..9c91e8959d 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -386,29 +386,59 @@ class GuardChecks { const curr = currNode ? currNode.value : null; const outlet = parentOutletMap ? parentOutletMap._outlets[futureNode.value.outlet] : null; + // reusing the node if (curr && future._routeConfig === curr._routeConfig) { if (!shallowEqual(future.params, curr.params)) { this.checks.push(new CanDeactivate(outlet.component, curr), new CanActivate(future)); } - this.traverseChildRoutes(futureNode, currNode, outlet ? outlet.outletMap : null); + + // If we have a component, we need to go through an outlet. + if (future.component) { + this.traverseChildRoutes(futureNode, currNode, outlet ? outlet.outletMap : null); + + // if we have a componentless route, we recurse but keep the same outlet map. + } else { + this.traverseChildRoutes(futureNode, currNode, parentOutletMap); + } } else { - this.deactivateOutletAndItChildren(curr, outlet); + if (curr) { + // if we had a normal route, we need to deactivate only that outlet. + if (curr.component) { + this.deactivateOutletAndItChildren(curr, outlet); + + // if we had a componentless route, we need to deactivate everything! + } else { + this.deactivateOutletMap(parentOutletMap); + } + } + this.checks.push(new CanActivate(future)); - this.traverseChildRoutes(futureNode, null, outlet ? outlet.outletMap : null); + // If we have a component, we need to go through an outlet. + if (future.component) { + this.traverseChildRoutes(futureNode, null, outlet ? outlet.outletMap : null); + + // if we have a componentless route, we recurse but keep the same outlet map. + } else { + this.traverseChildRoutes(futureNode, null, parentOutletMap); + } } } private deactivateOutletAndItChildren(route: ActivatedRouteSnapshot, outlet: RouterOutlet): void { if (outlet && outlet.isActivated) { - forEach(outlet.outletMap._outlets, (v: RouterOutlet) => { - if (v.isActivated) { - this.deactivateOutletAndItChildren(v.activatedRoute.snapshot, v); - } - }); + this.deactivateOutletMap(outlet.outletMap); this.checks.push(new CanDeactivate(outlet.component, route)); } } + private deactivateOutletMap(outletMap: RouterOutletMap): void { + forEach(outletMap._outlets, (v: RouterOutlet) => { + if (v.isActivated) { + this.deactivateOutletAndItChildren(v.activatedRoute.snapshot, v); + } + }); + } + private runCanActivate(future: ActivatedRouteSnapshot): Observable { const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; if (!canActivate || canActivate.length === 0) return of (true); @@ -431,6 +461,7 @@ class GuardChecks { return Observable.from(canDeactivate) .map(c => { const guard = this.injector.get(c); + if (guard.canDeactivate) { return wrapIntoObservable(guard.canDeactivate(component, curr, this.curr)); } else { @@ -480,36 +511,69 @@ class ActivateRoutes { const future = futureNode.value; const curr = currNode ? currNode.value : null; - const outlet = getOutlet(parentOutletMap, futureNode.value); - + // reusing the node if (future === curr) { + // advance the route to push the parameters advanceActivatedRoute(future); - this.activateChildRoutes(futureNode, currNode, outlet.outletMap); + + // If we have a normal route, we need to go through an outlet. + if (future.component) { + const outlet = getOutlet(parentOutletMap, futureNode.value); + this.activateChildRoutes(futureNode, currNode, outlet.outletMap); + + // if we have a componentless route, we recurse but keep the same outlet map. + } else { + this.activateChildRoutes(futureNode, currNode, parentOutletMap); + } } else { - this.deactivateOutletAndItChildren(outlet); - const outletMap = new RouterOutletMap(); - this.activateNewRoutes(outletMap, future, outlet); - this.activateChildRoutes(futureNode, null, outletMap); + if (curr) { + // if we had a normal route, we need to deactivate only that outlet. + if (curr.component) { + const outlet = getOutlet(parentOutletMap, futureNode.value); + this.deactivateOutletAndItChildren(outlet); + + // if we had a componentless route, we need to deactivate everything! + } else { + this.deactivateOutletMap(parentOutletMap); + } + } + + // if we have a normal route, we need to advance the route + // and place the component into the outlet. After that recurse. + if (future.component) { + advanceActivatedRoute(future); + const outlet = getOutlet(parentOutletMap, futureNode.value); + const outletMap = new RouterOutletMap(); + this.placeComponentIntoOutlet(outletMap, future, outlet); + this.activateChildRoutes(futureNode, null, outletMap); + + // if we have a componentless route, we recurse but keep the same outlet map. + } else { + advanceActivatedRoute(future); + this.activateChildRoutes(futureNode, null, parentOutletMap); + } } } - private activateNewRoutes( + private placeComponentIntoOutlet( outletMap: RouterOutletMap, future: ActivatedRoute, outlet: RouterOutlet): void { const resolved = ReflectiveInjector.resolve([ {provide: ActivatedRoute, useValue: future}, {provide: RouterOutletMap, useValue: outletMap} ]); - advanceActivatedRoute(future); outlet.activate(future._futureSnapshot._resolvedComponentFactory, future, resolved, outletMap); } private deactivateOutletAndItChildren(outlet: RouterOutlet): void { if (outlet && outlet.isActivated) { - forEach( - outlet.outletMap._outlets, (v: RouterOutlet) => this.deactivateOutletAndItChildren(v)); + this.deactivateOutletMap(outlet.outletMap); outlet.deactivate(); } } + + private deactivateOutletMap(outletMap: RouterOutletMap): void { + forEach(outletMap._outlets, (v: RouterOutlet) => this.deactivateOutletAndItChildren(v)); + } } function pushQueryParamsAndFragment(state: RouterState): void { diff --git a/modules/@angular/router/test/apply_redirects.spec.ts b/modules/@angular/router/test/apply_redirects.spec.ts index 25c57c8847..59942a990f 100644 --- a/modules/@angular/router/test/apply_redirects.spec.ts +++ b/modules/@angular/router/test/apply_redirects.spec.ts @@ -125,6 +125,21 @@ describe('applyRedirects', () => { }); }); + xit("should support redirects with both main and aux", () => { + checkRedirect([ + {path: 'a', children: [ + {path: 'b', component: ComponentB}, + {path: '', redirectTo: 'b'}, + + {path: 'c', component: ComponentC, outlet: 'aux'}, + {path: '', redirectTo: 'c', outlet: 'aux'} + ]}, + {path: 'a', redirectTo: ''} + ], "a", (t:UrlTree) => { + compareTrees(t, tree('a/(b//aux:c)')); + }); + }); + it("should redirect empty path route only when terminal", () => { const config = [ {path: 'a', component: ComponentA, children: [ diff --git a/modules/@angular/router/test/config.spec.ts b/modules/@angular/router/test/config.spec.ts index 5812bcb7a9..503fbc3bcd 100644 --- a/modules/@angular/router/test/config.spec.ts +++ b/modules/@angular/router/test/config.spec.ts @@ -37,6 +37,14 @@ describe('config', () => { }).toThrowError(`Invalid route configuration: routes must have path specified`); }); + it("should throw when none of component and children or direct are missing", () => { + expect(() => { + validateConfig([ + {path: 'a'} + ]); + }).toThrowError(`Invalid configuration of route 'a': component, redirectTo, children must be provided`); + }); + it("should throw when path starts with a slash", () => { expect(() => { validateConfig([ diff --git a/modules/@angular/router/test/create_router_state.spec.ts b/modules/@angular/router/test/create_router_state.spec.ts index 4cfa90bc8d..31c22a01b5 100644 --- a/modules/@angular/router/test/create_router_state.spec.ts +++ b/modules/@angular/router/test/create_router_state.spec.ts @@ -44,6 +44,32 @@ describe('create router state', () => { expect(prevC[1]).not.toBe(currC[1]); checkActivatedRoute(currC[1], ComponentC, 'left'); }); + + it('should handle componentless routes', () => { + const config = [ + { path: 'a/:id', children: [ + { path: 'b', component: ComponentA }, + { path: 'c', component: ComponentB, outlet: 'right' } + ] } + ]; + + + const prevState = createRouterState(createState(config, "a/1;p=11/(b//right:c)"), emptyState()); + advanceState(prevState); + const state = createRouterState(createState(config, "a/2;p=22/(b//right:c)"), prevState); + + expect(prevState.root).toBe(state.root); + const prevP = prevState.firstChild(prevState.root); + const currP = state.firstChild(state.root); + expect(prevP).toBe(currP); + + const prevC = prevState.children(prevP); + const currC = state.children(currP); + + expect(currP._futureSnapshot.params).toEqual({id: '2', p: '22'}); + checkActivatedRoute(currC[0], ComponentA); + checkActivatedRoute(currC[1], ComponentB, 'right'); + }); }); function advanceState(state: RouterState): void { diff --git a/modules/@angular/router/test/recognize.spec.ts b/modules/@angular/router/test/recognize.spec.ts index 73ff4b0354..ef9a88ccb7 100644 --- a/modules/@angular/router/test/recognize.spec.ts +++ b/modules/@angular/router/test/recognize.spec.ts @@ -243,6 +243,112 @@ describe('recognize', () => { }); }); + describe("componentless routes", () => { + it("should work", () => { + checkRecognize([ + { + path: 'p/:id', + children: [ + {path: 'a', component: ComponentA}, + {path: 'b', component: ComponentB, outlet: 'aux'} + ] + } + ], "p/11;pp=22/(a;pa=33//aux:b;pb=44)", (s:RouterStateSnapshot) => { + const p = s.firstChild(s.root); + checkActivatedRoute(p, "p/11", {id: '11', pp: '22'}, undefined); + + const c = s.children(p); + checkActivatedRoute(c[0], "a", {id: '11', pp: '22', pa: '33'}, ComponentA); + checkActivatedRoute(c[1], "b", {id: '11', pp: '22', pb: '44'}, ComponentB, "aux"); + }); + }); + + it("should merge params until encounters a normal route", () => { + checkRecognize([ + { + path: 'p/:id', + children: [ + {path: 'a/:name', children: [ + {path: 'b', component: ComponentB, children: [ + {path: 'c', component: ComponentC} + ]} + ]} + ] + } + ], "p/11/a/victor/b/c", (s:RouterStateSnapshot) => { + const p = s.firstChild(s.root); + checkActivatedRoute(p, "p/11", {id: '11'}, undefined); + + const a = s.firstChild(p); + checkActivatedRoute(a, "a/victor", {id: '11', name: 'victor'}, undefined); + + const b = s.firstChild(a); + checkActivatedRoute(b, "b", {id: '11', name: 'victor'}, ComponentB); + + const c = s.firstChild(b); + checkActivatedRoute(c, "c", {}, ComponentC); + }); + }); + + xit("should work with empty paths", () => { + checkRecognize([ + { + path: 'p/:id', + children: [ + {path: '', component: ComponentA}, + {path: '', component: ComponentB, outlet: 'aux'} + ] + } + ], "p/11", (s:RouterStateSnapshot) => { + const p = s.firstChild(s.root); + checkActivatedRoute(p, "p/11", {id: '11'}, undefined); + + const c = s.children(p); + console.log("lsfs", c); + checkActivatedRoute(c[0], "", {}, ComponentA); + checkActivatedRoute(c[1], "", {}, ComponentB, "aux"); + }); + }); + + xit("should work with empty paths and params", () => { + checkRecognize([ + { + path: 'p/:id', + children: [ + {path: '', component: ComponentA}, + {path: '', component: ComponentB, outlet: 'aux'} + ] + } + ], "p/11/(;pa=33//aux:;pb=44)", (s:RouterStateSnapshot) => { + const p = s.firstChild(s.root); + checkActivatedRoute(p, "p/11", {id: '11'}, undefined); + + const c = s.children(p); + checkActivatedRoute(c[0], "", {pa: '33'}, ComponentA); + checkActivatedRoute(c[1], "", {pb: '44'}, ComponentB, "aux"); + }); + }); + + xit("should work with only aux path", () => { + checkRecognize([ + { + path: 'p/:id', + children: [ + {path: '', component: ComponentA}, + {path: '', component: ComponentB, outlet: 'aux'} + ] + } + ], "p/11", (s:RouterStateSnapshot) => { + const p = s.firstChild(s.root); + checkActivatedRoute(p, "p/11(aux:;pb=44)", {id: '11'}, undefined); + + const c = s.children(p); + checkActivatedRoute(c[0], "", {}, ComponentA); + checkActivatedRoute(c[1], "", {pb: '44'}, ComponentB, "aux"); + }); + }); + }); + describe("query parameters", () => { it("should support query params", () => { const config = [{path: 'a', component: ComponentA}]; diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index 4cde4d7cb7..3de085f739 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -375,7 +375,46 @@ describe("Integration", () => { advance(fixture); expect(location.path()).toEqual('/team/33/simple'); }))); - + + it('should handle componentless paths', + fakeAsync(inject([Router, TestComponentBuilder, Location], (router:Router, tcb:TestComponentBuilder, location:Location) => { + const fixture = tcb.createFakeAsync(RootCmpWithTwoOutlets); + advance(fixture); + + router.resetConfig([ + { path: 'parent/:id', children: [ + { path: 'simple', component: SimpleCmp }, + { path: 'user/:name', component: UserCmp, outlet: 'right' } + ] }, + { path: 'user/:name', component: UserCmp } + ]); + + + // navigate to a componentless route + router.navigateByUrl('/parent/11/(simple//right:user/victor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); + expect(fixture.debugElement.nativeElement).toHaveText('primary {simple} right {user victor}'); + + // navigate to the same route with different params (reuse) + router.navigateByUrl('/parent/22/(simple//right:user/fedor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/22/(simple//right:user/fedor)'); + expect(fixture.debugElement.nativeElement).toHaveText('primary {simple} right {user fedor}'); + + // navigate to a normal route (check deactivation) + router.navigateByUrl('/user/victor'); + advance(fixture); + expect(location.path()).toEqual('/user/victor'); + expect(fixture.debugElement.nativeElement).toHaveText('primary {user victor} right {}'); + + // navigate back to a componentless route + router.navigateByUrl('/parent/11/(simple//right:user/victor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); + expect(fixture.debugElement.nativeElement).toHaveText('primary {simple} right {user victor}'); + }))); + describe("router links", () => { it("should support string router links", fakeAsync(inject([Router, TestComponentBuilder], (router:Router, tcb:TestComponentBuilder) => { @@ -526,6 +565,29 @@ describe("Integration", () => { }))); }); + describe("should not activate a route when CanActivate returns false (componentless route)", () => { + beforeEachProviders(() => [ + {provide: 'alwaysFalse', useValue: (a:any, b:any) => false} + ]); + + it('works', + fakeAsync(inject([Router, TestComponentBuilder, Location], (router:Router, tcb:TestComponentBuilder, location:Location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.resetConfig([ + { path: 'parent', canActivate: ['alwaysFalse'], children: [ + { path: 'team/:id', component: TeamCmp } + ]} + ]); + + router.navigateByUrl('parent/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/'); + }))); + }); + describe("should activate a route when CanActivate returns true", () => { beforeEachProviders(() => [ {provide: 'alwaysTrue', useValue: (a:ActivatedRouteSnapshot, s:RouterStateSnapshot) => true} @@ -598,15 +660,17 @@ describe("Integration", () => { describe("CanDeactivate", () => { describe("should not deactivate a route when CanDeactivate returns false", () => { beforeEachProviders(() => [ - {provide: 'CanDeactivateTeam', useValue: (c:TeamCmp, a:ActivatedRouteSnapshot, b:RouterStateSnapshot) => { + {provide: 'CanDeactivateParent', useValue: (c:any, a:ActivatedRouteSnapshot, b:RouterStateSnapshot) => { + return a.params['id'] === "22"; + }}, + {provide: 'CanDeactivateTeam', useValue: (c:any, a:ActivatedRouteSnapshot, b:RouterStateSnapshot) => { return c.route.snapshot.params['id'] === "22"; }}, - {provide: 'CanDeactivateUser', useValue: (c:UserCmp, a:ActivatedRouteSnapshot, b:RouterStateSnapshot) => { + {provide: 'CanDeactivateUser', useValue: (c:any, a:ActivatedRouteSnapshot, b:RouterStateSnapshot) => { return a.params['name'] === 'victor'; }} ]); - it('works', fakeAsync(inject([Router, TestComponentBuilder, Location], (router:Router, tcb:TestComponentBuilder, location:Location) => { const fixture = tcb.createFakeAsync(RootCmp); @@ -618,20 +682,41 @@ describe("Integration", () => { router.navigateByUrl('/team/22'); advance(fixture); - expect(location.path()).toEqual('/team/22'); router.navigateByUrl('/team/33'); advance(fixture); - expect(location.path()).toEqual('/team/33'); router.navigateByUrl('/team/44'); advance(fixture); - expect(location.path()).toEqual('/team/33'); }))); + it('works (componentless route)', + fakeAsync(inject([Router, TestComponentBuilder, Location], (router:Router, tcb:TestComponentBuilder, location:Location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.resetConfig([ + { path: 'parent/:id', canDeactivate: ["CanDeactivateParent"], children: [ + { path: 'simple', component: SimpleCmp } + ] } + ]); + + router.navigateByUrl('/parent/22/simple'); + advance(fixture); + expect(location.path()).toEqual('/parent/22/simple'); + + router.navigateByUrl('/parent/33/simple'); + advance(fixture); + expect(location.path()).toEqual('/parent/33/simple'); + + router.navigateByUrl('/parent/44/simple'); + advance(fixture); + expect(location.path()).toEqual('/parent/33/simple'); + }))); + it('works with a nested route', fakeAsync(inject([Router, TestComponentBuilder, Location], (router:Router, tcb:TestComponentBuilder, location:Location) => { const fixture = tcb.createFakeAsync(RootCmp); @@ -927,6 +1012,13 @@ class QueryParamsAndFragmentCmp { }) class RootCmp {} +@Component({ + selector: 'root-cmp', + template: `primary {} right {}`, + directives: [ROUTER_DIRECTIVES] +}) +class RootCmpWithTwoOutlets {} + function advance(fixture: ComponentFixture): void { tick(); fixture.detectChanges(); diff --git a/modules/@angular/router/tsconfig.json b/modules/@angular/router/tsconfig.json index 168a8fa3f7..8cabff1338 100644 --- a/modules/@angular/router/tsconfig.json +++ b/modules/@angular/router/tsconfig.json @@ -39,6 +39,7 @@ "test/url_tree.spec.ts", "test/utils/tree.spec.ts", "test/url_serializer.spec.ts", + "test/resolve.spec.ts", "test/apply_redirects.spec.ts", "test/recognize.spec.ts", "test/create_router_state.spec.ts",