diff --git a/modules/@angular/router/src/create_url_tree.ts b/modules/@angular/router/src/create_url_tree.ts index fae0b91e42..b30b2591cc 100644 --- a/modules/@angular/router/src/create_url_tree.ts +++ b/modules/@angular/router/src/create_url_tree.ts @@ -96,8 +96,7 @@ function findStartingNode(normalizedChange: NormalizedNavigationCommands, urlTre } function findUrlSegment(route: ActivatedRoute, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment { - const segments = (route.urlSegments).value; - const urlSegment = segments[segments.length - 1]; + const urlSegment = route.snapshot._lastUrlSegment; const path = urlTree.pathFromRoot(urlSegment); if (path.length <= numberOfDoubleDots) { throw new Error("Invalid number of '../'"); diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index d8c9196acb..8a8eff4b06 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -9,7 +9,7 @@ import { Observable } from 'rxjs/Observable'; export function recognize(rootComponentType: Type, config: RouterConfig, url: UrlTree): Observable { try { - const match = new MatchResult(rootComponentType, config, [url.root], {}, url._root.children, [], PRIMARY_OUTLET, null); + const match = new MatchResult(rootComponentType, config, [url.root], {}, url._root.children, [], PRIMARY_OUTLET, null, url.root); const roots = constructActivatedRoute(match); const res = new RouterStateSnapshot(roots[0], url.queryParameters, url.fragment); return new Observable(obs => { @@ -23,18 +23,21 @@ export function recognize(rootComponentType: Type, config: RouterConfig, url: Ur function constructActivatedRoute(match: MatchResult): TreeNode[] { const activatedRoute = createActivatedRouteSnapshot(match); - if (match.leftOverUrl.length > 0) { - const children = recognizeMany(match.children, match.leftOverUrl); - checkOutletNameUniqueness(children); - children.sort((a, b) => { - if (a.value.outlet === PRIMARY_OUTLET) return -1; - if (b.value.outlet === PRIMARY_OUTLET) return 1; - return a.value.outlet.localeCompare(b.value.outlet) - }); - return [new TreeNode(activatedRoute, children)]; - } else { - return [new TreeNode(activatedRoute, [])]; - } + const children = match.leftOverUrl.length > 0 ? + recognizeMany(match.children, match.leftOverUrl) : recognizeLeftOvers(match.children, match.lastUrlSegment); + checkOutletNameUniqueness(children); + children.sort((a, b) => { + if (a.value.outlet === PRIMARY_OUTLET) return -1; + if (b.value.outlet === PRIMARY_OUTLET) return 1; + return a.value.outlet.localeCompare(b.value.outlet) + }); + return [new TreeNode(activatedRoute, children)]; +} + +function recognizeLeftOvers(config: Route[], lastUrlSegment: UrlSegment): TreeNode[] { + if (!config) return []; + const mIndex = matchIndex(config, [], lastUrlSegment); + return mIndex ? constructActivatedRoute(mIndex) : []; } function recognizeMany(config: Route[], urls: TreeNode[]): TreeNode[] { @@ -42,7 +45,7 @@ function recognizeMany(config: Route[], urls: TreeNode[]): TreeNode< } function createActivatedRouteSnapshot(match: MatchResult): ActivatedRouteSnapshot { - return new ActivatedRouteSnapshot(match.consumedUrlSegments, match.parameters, match.outlet, match.component, match.route); + return new ActivatedRouteSnapshot(match.consumedUrlSegments, match.parameters, match.outlet, match.component, match.route, match.lastUrlSegment); } function recognizeOne(config: Route[], url: TreeNode): TreeNode[] { @@ -72,7 +75,7 @@ function match(config: Route[], url: TreeNode): MatchResult { const m = matchNonIndex(config, url); if (m) return m; - const mIndex = matchIndex(config, url); + const mIndex = matchIndex(config, [url], url.value); if (mIndex) return mIndex; const availableRoutes = config.map(r => { @@ -91,12 +94,12 @@ function matchNonIndex(config: Route[], url: TreeNode): MatchResult return null; } -function matchIndex(config: Route[], url: TreeNode): MatchResult | null { +function matchIndex(config: Route[], leftOverUrls: TreeNode[], lastUrlSegment: UrlSegment): MatchResult | null { for (let r of config) { if (r.index) { const outlet = r.outlet ? r.outlet : PRIMARY_OUTLET; const children = r.children ? r.children : []; - return new MatchResult(r.component, children, [], {}, [url], [], outlet, r); + return new MatchResult(r.component, children, [], lastUrlSegment.parameters, leftOverUrls, [], outlet, r, lastUrlSegment); } } return null; @@ -115,7 +118,7 @@ function matchWithParts(route: Route, url: TreeNode): MatchResult | u = first(u.children); } const last = consumedUrl[consumedUrl.length - 1]; - return new MatchResult(route.component, [], consumedUrl, last.parameters, [], [], PRIMARY_OUTLET, route); + return new MatchResult(route.component, [], consumedUrl, last.parameters, [], [], PRIMARY_OUTLET, route, last); } const parts = path.split("/"); @@ -160,7 +163,7 @@ function matchWithParts(route: Route, url: TreeNode): MatchResult | const outlet = route.outlet ? route.outlet : PRIMARY_OUTLET; return new MatchResult(route.component, children, consumedUrlSegments, parameters, lastSegment.children, - secondarySubtrees, outlet, route); + secondarySubtrees, outlet, route, lastSegment.value); } class MatchResult { @@ -171,6 +174,7 @@ class MatchResult { public leftOverUrl: TreeNode[], public secondary: TreeNode[], public outlet: string, - public route: Route + public route: Route, + public lastUrlSegment: UrlSegment ) {} } \ No newline at end of file diff --git a/modules/@angular/router/src/router_state.ts b/modules/@angular/router/src/router_state.ts index c8355e4329..84e04e7468 100644 --- a/modules/@angular/router/src/router_state.ts +++ b/modules/@angular/router/src/router_state.ts @@ -38,11 +38,12 @@ export function createEmptyState(rootComponent: Type): RouterState { } function createEmptyStateSnapshot(rootComponent: Type): RouterStateSnapshot { - const emptyUrl = [new UrlSegment("", {}, PRIMARY_OUTLET)]; + const rootUrlSegment = new UrlSegment("", {}, PRIMARY_OUTLET); + const emptyUrl = [rootUrlSegment]; const emptyParams = {}; const emptyQueryParams = {}; const fragment = ""; - const activated = new ActivatedRouteSnapshot(emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent, null); + const activated = new ActivatedRouteSnapshot(emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent, null, rootUrlSegment); return new RouterStateSnapshot(new TreeNode(activated, []), emptyQueryParams, fragment); } @@ -91,12 +92,17 @@ export class ActivatedRouteSnapshot { /** @internal **/ _routeConfig: Route; + /** @internal **/ + _lastUrlSegment: UrlSegment; + constructor(public urlSegments: UrlSegment[], public params: Params, public outlet: string, public component: Type | string, - routeConfig: Route) { + routeConfig: Route, + lastUrlSegment: UrlSegment) { this._routeConfig = routeConfig; + this._lastUrlSegment = lastUrlSegment; } } diff --git a/modules/@angular/router/test/create_url_tree.spec.ts b/modules/@angular/router/test/create_url_tree.spec.ts index a680e53c4b..c468ae3e7e 100644 --- a/modules/@angular/router/test/create_url_tree.spec.ts +++ b/modules/@angular/router/test/create_url_tree.spec.ts @@ -1,9 +1,8 @@ import {DefaultUrlSerializer} from '../src/url_serializer'; import {UrlTree, UrlSegment} from '../src/url_tree'; -import {ActivatedRoute} from '../src/router_state'; +import {ActivatedRoute, ActivatedRouteSnapshot} from '../src/router_state'; import {PRIMARY_OUTLET, Params} from '../src/shared'; import {createUrlTree} from '../src/create_url_tree'; -import {BehaviorSubject} from 'rxjs/BehaviorSubject'; describe('createUrlTree', () => { const serializer = new DefaultUrlSerializer(); @@ -175,6 +174,7 @@ function create(start: UrlSegment | null, tree: UrlTree, commands: any[], queryP if (!start) { expect(start).toBeDefined(); } - const a = new ActivatedRoute(new BehaviorSubject([start]), null, PRIMARY_OUTLET, "someComponent", null); + const s = new ActivatedRouteSnapshot([], null, PRIMARY_OUTLET, "someComponent", null, start); + const a = new ActivatedRoute(null, null, PRIMARY_OUTLET, "someComponent", s); return createUrlTree(a, tree, commands, queryParameters, fragment); } \ No newline at end of file diff --git a/modules/@angular/router/test/recognize.spec.ts b/modules/@angular/router/test/recognize.spec.ts index ed88ec4af1..f4a2b5fe4e 100644 --- a/modules/@angular/router/test/recognize.spec.ts +++ b/modules/@angular/router/test/recognize.spec.ts @@ -112,11 +112,31 @@ describe('recognize', () => { }); describe("index", () => { - it("should support index routes", () => { + it("should support root index routes", () => { recognize(RootComponent, [ {index: true, component: ComponentA} ], tree("")).forEach(s => { + checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); + }); + }); + + it("should support nested root index routes", () => { + recognize(RootComponent, [ + {index: true, component: ComponentA, children: [{index: true, component: ComponentB}]} + ], tree("")).forEach(s => { + checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); + checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); + }); + }); + + it("should support index routes", () => { + recognize(RootComponent, [ + {path: 'a', component: ComponentA, children: [ + {index: true, component: ComponentB} + ]} + ], tree("a")).forEach(s => { checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA); + checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); }); }); @@ -137,6 +157,15 @@ describe('recognize', () => { s.firstChild(s.firstChild(s.firstChild(s.root))), "c/10", {id: '10'}, ComponentC); }); }); + + it("should pass parameters to every nested index route (case with non-index route)", () => { + recognize(RootComponent, [ + {path: 'a', component: ComponentA, children: [{index: true, component: ComponentB}]} + ], tree("/a;a=1")).forEach(s => { + checkActivatedRoute(s.firstChild(s.root), "a", {a: '1'}, ComponentA); + checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {a: '1'}, ComponentB); + }); + }); }); describe("wildcards", () => { @@ -198,7 +227,7 @@ describe('recognize', () => { function checkActivatedRoute(actual: ActivatedRouteSnapshot | null, url: string, params: Params, cmp: Function, outlet: string = PRIMARY_OUTLET):void { if (actual === null) { - expect(actual).toBeDefined(); + expect(actual).not.toBeNull(); } else { expect(actual.urlSegments.map(s => s.path).join("/")).toEqual(url); expect(actual.params).toEqual(params); diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index 76a63c97e4..d26c394d0f 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -1,6 +1,7 @@ import {Component, Injector} from '@angular/core'; import { describe, + ddescribe, it, iit, xit, @@ -199,6 +200,26 @@ describe("Integration", () => { expect(user.recordedParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); }))); + it('should work when navigating to /', + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => { + router.resetConfig([ + { index: true, component: SimpleCmp }, + { path: '/user/:name', component: UserCmp } + ]); + + const fixture = tcb.createFakeAsync(RootCmp); + + router.navigateByUrl('/user/victor'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('user victor'); + + router.navigateByUrl('/'); + advance(fixture); + + expect(fixture.debugElement.nativeElement).toHaveText('simple'); + }))); + describe("router links", () => { it("should support string router links", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {