fix(router): use lists for RouteConfig annotations

This commit is contained in:
Brian Ford 2015-04-29 15:47:12 -07:00
parent ea546f5069
commit 4965226f3f
7 changed files with 115 additions and 62 deletions

View File

@ -1,18 +1,13 @@
import {CONST} from 'angular2/src/facade/lang'; import {CONST} from 'angular2/src/facade/lang';
import {List} from 'angular2/src/facade/collection';
/** /**
* You use the RouteConfig annotation to ... * You use the RouteConfig annotation to ...
*/ */
export class RouteConfig { export class RouteConfig {
path:string; configs;
redirectTo:string;
component:any;
//TODO: "alias," or "as"
@CONST() @CONST()
constructor({path, component, redirectTo}:{path:string, component:any, redirectTo:string} = {}) { constructor(configs:List) {
this.path = path; this.configs = configs;
this.component = component;
this.redirectTo = redirectTo;
} }
} }

View File

@ -41,11 +41,16 @@ export class RouteRecognizer {
var solution = StringMapWrapper.create(); var solution = StringMapWrapper.create();
StringMapWrapper.set(solution, 'handler', pathRecognizer.handler); StringMapWrapper.set(solution, 'handler', pathRecognizer.handler);
StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url)); StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url));
StringMapWrapper.set(solution, 'matchedUrl', match[0]);
//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); var unmatchedUrl = StringWrapper.substring(url, match[0].length);
StringMapWrapper.set(solution, 'unmatchedUrl', unmatchedUrl); StringMapWrapper.set(solution, 'unmatchedUrl', unmatchedUrl);
}
ListWrapper.push(solutions, solution); ListWrapper.push(solutions, solution);
} }
}); });

View File

@ -1,10 +1,12 @@
import {RouteRecognizer} from './route_recognizer'; import {RouteRecognizer} from './route_recognizer';
import {Instruction, noopInstruction} from './instruction'; import {Instruction, noopInstruction} from './instruction';
import {List, ListWrapper, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; 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 {RouteConfig} from './route_config';
import {reflector} from 'angular2/src/reflection/reflection'; import {reflector} from 'angular2/src/reflection/reflection';
export const rootHostComponent = 'ROOT_HOST';
export class RouteRegistry { export class RouteRegistry {
_rules:Map<any, RouteRecognizer>; _rules:Map<any, RouteRecognizer>;
@ -12,10 +14,7 @@ export class RouteRegistry {
this._rules = MapWrapper.create(); this._rules = MapWrapper.create();
} }
config(parentComponent, path:string, component:any, alias:string = null) { config(parentComponent, config) {
if (parentComponent === 'app') {
parentComponent = '/';
}
var recognizer:RouteRecognizer; var recognizer:RouteRecognizer;
if (MapWrapper.contains(this._rules, parentComponent)) { if (MapWrapper.contains(this._rules, parentComponent)) {
@ -25,16 +24,14 @@ export class RouteRegistry {
MapWrapper.set(this._rules, parentComponent, recognizer); MapWrapper.set(this._rules, parentComponent, recognizer);
} }
config = normalizeConfig(config);
var components = StringMapWrapper.get(config, 'components');
StringMapWrapper.forEach(components, (component, _) => {
this._configFromComponent(component); this._configFromComponent(component);
});
//TODO: support sibling components recognizer.addConfig(config['path'], config, config['alias']);
var components = StringMapWrapper.create();
StringMapWrapper.set(components, 'default', component);
var handler = StringMapWrapper.create();
StringMapWrapper.set(handler, 'components', components);
recognizer.addConfig(path, handler, alias);
} }
_configFromComponent(component) { _configFromComponent(component) {
@ -53,7 +50,9 @@ export class RouteRegistry {
var annotation = annotations[i]; var annotation = annotations[i];
if (annotation instanceof RouteConfig) { 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: make recognized context a class
// TODO: change parentComponent into parentContext // TODO: change parentComponent into parentContext
recognize(url:string, parentComponent = '/') { recognize(url:string, parentComponent = rootHostComponent) {
var componentRecognizer = MapWrapper.get(this._rules, parentComponent); var componentRecognizer = MapWrapper.get(this._rules, parentComponent);
if (isBlank(componentRecognizer)) { if (isBlank(componentRecognizer)) {
return null; return null;
@ -106,7 +105,7 @@ export class RouteRegistry {
generate(name:string, params:any) { generate(name:string, params:any) {
//TODO: implement for hierarchical routes //TODO: implement for hierarchical routes
var componentRecognizer = MapWrapper.get(this._rules, '/'); var componentRecognizer = MapWrapper.get(this._rules, rootHostComponent);
if (isPresent(componentRecognizer)) { if (isPresent(componentRecognizer)) {
return componentRecognizer.generate(name, params); return componentRecognizer.generate(name, params);
} }
@ -127,3 +126,29 @@ function handlerToLeafInstructions(context, parentComponent) {
matchedUrl: context['matchedUrl'] 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;
}

View File

@ -2,7 +2,7 @@ import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection'; import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
import {isBlank} from 'angular2/src/facade/lang'; import {isBlank} from 'angular2/src/facade/lang';
import {RouteRegistry} from './route_registry'; import {RouteRegistry, rootHostComponent} from './route_registry';
import {Pipeline} from './pipeline'; import {Pipeline} from './pipeline';
import {Instruction} from './instruction'; import {Instruction} from './instruction';
import {RouterOutlet} from './router_outlet'; import {RouterOutlet} from './router_outlet';
@ -18,7 +18,7 @@ import {Location} from './location';
* @exportedAs angular2/router * @exportedAs angular2/router
*/ */
export class Router { export class Router {
name; hostComponent:any;
parent:Router; parent:Router;
navigating:boolean; navigating:boolean;
lastNavigationAttempt: string; lastNavigationAttempt: string;
@ -31,8 +31,8 @@ export class Router {
_subject:EventEmitter; _subject:EventEmitter;
_location:Location; _location:Location;
constructor(registry:RouteRegistry, pipeline:Pipeline, location:Location, parent:Router = null, name = '/') { constructor(registry:RouteRegistry, pipeline:Pipeline, location:Location, parent:Router, hostComponent) {
this.name = name; this.hostComponent = hostComponent;
this.navigating = false; this.navigating = false;
this.parent = parent; this.parent = parent;
this.previousUrl = null; this.previousUrl = null;
@ -42,8 +42,7 @@ export class Router {
this._registry = registry; this._registry = registry;
this._pipeline = pipeline; this._pipeline = pipeline;
this._subject = new EventEmitter(); this._subject = new EventEmitter();
this._location.subscribe((url) => this.navigate(url)); //this._location.subscribe((url) => this.navigate(url));
this.navigate(location.path());
} }
@ -73,11 +72,30 @@ export class Router {
* # Usage * # 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) { config(config:any) {
this._registry.config(this.name, path, component, alias);
//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(); return this.renavigate();
} }
@ -184,13 +202,14 @@ export class Router {
export class RootRouter extends Router { export class RootRouter extends Router {
constructor(pipeline:Pipeline, location:Location) { 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 { class ChildRouter extends Router {
constructor(parent, name) { constructor(parent:Router, hostComponent) {
super(parent._registry, parent._pipeline, parent._location, parent, name); super(parent._registry, parent._pipeline, parent._location, parent, hostComponent);
this.parent = parent; this.parent = parent;
} }
} }

View File

@ -53,7 +53,7 @@ export function main() {
it('should work in a simple case', inject([AsyncTestCompleter], (async) => { it('should work in a simple case', inject([AsyncTestCompleter], (async) => {
compile() compile()
.then((_) => router.config('/test', HelloCmp)) .then((_) => router.config({'path': '/test', 'component': HelloCmp}))
.then((_) => router.navigate('/test')) .then((_) => router.navigate('/test'))
.then((_) => { .then((_) => {
view.detectChanges(); view.detectChanges();
@ -65,7 +65,7 @@ export function main() {
it('should navigate between components with different parameters', inject([AsyncTestCompleter], (async) => { it('should navigate between components with different parameters', inject([AsyncTestCompleter], (async) => {
compile() compile()
.then((_) => router.config('/user/:name', UserCmp)) .then((_) => router.config({'path': '/user/:name', 'component': UserCmp}))
.then((_) => router.navigate('/user/brian')) .then((_) => router.navigate('/user/brian'))
.then((_) => { .then((_) => {
view.detectChanges(); view.detectChanges();
@ -82,7 +82,7 @@ export function main() {
it('should work with child routers', inject([AsyncTestCompleter], (async) => { it('should work with child routers', inject([AsyncTestCompleter], (async) => {
compile('outer { <router-outlet></router-outlet> }') compile('outer { <router-outlet></router-outlet> }')
.then((_) => router.config('/a', ParentCmp)) .then((_) => router.config({'path': '/a', 'component': ParentCmp}))
.then((_) => router.navigate('/a/b')) .then((_) => router.navigate('/a/b'))
.then((_) => { .then((_) => {
view.detectChanges(); view.detectChanges();
@ -95,7 +95,7 @@ export function main() {
it('should generate link hrefs', inject([AsyncTestCompleter], (async) => { it('should generate link hrefs', inject([AsyncTestCompleter], (async) => {
ctx.name = 'brian'; ctx.name = 'brian';
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>') compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
.then((_) => router.config('/user/:name', UserCmp, 'user')) .then((_) => router.config({'path': '/user/:name', 'component': UserCmp, 'alias': 'user'}))
.then((_) => router.navigate('/a/b')) .then((_) => router.navigate('/a/b'))
.then((_) => { .then((_) => {
view.detectChanges(); view.detectChanges();
@ -144,10 +144,10 @@ class UserCmp {
template: "inner { <router-outlet></router-outlet> }", template: "inner { <router-outlet></router-outlet> }",
directives: [RouterOutlet] directives: [RouterOutlet]
}) })
@RouteConfig({ @RouteConfig([{
path: '/b', path: '/b',
component: HelloCmp component: HelloCmp
}) }])
class ParentCmp { class ParentCmp {
constructor() {} constructor() {}
} }

View File

@ -6,36 +6,45 @@ import {
inject, beforeEach, inject, beforeEach,
SpyObject} from 'angular2/test_lib'; 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() { export function main() {
describe('RouteRegistry', () => { describe('RouteRegistry', () => {
var registry; var registry;
var handler = {};
var handler2 = {};
beforeEach(() => { beforeEach(() => {
registry = new RouteRegistry(); registry = new RouteRegistry();
}); });
it('should match the full URL', () => { it('should match the full URL', () => {
registry.config('/', '/', handler); registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA});
registry.config('/', '/test', handler2); registry.config(rootHostComponent, {'path': '/test', 'component': DummyCompB});
var instruction = registry.recognize('/test'); 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', () => { it('should match the full URL recursively', () => {
registry.config('/', '/first', handler); registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp});
registry.config(handler, '/second', handler2);
var instruction = registry.recognize('/first/second'); var instruction = registry.recognize('/first/second');
expect(instruction.getChildInstruction('default').component).toBe(handler); var parentInstruction = instruction.getChildInstruction('default');
expect(instruction.getChildInstruction('default').getChildInstruction('default').component).toBe(handler2); 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 {}

View File

@ -28,11 +28,11 @@ export function main() {
it('should navigate based on the initial URL state', inject([AsyncTestCompleter], (async) => { it('should navigate based on the initial URL state', inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyRef(); var outlet = makeDummyRef();
router.config('/', {'component': 'Index' }) router.config({'path': '/', 'component': 'Index' })
.then((_) => router.registerOutlet(outlet)) .then((_) => router.registerOutlet(outlet))
.then((_) => { .then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled(); expect(outlet.spy('activate')).toHaveBeenCalled();
expect(location.urlChanges).toEqual(['/']); expect(location.urlChanges).toEqual([]);
async.done(); async.done();
}); });
})); }));
@ -43,7 +43,7 @@ export function main() {
router.registerOutlet(outlet) router.registerOutlet(outlet)
.then((_) => { .then((_) => {
return router.config('/a', {'component': 'A' }); return router.config({'path': '/a', 'component': 'A' });
}) })
.then((_) => router.navigate('/a')) .then((_) => router.navigate('/a'))
.then((_) => { .then((_) => {
@ -60,7 +60,7 @@ export function main() {
.then((_) => router.navigate('/a')) .then((_) => router.navigate('/a'))
.then((_) => { .then((_) => {
expect(outlet.spy('activate')).not.toHaveBeenCalled(); expect(outlet.spy('activate')).not.toHaveBeenCalled();
return router.config('/a', {'component': 'A' }); return router.config({'path': '/a', 'component': 'A' });
}) })
.then((_) => { .then((_) => {
expect(outlet.spy('activate')).toHaveBeenCalled(); expect(outlet.spy('activate')).toHaveBeenCalled();