From 86f47273bc3751beaf5e19a18c869a46591b4c66 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Thu, 26 May 2016 16:51:44 -0700 Subject: [PATCH] feat(router): changes router config not to use names --- modules/@angular/router/src/config.ts | 1 - modules/@angular/router/src/recognize.ts | 95 ++++----- modules/@angular/router/src/router_state.ts | 21 +- modules/@angular/router/src/shared.ts | 10 + .../@angular/router/test/recognize.spec.ts | 180 ++++++++---------- 5 files changed, 147 insertions(+), 160 deletions(-) create mode 100644 modules/@angular/router/src/shared.ts diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index a0d0e53853..dbc8bd6b68 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -3,7 +3,6 @@ import { Type } from '@angular/core'; export type RouterConfig = Route[]; export interface Route { - name: string; index?: boolean; path?: string; component: Type | string; diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index ab707180c4..cf81b391e1 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -1,46 +1,48 @@ -import { UrlTree, UrlSegment, equalUrlSegments } from './url_tree'; -import { shallowEqual, flatten, first, merge } from './utils/collection'; +import { UrlTree, UrlSegment } from './url_tree'; +import { flatten, first, merge } from './utils/collection'; import { TreeNode, rootNode } from './utils/tree'; -import { RouterState, ActivatedRoute, Params, PRIMARY_OUTLET } from './router_state'; +import { RouterState, ActivatedRoute } from './router_state'; +import { Params, PRIMARY_OUTLET } from './shared'; import { RouterConfig, Route } from './config'; -import { ComponentResolver, ComponentFactory, Type } from '@angular/core'; +import { Type } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -export function recognize(componentResolver: ComponentResolver, config: RouterConfig, - url: UrlTree, existingState: RouterState): Promise { - const match = new MatchResult(existingState.root.component, config, [url.root], {}, rootNode(url).children, [], PRIMARY_OUTLET); - return constructActivatedRoute(componentResolver, match, rootNode(existingState)). - then(roots => { - (existingState.queryParams).next(url.queryParameters); - (existingState.fragment).next(url.fragment); - return new RouterState(roots[0], existingState.queryParams, existingState.fragment); +export function recognize(config: RouterConfig, url: UrlTree, existingState: RouterState): Observable { + try { + const match = new MatchResult(existingState.root.component, config, [url.root], {}, rootNode(url).children, [], PRIMARY_OUTLET); + (existingState.queryParams).next(url.queryParameters); + (existingState.fragment).next(url.fragment); + const roots = constructActivatedRoute(match, rootNode(existingState)); + const res = new RouterState(roots[0], existingState.queryParams, existingState.fragment); + return new Observable(obs => { + obs.next(res); + obs.complete(); }); + } catch(e) { + return new Observable(obs => obs.error(e)); + } } -function constructActivatedRoute(componentResolver: ComponentResolver, match: MatchResult, - existingRoute: TreeNode | null): Promise[]> { - //TODO: remove the cast after Angular is fixed - return componentResolver.resolveComponent(match.component).then(factory => { - const activatedRoute = createOrReuseRoute(match, factory, existingRoute); - const existingChildren = existingRoute ? existingRoute.children : []; +function constructActivatedRoute(match: MatchResult, existingRoute: TreeNode | null): TreeNode[] { + const activatedRoute = createOrReuseRoute(match, existingRoute); + const existingChildren = existingRoute ? existingRoute.children : []; - if (match.leftOverUrl.length > 0) { - return recognizeMany(componentResolver, match.children, match.leftOverUrl, existingChildren) - .then(checkOutletNameUniqueness) - .then(children => [new TreeNode(activatedRoute, children)]); - } else { - return Promise.resolve([new TreeNode(activatedRoute, [])]); - } - }); + if (match.leftOverUrl.length > 0) { + const children = recognizeMany(match.children, match.leftOverUrl, existingChildren); + checkOutletNameUniqueness(children); + return [new TreeNode(activatedRoute, children)]; + } else { + return [new TreeNode(activatedRoute, [])]; + } } -function recognizeMany(componentResolver: ComponentResolver, config: Route[], urls: TreeNode[], - existingRoutes: TreeNode[]): Promise[]> { - const recognized = urls.map(url => recognizeOne(componentResolver, config, url, existingRoutes)); - return Promise.all(recognized).then(flatten); +function recognizeMany(config: Route[], urls: TreeNode[], + existingRoutes: TreeNode[]): TreeNode[] { + return flatten(urls.map(url => recognizeOne(config, url, existingRoutes))); } -function createOrReuseRoute(match: MatchResult, factory: ComponentFactory, existing: TreeNode | null): ActivatedRoute { +function createOrReuseRoute(match: MatchResult, existing: TreeNode | null): ActivatedRoute { if (existing) { const v = existing.value; if (v.component === match.component && v.outlet === match.outlet) { @@ -49,26 +51,21 @@ function createOrReuseRoute(match: MatchResult, factory: ComponentFactory, return v; } } - return new ActivatedRoute(new BehaviorSubject(match.consumedUrlSegments), new BehaviorSubject(match.parameters), match.outlet, - factory.componentType, factory); + return new ActivatedRoute(new BehaviorSubject(match.consumedUrlSegments), new BehaviorSubject(match.parameters), match.outlet, match.component); } -function recognizeOne(componentResolver: ComponentResolver, config: Route[], - url: TreeNode, - existingRoutes: TreeNode[]): Promise[]> { - let m; - try { - m = match(config, url); - } catch (e) { - return Promise.reject(e); - } +function recognizeOne(config: Route[], url: TreeNode, + existingRoutes: TreeNode[]): TreeNode[] { + let m = match(config, url); const routesWithRightOutlet = existingRoutes.filter(r => r.value.outlet == m.outlet); const routeWithRightOutlet = routesWithRightOutlet.length > 0 ? routesWithRightOutlet[0] : null; - const primary = constructActivatedRoute(componentResolver, m, routeWithRightOutlet); - const secondary = recognizeMany(componentResolver, config, m.secondary, existingRoutes); - return Promise.all([primary, secondary]).then(flatten).then(checkOutletNameUniqueness); + const primary = constructActivatedRoute(m, routeWithRightOutlet); + const secondary = recognizeMany(config, m.secondary, existingRoutes); + const res = primary.concat(secondary); + checkOutletNameUniqueness(res); + return res; } function checkOutletNameUniqueness(nodes: TreeNode[]): TreeNode[] { @@ -92,7 +89,10 @@ function match(config: Route[], url: TreeNode): MatchResult { const mIndex = matchIndex(config, url); if (mIndex) return mIndex; - const availableRoutes = config.map(r => `'${r.path}'`).join(", "); + 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}].`); } @@ -118,6 +118,7 @@ function matchIndex(config: Route[], url: TreeNode): MatchResult | n function matchWithParts(route: Route, url: TreeNode): MatchResult | null { if (!route.path) return null; + if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== url.value.outlet) return null; const path = route.path.startsWith("/") ? route.path.substring(1) : route.path; if (path === "**") { @@ -185,4 +186,4 @@ class MatchResult { public secondary: TreeNode[], public outlet: string ) {} -} +} \ 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 93f19bb3d5..8c0c01b29e 100644 --- a/modules/@angular/router/src/router_state.ts +++ b/modules/@angular/router/src/router_state.ts @@ -1,19 +1,9 @@ import { Tree, TreeNode } from './utils/tree'; import { UrlSegment } from './url_tree'; +import { Params, PRIMARY_OUTLET } from './shared'; import { Observable } from 'rxjs/Observable'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { ComponentFactory, Type } from '@angular/core'; - -/** - * A collection of parameters. - */ -export type Params = { [key: string]: string }; - -/** - * Name of the primary outlet. - * @type {string} - */ -export const PRIMARY_OUTLET: string = "PRIMARY_OUTLET"; +import { Type } from '@angular/core'; /** * The state of the router at a particular moment in time. @@ -37,11 +27,11 @@ export class RouterState extends Tree { } export function createEmptyState(rootComponent: Type): RouterState { - const emptyUrl = new BehaviorSubject([new UrlSegment("", {})]); + const emptyUrl = new BehaviorSubject([new UrlSegment("", {}, PRIMARY_OUTLET)]); const emptyParams = new BehaviorSubject({}); const emptyQueryParams = new BehaviorSubject({}); const fragment = new BehaviorSubject(""); - const activated = new ActivatedRoute(emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent, null); + const activated = new ActivatedRoute(emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent); return new RouterState(new TreeNode(activated, []), emptyQueryParams, fragment); } @@ -62,6 +52,5 @@ export class ActivatedRoute { constructor(public urlSegments: Observable, public params: Observable, public outlet: string, - public component: Type, - public factory: ComponentFactory) {} + public component: Type | string) {} } \ No newline at end of file diff --git a/modules/@angular/router/src/shared.ts b/modules/@angular/router/src/shared.ts new file mode 100644 index 0000000000..c97632bcd6 --- /dev/null +++ b/modules/@angular/router/src/shared.ts @@ -0,0 +1,10 @@ +/** + * Name of the primary outlet. + * @type {string} + */ +export const PRIMARY_OUTLET: string = "PRIMARY_OUTLET"; + +/** + * A collection of parameters. + */ +export type Params = { [key: string]: string }; diff --git a/modules/@angular/router/test/recognize.spec.ts b/modules/@angular/router/test/recognize.spec.ts index c6b854f601..f08aad063f 100644 --- a/modules/@angular/router/test/recognize.spec.ts +++ b/modules/@angular/router/test/recognize.spec.ts @@ -1,228 +1,216 @@ import {DefaultUrlSerializer} from '../src/url_serializer'; import {UrlTree} from '../src/url_tree'; -import {createEmptyState, Params, ActivatedRoute, PRIMARY_OUTLET} from '../src/router_state'; +import {Params, PRIMARY_OUTLET} from '../src/shared'; +import {createEmptyState, ActivatedRoute} from '../src/router_state'; import {recognize} from '../src/recognize'; describe('recognize', () => { const empty = () => createEmptyState(RootComponent); - const fakeComponentResolver = { - resolveComponent(componentType:any):Promise { return Promise.resolve({componentType}); }, - clearCache() {} - }; it('should work', (done) => { - recognize(fakeComponentResolver, [ + recognize([ { - name: 'a', path: 'a', component: ComponentA } - ], tree("a"), empty()).then(s => { + ], tree("a"), empty()).forEach(s => { checkActivatedRoute(s.root, "", {}, RootComponent); checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA); done(); }); }); - it('should handle position args', (done) => { - recognize(fakeComponentResolver, [ + it('should handle position args', () => { + recognize([ { - name: 'a', path: 'a/:id', component: ComponentA, children: [ - { name: 'b', path: 'b/:id', component: ComponentB} + { path: 'b/:id', component: ComponentB} ] } - ], tree("a/paramA/b/paramB"), empty()).then(s => { + ], tree("a/paramA/b/paramB"), empty()).forEach(s => { checkActivatedRoute(s.root, "", {}, RootComponent); checkActivatedRoute(s.firstChild(s.root), "a/paramA", {id: 'paramA'}, ComponentA); checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "b/paramB", {id: 'paramB'}, ComponentB); - done(); }); }); - it('should reuse activated routes', (done) => { - const config = [{name: 'a', path: 'a/:id', component: ComponentA}]; - recognize(fakeComponentResolver, config, tree("a/paramA"), empty()).then(s => { + it('should reuse activated routes', () => { + const config = [{path: 'a/:id', component: ComponentA}]; + recognize(config, tree("a/paramA"), empty()).forEach(s => { const n1 = s.firstChild(s.root); const recorded = []; n1!.params.forEach(r => recorded.push(r)); - recognize(fakeComponentResolver, config, tree("a/paramB"), s).then(s2 => { + recognize(config, tree("a/paramB"), s).forEach(s2 => { const n2 = s2.firstChild(s2.root); expect(n1).toBe(n2); expect(recorded).toEqual([{id: 'paramA'}, {id: 'paramB'}]); - done(); }); }); }); - it('should support secondary routes', (done) => { - recognize(fakeComponentResolver, [ - { name: 'a', path: 'a', component: ComponentA }, - { name: 'b', path: 'b', component: ComponentB, outlet: 'left' }, - { name: 'c', path: 'c', component: ComponentC, outlet: 'right' } - ], tree("a(b//c)"), empty()).then(s => { + it('should support secondary routes', () => { + recognize([ + { path: 'a', component: ComponentA }, + { path: 'b', component: ComponentB, outlet: 'left' }, + { path: 'c', component: ComponentC, outlet: 'right' } + ], tree("a(left:b//right:c)"), empty()).forEach(s => { const c = s.children(s.root); checkActivatedRoute(c[0], "a", {}, ComponentA); checkActivatedRoute(c[1], "b", {}, ComponentB, 'left'); checkActivatedRoute(c[2], "c", {}, ComponentC, 'right'); - done(); }); }); - it('should handle nested secondary routes', (done) => { - recognize(fakeComponentResolver, [ - { name: 'a', path: 'a', component: ComponentA }, - { name: 'b', path: 'b', component: ComponentB, outlet: 'left' }, - { name: 'c', path: 'c', component: ComponentC, outlet: 'right' } - ], tree("a(b(c))"), empty()).then(s => { + it('should use outlet name when matching secondary routes', () => { + recognize([ + { path: 'a', component: ComponentA }, + { path: 'b', component: ComponentB, outlet: 'left' }, + { path: 'b', component: ComponentC, outlet: 'right' } + ], tree("a(right:b)"), empty()).forEach(s => { + const c = s.children(s.root); + checkActivatedRoute(c[0], "a", {}, ComponentA); + checkActivatedRoute(c[1], "b", {}, ComponentC, 'right'); + }); + }); + + it('should handle nested secondary routes', () => { + recognize([ + { path: 'a', component: ComponentA }, + { path: 'b', component: ComponentB, outlet: 'left' }, + { path: 'c', component: ComponentC, outlet: 'right' } + ], tree("a(left:b(right:c))"), empty()).forEach(s => { const c = s.children(s.root); checkActivatedRoute(c[0], "a", {}, ComponentA); checkActivatedRoute(c[1], "b", {}, ComponentB, 'left'); checkActivatedRoute(c[2], "c", {}, ComponentC, 'right'); - done(); }); }); - it('should handle non top-level secondary routes', (done) => { - recognize(fakeComponentResolver, [ - { name: 'a', path: 'a', component: ComponentA, children: [ - { name: 'b', path: 'b', component: ComponentB }, - { name: 'c', path: 'c', component: ComponentC, outlet: 'left' } + it('should handle non top-level secondary routes', () => { + recognize([ + { path: 'a', component: ComponentA, children: [ + { path: 'b', component: ComponentB }, + { path: 'c', component: ComponentC, outlet: 'left' } ] }, - ], tree("a/b(c))"), empty()).then(s => { + ], tree("a/b(left:c))"), empty()).forEach(s => { const c = s.children(s.firstChild(s.root)); checkActivatedRoute(c[0], "b", {}, ComponentB, PRIMARY_OUTLET); checkActivatedRoute(c[1], "c", {}, ComponentC, 'left'); - done(); }); }); - it('should support matrix parameters', (done) => { - recognize(fakeComponentResolver, [ + it('should support matrix parameters', () => { + recognize([ { - name: 'a', path: 'a', component: ComponentA, children: [ - { name: 'b', path: 'b', component: ComponentB }, - { name: 'c', path: 'c', component: ComponentC, outlet: 'left' } + { path: 'b', component: ComponentB }, + { path: 'c', component: ComponentC, outlet: 'left' } ] } - ], tree("a;a1=11;a2=22/b;b1=111;b2=222(c;c1=1111;c2=2222)"), empty()).then(s => { + ], tree("a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)"), empty()).forEach(s => { checkActivatedRoute(s.firstChild(s.root), "a", {a1: '11', a2: '22'}, ComponentA); const c = s.children(s.firstChild(s.root)); checkActivatedRoute(c[0], "b", {b1: '111', b2: '222'}, ComponentB); checkActivatedRoute(c[1], "c", {c1: '1111', c2: '2222'}, ComponentC, 'left'); - done(); }); }); describe("index", () => { - it("should support index routes", (done) => { - recognize(fakeComponentResolver, [ - { - name: 'a', index: true, component: ComponentA - } - ], tree(""), empty()).then(s => { + it("should support index routes", () => { + recognize([ + {index: true, component: ComponentA} + ], tree(""), empty()).forEach(s => { checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA); - done(); }); }); - it("should support index routes with children", (done) => { - recognize(fakeComponentResolver, [ + it("should support index routes with children", () => { + recognize([ { - name: 'a', index: true, component: ComponentA, children: [ - { name: 'b', index: true, component: ComponentB, children: [ - {name: 'c', path: 'c/:id', component: ComponentC} + index: true, component: ComponentA, children: [ + { index: true, component: ComponentB, children: [ + {path: 'c/:id', component: ComponentC} ] } ] } - ], tree("c/10"), empty()).then(s => { + ], tree("c/10"), empty()).forEach(s => { checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); checkActivatedRoute( s.firstChild(s.firstChild(s.firstChild(s.root))), "c/10", {id: '10'}, ComponentC); - done(); }); }); }); describe("wildcards", () => { - it("should support simple wildcards", (done) => { - recognize(fakeComponentResolver, [ - { - name: 'a', path: '**', component: ComponentA - } - ], tree("a/b/c/d;a1=11"), empty()).then(s => { + it("should support simple wildcards", () => { + recognize([ + {path: '**', component: ComponentA} + ], tree("a/b/c/d;a1=11"), empty()).forEach(s => { checkActivatedRoute(s.firstChild(s.root), "a/b/c/d", {a1:'11'}, ComponentA); - done(); }); }); }); describe("query parameters", () => { - it("should support query params", (done) => { - const config = [{name: 'a', path: 'a', component: ComponentA}]; - recognize(fakeComponentResolver, config, tree("a?q=11"), empty()).then(s => { + it("should support query params", () => { + const config = [{path: 'a', component: ComponentA}]; + recognize(config, tree("a?q=11"), empty()).forEach(s => { const q1 = s.queryParams; const recorded = []; q1!.forEach(r => recorded.push(r)); - recognize(fakeComponentResolver, config, tree("a?q=22"), s).then(s2 => { + recognize(config, tree("a?q=22"), s).forEach(s2 => { const q2 = s2.queryParams; expect(q1).toBe(q2); expect(recorded).toEqual([{q: '11'}, {q: '22'}]); - done(); }); }); }); }); describe("fragment", () => { - it("should support fragment", (done) => { - const config = [{name: 'a', path: 'a', component: ComponentA}]; - recognize(fakeComponentResolver, config, tree("a#f1"), empty()).then(s => { + it("should support fragment", () => { + const config = [{path: 'a', component: ComponentA}]; + recognize(config, tree("a#f1"), empty()).forEach(s => { const f1 = s.fragment; const recorded = []; f1!.forEach(r => recorded.push(r)); - recognize(fakeComponentResolver, config, tree("a#f2"), s).then(s2 => { + recognize(config, tree("a#f2"), s).forEach(s2 => { const f2 = s2.fragment; expect(f1).toBe(f2); expect(recorded).toEqual(["f1", "f2"]); - done(); }); }); }); }); describe("error handling", () => { - it('should error when two routes with the same outlet name got matched', (done) => { - recognize(fakeComponentResolver, [ - { name: 'a', path: 'a', component: ComponentA }, - { name: 'b', path: 'b', component: ComponentB, outlet: 'aux' }, - { name: 'c', path: 'c', component: ComponentC, outlet: 'aux' } - ], tree("a(b//c)"), empty()).catch(s => { - expect(s.toString()).toContain("Two segments cannot have the same outlet name: 'b' and 'c'."); - done(); + it('should error when two routes with the same outlet name got matched', () => { + recognize([ + { path: 'a', component: ComponentA }, + { path: 'b', component: ComponentB, outlet: 'aux' }, + { path: 'c', component: ComponentC, outlet: 'aux' } + ], tree("a(aux:b//aux:c)"), empty()).subscribe(null, s => { + expect(s.toString()).toContain("Two segments cannot have the same outlet name: 'aux:b' and 'aux:c'."); }); }); - it("should error when no matching routes", (done) => { - recognize(fakeComponentResolver, [ - { name: 'a', path: 'a', component: ComponentA } - ], tree("invalid"), empty()).catch(s => { + it("should error when no matching routes", () => { + recognize([ + { path: 'a', component: ComponentA } + ], tree("invalid"), empty()).subscribe(null, s => { expect(s.toString()).toContain("Cannot match any routes"); - done(); }); }); - it("should error when no matching routes (too short)", (done) => { - recognize(fakeComponentResolver, [ - { name: 'a', path: 'a/:id', component: ComponentA } - ], tree("a"), empty()).catch(s => { + it("should error when no matching routes (too short)", () => { + recognize([ + { path: 'a/:id', component: ComponentA } + ], tree("a"), empty()).subscribe(null, s => { expect(s.toString()).toContain("Cannot match any routes"); - done(); }); }); });