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