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 {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;
}
}

View File

@ -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);
}
});

View File

@ -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<any, RouteRecognizer>;
@ -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;
}

View File

@ -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<any, Router>;
_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;
}
}

View File

@ -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 { <router-outlet></router-outlet> }')
.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('<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((_) => {
view.detectChanges();
@ -144,10 +144,10 @@ class UserCmp {
template: "inner { <router-outlet></router-outlet> }",
directives: [RouterOutlet]
})
@RouteConfig({
@RouteConfig([{
path: '/b',
component: HelloCmp
})
}])
class ParentCmp {
constructor() {}
}

View File

@ -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 {}

View File

@ -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();