diff --git a/modules/angular2/src/router/route_config.js b/modules/angular2/src/router/route_config.js index 36451d39d8..73b4dc0d24 100644 --- a/modules/angular2/src/router/route_config.js +++ b/modules/angular2/src/router/route_config.js @@ -1,18 +1,13 @@ import {CONST} from 'angular2/src/facade/lang'; +import {List} from 'angular2/src/facade/collection'; /** * You use the RouteConfig annotation to ... */ export class RouteConfig { - path:string; - redirectTo:string; - component:any; - //TODO: "alias," or "as" - + configs; @CONST() - constructor({path, component, redirectTo}:{path:string, component:any, redirectTo:string} = {}) { - this.path = path; - this.component = component; - this.redirectTo = redirectTo; + constructor(configs:List) { + this.configs = configs; } } diff --git a/modules/angular2/src/router/route_recognizer.js b/modules/angular2/src/router/route_recognizer.js index 3737c383e1..12171b5b9a 100644 --- a/modules/angular2/src/router/route_recognizer.js +++ b/modules/angular2/src/router/route_recognizer.js @@ -41,11 +41,16 @@ export class RouteRecognizer { var solution = StringMapWrapper.create(); StringMapWrapper.set(solution, 'handler', pathRecognizer.handler); StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url)); - StringMapWrapper.set(solution, 'matchedUrl', match[0]); - - var unmatchedUrl = StringWrapper.substring(url, match[0].length); - StringMapWrapper.set(solution, 'unmatchedUrl', unmatchedUrl); + //TODO(btford): determine a good generic way to deal with terminal matches + if (url === '/') { + StringMapWrapper.set(solution, 'matchedUrl', '/'); + StringMapWrapper.set(solution, 'unmatchedUrl', ''); + } else { + StringMapWrapper.set(solution, 'matchedUrl', match[0]); + var unmatchedUrl = StringWrapper.substring(url, match[0].length); + StringMapWrapper.set(solution, 'unmatchedUrl', unmatchedUrl); + } ListWrapper.push(solutions, solution); } }); diff --git a/modules/angular2/src/router/route_registry.js b/modules/angular2/src/router/route_registry.js index e5e78ea0e1..b56e90c962 100644 --- a/modules/angular2/src/router/route_registry.js +++ b/modules/angular2/src/router/route_registry.js @@ -1,10 +1,12 @@ import {RouteRecognizer} from './route_recognizer'; import {Instruction, noopInstruction} from './instruction'; import {List, ListWrapper, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; -import {isPresent, isBlank, isType, StringWrapper} from 'angular2/src/facade/lang'; +import {isPresent, isBlank, isType, StringWrapper, CONST} from 'angular2/src/facade/lang'; import {RouteConfig} from './route_config'; import {reflector} from 'angular2/src/reflection/reflection'; +export const rootHostComponent = 'ROOT_HOST'; + export class RouteRegistry { _rules:Map; @@ -12,10 +14,7 @@ export class RouteRegistry { this._rules = MapWrapper.create(); } - config(parentComponent, path:string, component:any, alias:string = null) { - if (parentComponent === 'app') { - parentComponent = '/'; - } + config(parentComponent, config) { var recognizer:RouteRecognizer; if (MapWrapper.contains(this._rules, parentComponent)) { @@ -25,16 +24,14 @@ export class RouteRegistry { MapWrapper.set(this._rules, parentComponent, recognizer); } - this._configFromComponent(component); + config = normalizeConfig(config); - //TODO: support sibling components - var components = StringMapWrapper.create(); - StringMapWrapper.set(components, 'default', component); + var components = StringMapWrapper.get(config, 'components'); + StringMapWrapper.forEach(components, (component, _) => { + this._configFromComponent(component); + }); - var handler = StringMapWrapper.create(); - StringMapWrapper.set(handler, 'components', components); - - recognizer.addConfig(path, handler, alias); + recognizer.addConfig(config['path'], config, config['alias']); } _configFromComponent(component) { @@ -53,7 +50,9 @@ export class RouteRegistry { var annotation = annotations[i]; if (annotation instanceof RouteConfig) { - this.config(component, annotation.path, annotation.component); + ListWrapper.forEach(annotation.configs, (config) => { + this.config(component, config); + }) } } } @@ -62,7 +61,7 @@ export class RouteRegistry { // TODO: make recognized context a class // TODO: change parentComponent into parentContext - recognize(url:string, parentComponent = '/') { + recognize(url:string, parentComponent = rootHostComponent) { var componentRecognizer = MapWrapper.get(this._rules, parentComponent); if (isBlank(componentRecognizer)) { return null; @@ -106,7 +105,7 @@ export class RouteRegistry { generate(name:string, params:any) { //TODO: implement for hierarchical routes - var componentRecognizer = MapWrapper.get(this._rules, '/'); + var componentRecognizer = MapWrapper.get(this._rules, rootHostComponent); if (isPresent(componentRecognizer)) { return componentRecognizer.generate(name, params); } @@ -127,3 +126,29 @@ function handlerToLeafInstructions(context, parentComponent) { matchedUrl: context['matchedUrl'] }); } + +// given: +// { component: Foo } +// mutates the config to: +// { components: { default: Foo } } +function normalizeConfig(config:StringMap) { + if (StringMapWrapper.contains(config, 'component')) { + var component = StringMapWrapper.get(config, 'component'); + var components = StringMapWrapper.create(); + StringMapWrapper.set(components, 'default', component); + + var newConfig = StringMapWrapper.create(); + StringMapWrapper.set(newConfig, 'components', components); + + StringMapWrapper.forEach(config, (value, key) => { + if (!StringWrapper.equals(key, 'component') && !StringWrapper.equals(key, 'components')) { + StringMapWrapper.set(newConfig, key, value); + } + }); + + return newConfig; + } else if (!StringMapWrapper.contains(config, 'components')) { + throw new Error('Config does not include a "component" or "components" key.'); + } + return config; +} diff --git a/modules/angular2/src/router/router.js b/modules/angular2/src/router/router.js index 006493e03a..abdebd6ad0 100644 --- a/modules/angular2/src/router/router.js +++ b/modules/angular2/src/router/router.js @@ -2,7 +2,7 @@ import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2 import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection'; import {isBlank} from 'angular2/src/facade/lang'; -import {RouteRegistry} from './route_registry'; +import {RouteRegistry, rootHostComponent} from './route_registry'; import {Pipeline} from './pipeline'; import {Instruction} from './instruction'; import {RouterOutlet} from './router_outlet'; @@ -18,7 +18,7 @@ import {Location} from './location'; * @exportedAs angular2/router */ export class Router { - name; + hostComponent:any; parent:Router; navigating:boolean; lastNavigationAttempt: string; @@ -30,9 +30,9 @@ export class Router { _children:Map; _subject:EventEmitter; _location:Location; - - constructor(registry:RouteRegistry, pipeline:Pipeline, location:Location, parent:Router = null, name = '/') { - this.name = name; + + constructor(registry:RouteRegistry, pipeline:Pipeline, location:Location, parent:Router, hostComponent) { + this.hostComponent = hostComponent; this.navigating = false; this.parent = parent; this.previousUrl = null; @@ -42,8 +42,7 @@ export class Router { this._registry = registry; this._pipeline = pipeline; this._subject = new EventEmitter(); - this._location.subscribe((url) => this.navigate(url)); - this.navigate(location.path()); + //this._location.subscribe((url) => this.navigate(url)); } @@ -73,11 +72,30 @@ export class Router { * # Usage * * ``` - * router.config('/', SomeCmp); + * router.config({ 'path': '/', 'component': IndexCmp}); * ``` + * + * Or: + * + * ``` + * router.config([ + * { 'path': '/', 'component': IndexComp }, + * { 'path': '/user/:id', 'component': UserComp }, + * ]); + * ``` + * */ - config(path:string, component, alias:string=null) { - this._registry.config(this.name, path, component, alias); + config(config:any) { + + //TODO: use correct check + if (config instanceof List) { + path.forEach((configObject) => { + // TODO: this is a hack + this._registry.config(this.hostComponent, configObject); + }) + } else { + this._registry.config(this.hostComponent, config); + } return this.renavigate(); } @@ -184,13 +202,14 @@ export class Router { export class RootRouter extends Router { constructor(pipeline:Pipeline, location:Location) { - super(new RouteRegistry(), pipeline, location, null, '/'); + super(new RouteRegistry(), pipeline, location, null, rootHostComponent); + this.navigate(location.path()); } } class ChildRouter extends Router { - constructor(parent, name) { - super(parent._registry, parent._pipeline, parent._location, parent, name); + constructor(parent:Router, hostComponent) { + super(parent._registry, parent._pipeline, parent._location, parent, hostComponent); this.parent = parent; } } diff --git a/modules/angular2/test/router/outlet_spec.js b/modules/angular2/test/router/outlet_spec.js index da16d0d64d..6c8ab5425a 100644 --- a/modules/angular2/test/router/outlet_spec.js +++ b/modules/angular2/test/router/outlet_spec.js @@ -53,7 +53,7 @@ export function main() { it('should work in a simple case', inject([AsyncTestCompleter], (async) => { compile() - .then((_) => router.config('/test', HelloCmp)) + .then((_) => router.config({'path': '/test', 'component': HelloCmp})) .then((_) => router.navigate('/test')) .then((_) => { view.detectChanges(); @@ -65,7 +65,7 @@ export function main() { it('should navigate between components with different parameters', inject([AsyncTestCompleter], (async) => { compile() - .then((_) => router.config('/user/:name', UserCmp)) + .then((_) => router.config({'path': '/user/:name', 'component': UserCmp})) .then((_) => router.navigate('/user/brian')) .then((_) => { view.detectChanges(); @@ -82,7 +82,7 @@ export function main() { it('should work with child routers', inject([AsyncTestCompleter], (async) => { compile('outer { }') - .then((_) => router.config('/a', ParentCmp)) + .then((_) => router.config({'path': '/a', 'component': ParentCmp})) .then((_) => router.navigate('/a/b')) .then((_) => { view.detectChanges(); @@ -95,7 +95,7 @@ export function main() { it('should generate link hrefs', inject([AsyncTestCompleter], (async) => { ctx.name = 'brian'; compile('{{name}}') - .then((_) => router.config('/user/:name', UserCmp, 'user')) + .then((_) => router.config({'path': '/user/:name', 'component': UserCmp, 'alias': 'user'})) .then((_) => router.navigate('/a/b')) .then((_) => { view.detectChanges(); @@ -144,10 +144,10 @@ class UserCmp { template: "inner { }", directives: [RouterOutlet] }) -@RouteConfig({ +@RouteConfig([{ path: '/b', component: HelloCmp -}) +}]) class ParentCmp { constructor() {} } diff --git a/modules/angular2/test/router/route_registry_spec.js b/modules/angular2/test/router/route_registry_spec.js index 773dad65a0..d6f0178382 100644 --- a/modules/angular2/test/router/route_registry_spec.js +++ b/modules/angular2/test/router/route_registry_spec.js @@ -6,36 +6,45 @@ import { inject, beforeEach, SpyObject} from 'angular2/test_lib'; -import {RouteRegistry} from 'angular2/src/router/route_registry'; +import {RouteRegistry, rootHostComponent} from 'angular2/src/router/route_registry'; +import {RouteConfig} from 'angular2/src/router/route_config'; export function main() { describe('RouteRegistry', () => { var registry; - var handler = {}; - var handler2 = {}; beforeEach(() => { registry = new RouteRegistry(); }); it('should match the full URL', () => { - registry.config('/', '/', handler); - registry.config('/', '/test', handler2); + registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA}); + registry.config(rootHostComponent, {'path': '/test', 'component': DummyCompB}); var instruction = registry.recognize('/test'); - expect(instruction.getChildInstruction('default').component).toBe(handler2); + expect(instruction.getChildInstruction('default').component).toBe(DummyCompB); }); it('should match the full URL recursively', () => { - registry.config('/', '/first', handler); - registry.config(handler, '/second', handler2); + registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp}); var instruction = registry.recognize('/first/second'); - expect(instruction.getChildInstruction('default').component).toBe(handler); - expect(instruction.getChildInstruction('default').getChildInstruction('default').component).toBe(handler2); + var parentInstruction = instruction.getChildInstruction('default'); + var childInstruction = parentInstruction.getChildInstruction('default'); + + expect(parentInstruction.component).toBe(DummyParentComp); + expect(childInstruction.component).toBe(DummyCompB); }); }); } + +@RouteConfig([ + {'path': '/second', 'component': DummyCompB } +]) +class DummyParentComp {} + +class DummyCompA {} +class DummyCompB {} diff --git a/modules/angular2/test/router/router_spec.js b/modules/angular2/test/router/router_spec.js index 387d397c14..8144360a4f 100644 --- a/modules/angular2/test/router/router_spec.js +++ b/modules/angular2/test/router/router_spec.js @@ -28,11 +28,11 @@ export function main() { it('should navigate based on the initial URL state', inject([AsyncTestCompleter], (async) => { var outlet = makeDummyRef(); - router.config('/', {'component': 'Index' }) + router.config({'path': '/', 'component': 'Index' }) .then((_) => router.registerOutlet(outlet)) .then((_) => { expect(outlet.spy('activate')).toHaveBeenCalled(); - expect(location.urlChanges).toEqual(['/']); + expect(location.urlChanges).toEqual([]); async.done(); }); })); @@ -43,7 +43,7 @@ export function main() { router.registerOutlet(outlet) .then((_) => { - return router.config('/a', {'component': 'A' }); + return router.config({'path': '/a', 'component': 'A' }); }) .then((_) => router.navigate('/a')) .then((_) => { @@ -60,7 +60,7 @@ export function main() { .then((_) => router.navigate('/a')) .then((_) => { expect(outlet.spy('activate')).not.toHaveBeenCalled(); - return router.config('/a', {'component': 'A' }); + return router.config({'path': '/a', 'component': 'A' }); }) .then((_) => { expect(outlet.spy('activate')).toHaveBeenCalled();