feat(router): support deep-linking to anywhere in the app

Closes #2642
This commit is contained in:
Brian Ford
2015-06-30 13:18:51 -07:00
parent 2335075506
commit f66ce096d8
16 changed files with 331 additions and 170 deletions
+6 -8
View File
@@ -42,7 +42,7 @@ export function main() {
beforeEachBindings(() => [
Pipeline,
RouteRegistry,
bind(RouteRegistry).toFactory(() => new RouteRegistry(MyComp)),
DirectiveResolver,
bind(Location).toClass(SpyLocation),
bind(Router)
@@ -129,7 +129,7 @@ export function main() {
it('should generate absolute hrefs that include the base href',
inject([AsyncTestCompleter], (async) => {
location.setBaseHref('/my/base');
compile('<a href="hello" router-link="user"></a>')
compile('<a href="hello" [router-link]="[\'./user\']"></a>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
@@ -141,7 +141,7 @@ export function main() {
it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" router-link="user"></a>')
compile('<a href="hello" [router-link]="[\'./user\']"></a>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
@@ -172,7 +172,7 @@ export function main() {
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
compile('<a href="hello" [router-link]="[\'./user\', {name: name}]">{{name}}</a>')
.then((_) => rtr.config({'path': '/user/:name', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
@@ -194,10 +194,8 @@ export function main() {
return dispatchedEvent;
};
it('test', inject([AsyncTestCompleter], (async) => { async.done(); }));
it('should navigate to link hrefs without params', inject([AsyncTestCompleter], (async) => {
compile('<a href="hello" router-link="user"></a>')
compile('<a href="hello" [router-link]="[\'./user\']"></a>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
@@ -218,7 +216,7 @@ export function main() {
it('should navigate to link hrefs in presence of base href',
inject([AsyncTestCompleter], (async) => {
location.setBaseHref('/base');
compile('<a href="hello" router-link="user"></a>')
compile('<a href="hello" [router-link]="[\'./user\']"></a>')
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
.then((_) => rtr.navigate('/a/b'))
.then((_) => {
@@ -10,43 +10,44 @@ import {
SpyObject
} from 'angular2/test_lib';
import {RouteRecognizer} from 'angular2/src/router/route_recognizer';
import {RouteRecognizer, RouteMatch} from 'angular2/src/router/route_recognizer';
export function main() {
describe('RouteRecognizer', () => {
var recognizer;
var handler = {'components': {'a': 'b'}};
var handler2 = {'components': {'b': 'c'}};
var handler = {'component': DummyCmpA};
var handler2 = {'component': DummyCmpB};
beforeEach(() => { recognizer = new RouteRecognizer(); });
it('should recognize a static segment', () => {
recognizer.addConfig('/test', handler);
expect(recognizer.recognize('/test')[0].handler).toEqual(handler);
var solution = recognizer.recognize('/test')[0];
expect(getComponentType(solution)).toEqual(handler['component']);
});
it('should recognize a single slash', () => {
recognizer.addConfig('/', handler);
var solution = recognizer.recognize('/')[0];
expect(solution.handler).toEqual(handler);
expect(getComponentType(solution)).toEqual(handler['component']);
});
it('should recognize a dynamic segment', () => {
recognizer.addConfig('/user/:name', handler);
var solution = recognizer.recognize('/user/brian')[0];
expect(solution.handler).toEqual(handler);
expect(solution.params).toEqual({'name': 'brian'});
expect(getComponentType(solution)).toEqual(handler['component']);
expect(solution.params()).toEqual({'name': 'brian'});
});
it('should recognize a star segment', () => {
recognizer.addConfig('/first/*rest', handler);
var solution = recognizer.recognize('/first/second/third')[0];
expect(solution.handler).toEqual(handler);
expect(solution.params).toEqual({'rest': 'second/third'});
expect(getComponentType(solution)).toEqual(handler['component']);
expect(solution.params()).toEqual({'rest': 'second/third'});
});
@@ -72,7 +73,7 @@ export function main() {
expect(solutions.length).toBe(1);
var solution = solutions[0];
expect(solution.handler).toEqual(handler);
expect(getComponentType(solution)).toEqual(handler['component']);
expect(solution.matchedUrl).toEqual('/b');
});
@@ -83,7 +84,7 @@ export function main() {
expect(solutions.length).toBe(1);
var solution = solutions[0];
expect(solution.handler).toEqual(handler);
expect(getComponentType(solution)).toEqual(handler['component']);
expect(solution.matchedUrl).toEqual('/bar');
});
@@ -106,16 +107,23 @@ export function main() {
expect(solutions[0].matchedUrl).toBe('/matias');
});
it('should generate URLs', () => {
it('should generate URLs with params', () => {
recognizer.addConfig('/app/user/:name', handler, 'user');
expect(recognizer.generate('user', {'name': 'misko'})).toEqual('/app/user/misko');
expect(recognizer.generate('user', {'name': 'misko'})['url']).toEqual('app/user/misko');
});
it('should throw in the absence of required params URLs', () => {
recognizer.addConfig('/app/user/:name', handler, 'user');
expect(() => recognizer.generate('user', {}))
recognizer.addConfig('app/user/:name', handler, 'user');
expect(() => recognizer.generate('user', {})['url'])
.toThrowError('Route generator for \'name\' was not included in parameters passed.');
});
});
}
function getComponentType(routeMatch: RouteMatch): any {
return routeMatch.recognizer.handler.componentType;
}
class DummyCmpA {}
class DummyCmpB {}
@@ -11,6 +11,7 @@ import {
} from 'angular2/test_lib';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {ListWrapper} from 'angular2/src/facade/collection';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {RouteConfig} from 'angular2/src/router/route_config_decorator';
@@ -19,7 +20,7 @@ export function main() {
describe('RouteRegistry', () => {
var registry, rootHostComponent = new Object();
beforeEach(() => { registry = new RouteRegistry(); });
beforeEach(() => { registry = new RouteRegistry(rootHostComponent); });
it('should match the full URL', inject([AsyncTestCompleter], (async) => {
registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA});
@@ -32,6 +33,68 @@ export function main() {
});
}));
it('should generate URLs starting at the given component', () => {
registry.config(rootHostComponent,
{'path': '/first/...', 'component': DummyParentComp, 'as': 'firstCmp'});
expect(registry.generate(['./firstCmp/secondCmp'], rootHostComponent))
.toEqual('/first/second');
expect(registry.generate(['./secondCmp'], DummyParentComp)).toEqual('/second');
});
it('should generate URLs with params', () => {
registry.config(
rootHostComponent,
{'path': '/first/:param/...', 'component': DummyParentParamComp, 'as': 'firstCmp'});
var url = registry.generate(['./firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}],
rootHostComponent);
expect(url).toEqual('/first/one/second/two');
});
it('should generate URLs from the root component when the path starts with /', () => {
registry.config(rootHostComponent,
{'path': '/first/...', 'component': DummyParentComp, 'as': 'firstCmp'});
expect(registry.generate(['/firstCmp', 'secondCmp'], rootHostComponent))
.toEqual('/first/second');
expect(registry.generate(['/firstCmp', 'secondCmp'], DummyParentComp))
.toEqual('/first/second');
expect(registry.generate(['/firstCmp/secondCmp'], DummyParentComp)).toEqual('/first/second');
});
it('should generate URLs of loaded components after they are loaded',
inject([AsyncTestCompleter], (async) => {
registry.config(rootHostComponent, {
'path': '/first/...',
'component': {'type': 'loader', 'loader': AsyncParentLoader},
'as': 'firstCmp'
});
expect(() => registry.generate(['/firstCmp/secondCmp'], rootHostComponent))
.toThrowError('Could not find route config for "secondCmp".');
registry.recognize('/first/second', rootHostComponent)
.then((_) => {
expect(registry.generate(['/firstCmp/secondCmp'], rootHostComponent))
.toEqual('/first/second');
async.done();
});
}));
it('should throw when linkParams does not start with a "/" or "./"', () => {
expect(() => registry.generate(['firstCmp', 'secondCmp'], rootHostComponent))
.toThrowError(
`Link "${ListWrapper.toJSON(['firstCmp', 'secondCmp'])}" must start with "/" or "./"`);
});
it('should throw when linkParams does not include a route name', () => {
expect(() => registry.generate(['./'], rootHostComponent))
.toThrowError(`Link "${ListWrapper.toJSON(['./'])}" must include a route name.`);
expect(() => registry.generate(['/'], rootHostComponent))
.toThrowError(`Link "${ListWrapper.toJSON(['/'])}" must include a route name.`);
});
it('should prefer static segments to dynamic', inject([AsyncTestCompleter], (async) => {
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompB});
registry.config(rootHostComponent, {'path': '/home', 'component': DummyCompA});
@@ -172,6 +235,11 @@ class DummyAsyncComp {
class DummyCompA {}
class DummyCompB {}
@RouteConfig([{'path': '/second', 'component': DummyCompB}])
@RouteConfig([{'path': '/second', 'component': DummyCompB, 'as': 'secondCmp'}])
class DummyParentComp {
}
@RouteConfig([{'path': '/second/:param', 'component': DummyCompB, 'as': 'secondCmp'}])
class DummyParentParamComp {
}
+1 -1
View File
@@ -31,7 +31,7 @@ export function main() {
beforeEachBindings(() => [
Pipeline,
RouteRegistry,
bind(RouteRegistry).toFactory(() => new RouteRegistry(AppCmp)),
DirectiveResolver,
bind(Location).toClass(SpyLocation),
bind(Router)