fix(router): traverse route config in depth-first order

Closes #17
This commit is contained in:
vsavkin 2016-06-06 10:15:23 -07:00
parent 793ac3f6b4
commit 9b356d9b86
2 changed files with 57 additions and 42 deletions

View File

@ -7,6 +7,8 @@ import { RouterConfig, Route } from './config';
import { Type } from '@angular/core'; import { Type } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
class CannotRecognize {}
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, url.root); const match = new MatchResult(rootComponentType, config, [url.root], {}, url._root.children, [], PRIMARY_OUTLET, null, url.root);
@ -17,8 +19,12 @@ export function recognize(rootComponentType: Type, config: RouterConfig, url: Ur
obs.complete(); obs.complete();
}); });
} catch(e) { } catch(e) {
if (e instanceof CannotRecognize) {
return new Observable<RouterStateSnapshot>(obs => obs.error(new Error("Cannot match any routes")));
} else {
return new Observable<RouterStateSnapshot>(obs => obs.error(e)); return new Observable<RouterStateSnapshot>(obs => obs.error(e));
} }
}
} }
function constructActivatedRoute(match: MatchResult): TreeNode<ActivatedRouteSnapshot>[] { function constructActivatedRoute(match: MatchResult): TreeNode<ActivatedRouteSnapshot>[] {
@ -49,12 +55,21 @@ function createActivatedRouteSnapshot(match: MatchResult): ActivatedRouteSnapsho
} }
function recognizeOne(config: Route[], url: TreeNode<UrlSegment>): TreeNode<ActivatedRouteSnapshot>[] { function recognizeOne(config: Route[], url: TreeNode<UrlSegment>): TreeNode<ActivatedRouteSnapshot>[] {
const m = match(config, url); const matches = match(config, url);
const primary = constructActivatedRoute(m); for(let match of matches) {
const secondary = recognizeMany(config, m.secondary); try {
const primary = constructActivatedRoute(match);
const secondary = recognizeMany(config, match.secondary);
const res = primary.concat(secondary); const res = primary.concat(secondary);
checkOutletNameUniqueness(res); checkOutletNameUniqueness(res);
return res; return res;
} catch (e) {
if (! (e instanceof CannotRecognize)) {
throw e;
}
}
}
throw new CannotRecognize();
} }
function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): TreeNode<ActivatedRouteSnapshot>[] { function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): TreeNode<ActivatedRouteSnapshot>[] {
@ -71,35 +86,29 @@ function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): T
return nodes; return nodes;
} }
function match(config: Route[], url: TreeNode<UrlSegment>): MatchResult { function match(config: Route[], url: TreeNode<UrlSegment>): MatchResult[] {
const m = matchNonIndex(config, url); const res = [];
if (m) return m; for (let r of config) {
if (r.index) {
const mIndex = matchIndex(config, [url], url.value); res.push(createIndexMatch(r, [url], url.value));
if (mIndex) return mIndex; } else {
const m = matchWithParts(r, url);
const availableRoutes = config.map(r => { if (m) res.push(m);
const outlet = !r.outlet ? '' : `${r.outlet}:`; }
return `'${outlet}${r.path}'`; }
}).join(", "); return res;
throw new Error(
`Cannot match any routes. Current segment: '${url.value}'. Available routes: [${availableRoutes}].`);
} }
function matchNonIndex(config: Route[], url: TreeNode<UrlSegment>): MatchResult | null { function createIndexMatch(r: Route, leftOverUrls:TreeNode<UrlSegment>[], lastUrlSegment:UrlSegment): MatchResult {
for (let r of config) { const outlet = r.outlet ? r.outlet : PRIMARY_OUTLET;
let m = matchWithParts(r, url); const children = r.children ? r.children : [];
if (m) return m; return new MatchResult(r.component, children, [], lastUrlSegment.parameters, leftOverUrls, [], outlet, r, lastUrlSegment);
}
return null;
} }
function matchIndex(config: Route[], leftOverUrls: TreeNode<UrlSegment>[], lastUrlSegment: 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; return createIndexMatch(r, leftOverUrls, lastUrlSegment);
const children = r.children ? r.children : [];
return new MatchResult(r.component, children, [], lastUrlSegment.parameters, leftOverUrls, [], outlet, r, lastUrlSegment);
} }
} }
return null; return null;

View File

@ -17,19 +17,6 @@ describe('recognize', () => {
}); });
}); });
it('should handle position args', () => {
recognize(RootComponent, [
{
path: 'a/:id', component: ComponentA, children: [
{ path: 'b/:id', component: ComponentB}
]
}
], tree("a/paramA/b/paramB")).forEach(s => {
checkActivatedRoute(s.root, "", {}, RootComponent);
checkActivatedRoute(s.firstChild(s.root), "a/paramA", {id: 'paramA'}, ComponentA);
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "b/paramB", {id: 'paramB'}, ComponentB);
});
});
it('should support secondary routes', () => { it('should support secondary routes', () => {
recognize(RootComponent, [ recognize(RootComponent, [
@ -44,6 +31,25 @@ describe('recognize', () => {
}); });
}); });
it('should match routes in the depth first order', () => {
recognize(RootComponent, [
{path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]},
{path: 'a/:id', component: ComponentC}
], tree("a/paramA")).forEach(s => {
checkActivatedRoute(s.root, "", {}, RootComponent);
checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA);
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "paramA", {id: 'paramA'}, ComponentB);
});
recognize(RootComponent, [
{path: 'a', component: ComponentA},
{path: 'a/:id', component: ComponentC}
], tree("a/paramA")).forEach(s => {
checkActivatedRoute(s.root, "", {}, RootComponent);
checkActivatedRoute(s.firstChild(s.root), "a/paramA", {id: 'paramA'}, ComponentC);
});
});
it('should use outlet name when matching secondary routes', () => { it('should use outlet name when matching secondary routes', () => {
recognize(RootComponent, [ recognize(RootComponent, [
{ path: 'a', component: ComponentA }, { path: 'a', component: ComponentA },