feat(router): support deep-linking to siblings

Closes #2807
This commit is contained in:
Brian Ford
2015-07-06 17:41:15 -07:00
parent d828664d0c
commit 286a249a9a
7 changed files with 198 additions and 92 deletions
+73 -2
View File
@@ -18,7 +18,7 @@ import {
import {Injector, bind} from 'angular2/di';
import {Component, View} from 'angular2/src/core/annotations/decorators';
import * as annotations from 'angular2/src/core/annotations_impl/view';
import {CONST} from 'angular2/src/facade/lang';
import {CONST, NumberWrapper} from 'angular2/src/facade/lang';
import {RootRouter} from 'angular2/src/router/router';
import {Pipeline} from 'angular2/src/router/pipeline';
@@ -42,7 +42,7 @@ export function main() {
beforeEachBindings(() => [
Pipeline,
bind(RouteRegistry).toFactory(() => new RouteRegistry(MyComp)),
RouteRegistry,
DirectiveResolver,
bind(Location).toClass(SpyLocation),
bind(Router)
@@ -185,6 +185,49 @@ export function main() {
});
}));
it('should generate link hrefs from a child to its sibling',
inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => rtr.config(
{'path': '/page/:number', 'component': SiblingPageCmp, 'as': 'page'}))
.then((_) => rtr.navigate('/page/1'))
.then((_) => {
rootTC.detectChanges();
expect(DOM.getAttribute(rootTC.componentViewChildren[1]
.componentViewChildren[0]
.children[0]
.nativeElement,
'href'))
.toEqual('/page/2');
async.done();
});
}));
it('should generate relative links preserving the existing parent route',
inject([AsyncTestCompleter], (async) => {
compile()
.then((_) =>
rtr.config({'path': '/book/:title/...', 'component': BookCmp, 'as': 'book'}))
.then((_) => rtr.navigate('/book/1984/page/1'))
.then((_) => {
rootTC.detectChanges();
expect(DOM.getAttribute(
rootTC.componentViewChildren[1].componentViewChildren[0].nativeElement,
'href'))
.toEqual('/book/1984/page/100');
expect(DOM.getAttribute(rootTC.componentViewChildren[1]
.componentViewChildren[2]
.componentViewChildren[0]
.children[0]
.nativeElement,
'href'))
.toEqual('/book/1984/page/2');
async.done();
});
}));
describe('when clicked', () => {
var clickOnElement = function(view) {
@@ -266,6 +309,34 @@ class UserCmp {
}
@Component({selector: 'page-cmp'})
@View({
template:
`page #{{pageNumber}} | <a href="hello" [router-link]="[\'../page\', {number: nextPage}]">next</a>`,
directives: [RouterLink]
})
class SiblingPageCmp {
pageNumber: number;
nextPage: number;
constructor(params: RouteParams) {
this.pageNumber = NumberWrapper.parseInt(params.get('number'), 10);
this.nextPage = this.pageNumber + 1;
}
}
@Component({selector: 'book-cmp'})
@View({
template: `<a href="hello" [router-link]="[\'./page\', {number: 100}]">{{title}}</a> |
<router-outlet></router-outlet>`,
directives: [RouterLink, RouterOutlet]
})
@RouteConfig([{path: '/page/:number', component: SiblingPageCmp, 'as': 'page'}])
class BookCmp {
title: string;
constructor(params: RouteParams) { this.title = params.get('title'); }
}
@Component({selector: 'parent-cmp'})
@View({template: "inner { <router-outlet></router-outlet> }", directives: [RouterOutlet]})
@RouteConfig([{path: '/b', component: HelloCmp}])
@@ -20,7 +20,7 @@ export function main() {
describe('RouteRegistry', () => {
var registry, rootHostComponent = new Object();
beforeEach(() => { registry = new RouteRegistry(rootHostComponent); });
beforeEach(() => { registry = new RouteRegistry(); });
it('should match the full URL', inject([AsyncTestCompleter], (async) => {
registry.config(rootHostComponent, {'path': '/', 'component': DummyCompA});
@@ -37,9 +37,9 @@ export function main() {
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');
expect(registry.generate(['firstCmp', 'secondCmp'], rootHostComponent))
.toEqual('first/second');
expect(registry.generate(['secondCmp'], DummyParentComp)).toEqual('second');
});
it('should generate URLs with params', () => {
@@ -47,20 +47,9 @@ export function main() {
rootHostComponent,
{'path': '/first/:param/...', 'component': DummyParentParamComp, 'as': 'firstCmp'});
var url = registry.generate(['./firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}],
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');
expect(url).toEqual('first/one/second/two');
});
it('should generate URLs of loaded components after they are loaded',
@@ -71,30 +60,17 @@ export function main() {
'as': 'firstCmp'
});
expect(() => registry.generate(['/firstCmp/secondCmp'], rootHostComponent))
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');
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});
+32 -1
View File
@@ -14,6 +14,7 @@ import {
import {IMPLEMENTS} from 'angular2/src/facade/lang';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {ListWrapper} from 'angular2/src/facade/collection';
import {Router, RootRouter} from 'angular2/src/router/router';
import {Pipeline} from 'angular2/src/router/pipeline';
import {RouterOutlet} from 'angular2/src/router/router_outlet';
@@ -21,6 +22,7 @@ import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {RouteConfig} from 'angular2/src/router/route_config_decorator';
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
import {bind} from 'angular2/di';
@@ -31,7 +33,7 @@ export function main() {
beforeEachBindings(() => [
Pipeline,
bind(RouteRegistry).toFactory(() => new RouteRegistry(AppCmp)),
RouteRegistry,
DirectiveResolver,
bind(Location).toClass(SpyLocation),
bind(Router)
@@ -74,6 +76,7 @@ export function main() {
});
}));
it('should navigate after being configured', inject([AsyncTestCompleter], (async) => {
var outlet = makeDummyOutlet();
@@ -88,6 +91,30 @@ export function main() {
async.done();
});
}));
it('should throw when linkParams does not start with a "/" or "./"', () => {
expect(() => router.generate(['firstCmp', 'secondCmp']))
.toThrowError(
`Link "${ListWrapper.toJSON(['firstCmp', 'secondCmp'])}" must start with "/", "./", or "../"`);
});
it('should throw when linkParams does not include a route name', () => {
expect(() => router.generate(['./']))
.toThrowError(`Link "${ListWrapper.toJSON(['./'])}" must include a route name.`);
expect(() => router.generate(['/']))
.toThrowError(`Link "${ListWrapper.toJSON(['/'])}" must include a route name.`);
});
it('should generate URLs from the root component when the path starts with /', () => {
router.config({'path': '/first/...', 'component': DummyParentComp, 'as': 'firstCmp'});
expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second');
expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second');
expect(router.generate(['/firstCmp/secondCmp'])).toEqual('/first/second');
});
});
}
@@ -99,6 +126,10 @@ class DummyOutlet extends SpyObject {
class DummyComponent {}
@RouteConfig([{'path': '/second', 'component': DummyComponent, 'as': 'secondCmp'}])
class DummyParentComp {
}
function makeDummyOutlet() {
var ref = new DummyOutlet();
ref.spy('activate').andCallFake((_) => PromiseWrapper.resolve(true));