X Tutup
Skip to content

Commit 07cdc2f

Browse files
committed
feat(router): add support for route links with no leading slash
Closes #4623
1 parent 7af27f9 commit 07cdc2f

File tree

4 files changed

+116
-15
lines changed

4 files changed

+116
-15
lines changed

modules/angular2/src/router/route_registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,14 @@ export class RouteRegistry {
254254
return instruction;
255255
}
256256

257+
public hasRoute(name: string, parentComponent: any): boolean {
258+
var componentRecognizer: RouteRecognizer = this._rules.get(parentComponent);
259+
if (isBlank(componentRecognizer)) {
260+
return false;
261+
}
262+
return componentRecognizer.hasRoute(name);
263+
}
264+
257265
// if the child includes a redirect like : "/" -> "/something",
258266
// we want to honor that redirection when creating the link
259267
private _generateRedirects(componentCursor: Type): Instruction {

modules/angular2/src/router/router.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -432,8 +432,21 @@ export class Router {
432432
}
433433
}
434434
} else if (first != '.') {
435-
throw new BaseException(
436-
`Link "${ListWrapper.toJSON(linkParams)}" must start with "/", "./", or "../"`);
435+
// For a link with no leading `./`, `/`, or `../`, we look for a sibling and child.
436+
// If both exist, we throw. Otherwise, we prefer whichever exists.
437+
var childRouteExists = this.registry.hasRoute(first, this.hostComponent);
438+
var parentRouteExists =
439+
isPresent(this.parent) && this.registry.hasRoute(first, this.parent.hostComponent);
440+
441+
if (parentRouteExists && childRouteExists) {
442+
let msg =
443+
`Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`;
444+
throw new BaseException(msg);
445+
}
446+
if (parentRouteExists) {
447+
router = this.parent;
448+
}
449+
rest = linkParams;
437450
}
438451

439452
if (rest[rest.length - 1] == '') {
@@ -445,7 +458,7 @@ export class Router {
445458
throw new BaseException(msg);
446459
}
447460

448-
// TODO: structural cloning and whatnot
461+
var nextInstruction = this.registry.generate(rest, router.hostComponent);
449462

450463
var url = [];
451464
var parent = router.parent;
@@ -454,8 +467,6 @@ export class Router {
454467
parent = parent.parent;
455468
}
456469

457-
var nextInstruction = this.registry.generate(rest, router.hostComponent);
458-
459470
while (url.length > 0) {
460471
nextInstruction = url.pop().replaceChild(nextInstruction);
461472
}

modules/angular2/test/router/integration/router_link_spec.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919

2020
import {NumberWrapper} from 'angular2/src/core/facade/lang';
2121
import {PromiseWrapper} from 'angular2/src/core/facade/async';
22+
import {ListWrapper} from 'angular2/src/core/facade/collection';
2223

2324
import {provide, Component, DirectiveResolver, View} from 'angular2/core';
2425

@@ -133,6 +134,58 @@ export function main() {
133134
});
134135
}));
135136

137+
it('should generate link hrefs from a child to its sibling with no leading slash',
138+
inject([AsyncTestCompleter], (async) => {
139+
compile()
140+
.then((_) => router.config([
141+
new Route({path: '/page/:number', component: NoPrefixSiblingPageCmp, as: 'Page'})
142+
]))
143+
.then((_) => router.navigateByUrl('/page/1'))
144+
.then((_) => {
145+
rootTC.detectChanges();
146+
expect(DOM.getAttribute(rootTC.debugElement.componentViewChildren[1]
147+
.componentViewChildren[0]
148+
.nativeElement,
149+
'href'))
150+
.toEqual('/page/2');
151+
async.done();
152+
});
153+
}));
154+
155+
it('should generate link hrefs to a child with no leading slash',
156+
inject([AsyncTestCompleter], (async) => {
157+
compile()
158+
.then((_) => router.config([
159+
new Route({path: '/book/:title/...', component: NoPrefixBookCmp, as: 'Book'})
160+
]))
161+
.then((_) => router.navigateByUrl('/book/1984/page/1'))
162+
.then((_) => {
163+
rootTC.detectChanges();
164+
expect(DOM.getAttribute(rootTC.debugElement.componentViewChildren[1]
165+
.componentViewChildren[0]
166+
.nativeElement,
167+
'href'))
168+
.toEqual('/book/1984/page/100');
169+
async.done();
170+
});
171+
}));
172+
173+
it('should throw when links without a leading slash are ambiguous',
174+
inject([AsyncTestCompleter], (async) => {
175+
compile()
176+
.then((_) => router.config([
177+
new Route({path: '/book/:title/...', component: AmbiguousBookCmp, as: 'Book'})
178+
]))
179+
.then((_) => router.navigateByUrl('/book/1984/page/1'))
180+
.then((_) => {
181+
var link = ListWrapper.toJSON(['Book', {number: 100}]);
182+
expect(() => rootTC.detectChanges())
183+
.toThrowErrorWith(
184+
`Link "${link}" is ambiguous, use "./" or "../" to disambiguate.`);
185+
async.done();
186+
});
187+
}));
188+
136189
it('should generate link hrefs when asynchronously loaded',
137190
inject([AsyncTestCompleter], (async) => {
138191
compile()
@@ -337,6 +390,21 @@ class SiblingPageCmp {
337390
}
338391
}
339392

393+
@Component({selector: 'page-cmp'})
394+
@View({
395+
template:
396+
`page #{{pageNumber}} | <a href="hello" [router-link]="[\'Page\', {number: nextPage}]">next</a>`,
397+
directives: [RouterLink]
398+
})
399+
class NoPrefixSiblingPageCmp {
400+
pageNumber: number;
401+
nextPage: number;
402+
constructor(params: RouteParams) {
403+
this.pageNumber = NumberWrapper.parseInt(params.get('number'), 10);
404+
this.nextPage = this.pageNumber + 1;
405+
}
406+
}
407+
340408
@Component({selector: 'hello-cmp'})
341409
@View({template: 'hello'})
342410
class HelloCmp {
@@ -377,3 +445,27 @@ class BookCmp {
377445
title: string;
378446
constructor(params: RouteParams) { this.title = params.get('title'); }
379447
}
448+
449+
@Component({selector: 'book-cmp'})
450+
@View({
451+
template: `<a href="hello" [router-link]="[\'Page\', {number: 100}]">{{title}}</a> |
452+
<router-outlet></router-outlet>`,
453+
directives: ROUTER_DIRECTIVES
454+
})
455+
@RouteConfig([new Route({path: '/page/:number', component: SiblingPageCmp, as: 'Page'})])
456+
class NoPrefixBookCmp {
457+
title: string;
458+
constructor(params: RouteParams) { this.title = params.get('title'); }
459+
}
460+
461+
@Component({selector: 'book-cmp'})
462+
@View({
463+
template: `<a href="hello" [router-link]="[\'Book\', {number: 100}]">{{title}}</a> |
464+
<router-outlet></router-outlet>`,
465+
directives: ROUTER_DIRECTIVES
466+
})
467+
@RouteConfig([new Route({path: '/page/:number', component: SiblingPageCmp, as: 'Book'})])
468+
class AmbiguousBookCmp {
469+
title: string;
470+
constructor(params: RouteParams) { this.title = params.get('title'); }
471+
}

modules/angular2/test/router/router_spec.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export function main() {
6161
});
6262
}));
6363

64-
6564
it('should activate viewports and update URL on navigate',
6665
inject([AsyncTestCompleter], (async) => {
6766
var outlet = makeDummyOutlet();
@@ -105,7 +104,6 @@ export function main() {
105104
});
106105
}));
107106

108-
109107
it('should navigate after being configured', inject([AsyncTestCompleter], (async) => {
110108
var outlet = makeDummyOutlet();
111109

@@ -121,14 +119,6 @@ export function main() {
121119
});
122120
}));
123121

124-
125-
it('should throw when linkParams does not start with a "/" or "./"', () => {
126-
expect(() => router.generate(['FirstCmp', 'SecondCmp']))
127-
.toThrowError(
128-
`Link "${ListWrapper.toJSON(['FirstCmp', 'SecondCmp'])}" must start with "/", "./", or "../"`);
129-
});
130-
131-
132122
it('should throw when linkParams does not include a route name', () => {
133123
expect(() => router.generate(['./']))
134124
.toThrowError(`Link "${ListWrapper.toJSON(['./'])}" must include a route name.`);

0 commit comments

Comments
 (0)
X Tutup