fix(router): fix index routes

This commit is contained in:
vsavkin 2016-06-02 11:30:38 -07:00
parent 243612e36d
commit d95f0fd83d
6 changed files with 89 additions and 30 deletions

View File

@ -96,8 +96,7 @@ function findStartingNode(normalizedChange: NormalizedNavigationCommands, urlTre
} }
function findUrlSegment(route: ActivatedRoute, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment { function findUrlSegment(route: ActivatedRoute, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment {
const segments = (<any>route.urlSegments).value; const urlSegment = route.snapshot._lastUrlSegment;
const urlSegment = segments[segments.length - 1];
const path = urlTree.pathFromRoot(urlSegment); const path = urlTree.pathFromRoot(urlSegment);
if (path.length <= numberOfDoubleDots) { if (path.length <= numberOfDoubleDots) {
throw new Error("Invalid number of '../'"); throw new Error("Invalid number of '../'");

View File

@ -9,7 +9,7 @@ import { Observable } from 'rxjs/Observable';
export function recognize(rootComponentType: Type, config: RouterConfig, url: UrlTree): Observable<RouterStateSnapshot> { export function recognize(rootComponentType: Type, config: RouterConfig, url: UrlTree): Observable<RouterStateSnapshot> {
try { 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 roots = constructActivatedRoute(match);
const res = new RouterStateSnapshot(roots[0], url.queryParameters, url.fragment); const res = new RouterStateSnapshot(roots[0], url.queryParameters, url.fragment);
return new Observable<RouterStateSnapshot>(obs => { return new Observable<RouterStateSnapshot>(obs => {
@ -23,18 +23,21 @@ export function recognize(rootComponentType: Type, config: RouterConfig, url: Ur
function constructActivatedRoute(match: MatchResult): TreeNode<ActivatedRouteSnapshot>[] { function constructActivatedRoute(match: MatchResult): TreeNode<ActivatedRouteSnapshot>[] {
const activatedRoute = createActivatedRouteSnapshot(match); const activatedRoute = createActivatedRouteSnapshot(match);
if (match.leftOverUrl.length > 0) { const children = match.leftOverUrl.length > 0 ?
const children = recognizeMany(match.children, match.leftOverUrl); recognizeMany(match.children, match.leftOverUrl) : recognizeLeftOvers(match.children, match.lastUrlSegment);
checkOutletNameUniqueness(children); checkOutletNameUniqueness(children);
children.sort((a, b) => { children.sort((a, b) => {
if (a.value.outlet === PRIMARY_OUTLET) return -1; if (a.value.outlet === PRIMARY_OUTLET) return -1;
if (b.value.outlet === PRIMARY_OUTLET) return 1; if (b.value.outlet === PRIMARY_OUTLET) return 1;
return a.value.outlet.localeCompare(b.value.outlet) return a.value.outlet.localeCompare(b.value.outlet)
}); });
return [new TreeNode<ActivatedRouteSnapshot>(activatedRoute, children)]; return [new TreeNode<ActivatedRouteSnapshot>(activatedRoute, children)];
} else { }
return [new TreeNode<ActivatedRouteSnapshot>(activatedRoute, [])];
} function recognizeLeftOvers(config: Route[], lastUrlSegment: UrlSegment): TreeNode<ActivatedRouteSnapshot>[] {
if (!config) return [];
const mIndex = matchIndex(config, [], lastUrlSegment);
return mIndex ? constructActivatedRoute(mIndex) : [];
} }
function recognizeMany(config: Route[], urls: TreeNode<UrlSegment>[]): TreeNode<ActivatedRouteSnapshot>[] { function recognizeMany(config: Route[], urls: TreeNode<UrlSegment>[]): TreeNode<ActivatedRouteSnapshot>[] {
@ -42,7 +45,7 @@ function recognizeMany(config: Route[], urls: TreeNode<UrlSegment>[]): TreeNode<
} }
function createActivatedRouteSnapshot(match: MatchResult): ActivatedRouteSnapshot { 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<UrlSegment>): TreeNode<ActivatedRouteSnapshot>[] { function recognizeOne(config: Route[], url: TreeNode<UrlSegment>): TreeNode<ActivatedRouteSnapshot>[] {
@ -72,7 +75,7 @@ function match(config: Route[], url: TreeNode<UrlSegment>): MatchResult {
const m = matchNonIndex(config, url); const m = matchNonIndex(config, url);
if (m) return m; if (m) return m;
const mIndex = matchIndex(config, url); const mIndex = matchIndex(config, [url], url.value);
if (mIndex) return mIndex; if (mIndex) return mIndex;
const availableRoutes = config.map(r => { const availableRoutes = config.map(r => {
@ -91,12 +94,12 @@ function matchNonIndex(config: Route[], url: TreeNode<UrlSegment>): MatchResult
return null; return null;
} }
function matchIndex(config: Route[], url: TreeNode<UrlSegment>): MatchResult | null { function matchIndex(config: Route[], leftOverUrls: TreeNode<UrlSegment>[], lastUrlSegment: UrlSegment): MatchResult | null {
for (let r of config) { for (let r of config) {
if (r.index) { if (r.index) {
const outlet = r.outlet ? r.outlet : PRIMARY_OUTLET; const outlet = r.outlet ? r.outlet : PRIMARY_OUTLET;
const children = r.children ? r.children : []; 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; return null;
@ -115,7 +118,7 @@ function matchWithParts(route: Route, url: TreeNode<UrlSegment>): MatchResult |
u = first(u.children); u = first(u.children);
} }
const last = consumedUrl[consumedUrl.length - 1]; 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("/"); const parts = path.split("/");
@ -160,7 +163,7 @@ function matchWithParts(route: Route, url: TreeNode<UrlSegment>): MatchResult |
const outlet = route.outlet ? route.outlet : PRIMARY_OUTLET; const outlet = route.outlet ? route.outlet : PRIMARY_OUTLET;
return new MatchResult(route.component, children, consumedUrlSegments, parameters, lastSegment.children, return new MatchResult(route.component, children, consumedUrlSegments, parameters, lastSegment.children,
secondarySubtrees, outlet, route); secondarySubtrees, outlet, route, lastSegment.value);
} }
class MatchResult { class MatchResult {
@ -171,6 +174,7 @@ class MatchResult {
public leftOverUrl: TreeNode<UrlSegment>[], public leftOverUrl: TreeNode<UrlSegment>[],
public secondary: TreeNode<UrlSegment>[], public secondary: TreeNode<UrlSegment>[],
public outlet: string, public outlet: string,
public route: Route public route: Route,
public lastUrlSegment: UrlSegment
) {} ) {}
} }

View File

@ -38,11 +38,12 @@ export function createEmptyState(rootComponent: Type): RouterState {
} }
function createEmptyStateSnapshot(rootComponent: Type): RouterStateSnapshot { function createEmptyStateSnapshot(rootComponent: Type): RouterStateSnapshot {
const emptyUrl = [new UrlSegment("", {}, PRIMARY_OUTLET)]; const rootUrlSegment = new UrlSegment("", {}, PRIMARY_OUTLET);
const emptyUrl = [rootUrlSegment];
const emptyParams = {}; const emptyParams = {};
const emptyQueryParams = {}; const emptyQueryParams = {};
const fragment = ""; 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<ActivatedRouteSnapshot>(activated, []), emptyQueryParams, fragment); return new RouterStateSnapshot(new TreeNode<ActivatedRouteSnapshot>(activated, []), emptyQueryParams, fragment);
} }
@ -91,12 +92,17 @@ export class ActivatedRouteSnapshot {
/** @internal **/ /** @internal **/
_routeConfig: Route; _routeConfig: Route;
/** @internal **/
_lastUrlSegment: UrlSegment;
constructor(public urlSegments: UrlSegment[], constructor(public urlSegments: UrlSegment[],
public params: Params, public params: Params,
public outlet: string, public outlet: string,
public component: Type | string, public component: Type | string,
routeConfig: Route) { routeConfig: Route,
lastUrlSegment: UrlSegment) {
this._routeConfig = routeConfig; this._routeConfig = routeConfig;
this._lastUrlSegment = lastUrlSegment;
} }
} }

View File

@ -1,9 +1,8 @@
import {DefaultUrlSerializer} from '../src/url_serializer'; import {DefaultUrlSerializer} from '../src/url_serializer';
import {UrlTree, UrlSegment} from '../src/url_tree'; 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 {PRIMARY_OUTLET, Params} from '../src/shared';
import {createUrlTree} from '../src/create_url_tree'; import {createUrlTree} from '../src/create_url_tree';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
describe('createUrlTree', () => { describe('createUrlTree', () => {
const serializer = new DefaultUrlSerializer(); const serializer = new DefaultUrlSerializer();
@ -175,6 +174,7 @@ function create(start: UrlSegment | null, tree: UrlTree, commands: any[], queryP
if (!start) { if (!start) {
expect(start).toBeDefined(); expect(start).toBeDefined();
} }
const a = new ActivatedRoute(new BehaviorSubject([start]), <any>null, PRIMARY_OUTLET, "someComponent", null); const s = new ActivatedRouteSnapshot([], <any>null, PRIMARY_OUTLET, "someComponent", null, start);
const a = new ActivatedRoute(<any>null, <any>null, PRIMARY_OUTLET, "someComponent", s);
return createUrlTree(a, tree, commands, queryParameters, fragment); return createUrlTree(a, tree, commands, queryParameters, fragment);
} }

View File

@ -112,11 +112,31 @@ describe('recognize', () => {
}); });
describe("index", () => { describe("index", () => {
it("should support index routes", () => { it("should support root index routes", () => {
recognize(RootComponent, [ recognize(RootComponent, [
{index: true, component: ComponentA} {index: true, component: ComponentA}
], tree("")).forEach(s => { ], 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(<any>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.root), "a", {}, ComponentA);
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
}); });
}); });
@ -137,6 +157,15 @@ describe('recognize', () => {
s.firstChild(<any>s.firstChild(<any>s.firstChild(s.root))), "c/10", {id: '10'}, ComponentC); s.firstChild(<any>s.firstChild(<any>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(<any>s.firstChild(s.root)), "", {a: '1'}, ComponentB);
});
});
}); });
describe("wildcards", () => { describe("wildcards", () => {
@ -198,7 +227,7 @@ describe('recognize', () => {
function checkActivatedRoute(actual: ActivatedRouteSnapshot | null, url: string, params: Params, cmp: Function, outlet: string = PRIMARY_OUTLET):void { function checkActivatedRoute(actual: ActivatedRouteSnapshot | null, url: string, params: Params, cmp: Function, outlet: string = PRIMARY_OUTLET):void {
if (actual === null) { if (actual === null) {
expect(actual).toBeDefined(); expect(actual).not.toBeNull();
} else { } else {
expect(actual.urlSegments.map(s => s.path).join("/")).toEqual(url); expect(actual.urlSegments.map(s => s.path).join("/")).toEqual(url);
expect(actual.params).toEqual(params); expect(actual.params).toEqual(params);

View File

@ -1,6 +1,7 @@
import {Component, Injector} from '@angular/core'; import {Component, Injector} from '@angular/core';
import { import {
describe, describe,
ddescribe,
it, it,
iit, iit,
xit, xit,
@ -199,6 +200,26 @@ describe("Integration", () => {
expect(user.recordedParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); 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", () => { describe("router links", () => {
it("should support string router links", it("should support string router links",
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {