Skip to content

Commit de56dd5

Browse files
committed
feat(router): add RouterLink
1 parent fa5bfe4 commit de56dd5

File tree

4 files changed

+213
-90
lines changed

4 files changed

+213
-90
lines changed

modules/angular2/alt_router.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ export {Router, RouterOutletMap} from './src/alt_router/router';
88
export {RouteSegment} from './src/alt_router/segments';
99
export {Routes} from './src/alt_router/metadata/decorators';
1010
export {Route} from './src/alt_router/metadata/metadata';
11-
export {RouterUrlParser, DefaultRouterUrlParser} from './src/alt_router/router_url_parser';
11+
export {
12+
RouterUrlSerializer,
13+
DefaultRouterUrlSerializer
14+
} from './src/alt_router/router_url_serializer';
1215
export {OnActivate} from './src/alt_router/interfaces';
1316

1417
import {RouterOutlet} from './src/alt_router/directives/router_outlet';
18+
import {RouterLink} from './src/alt_router/directives/router_link';
1519
import {CONST_EXPR} from './src/facade/lang';
1620

17-
export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet]);
21+
export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet, RouterLink]);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
ResolvedReflectiveProvider,
3+
Directive,
4+
DynamicComponentLoader,
5+
ViewContainerRef,
6+
Attribute,
7+
ComponentRef,
8+
ComponentFactory,
9+
ReflectiveInjector,
10+
OnInit,
11+
HostListener,
12+
HostBinding,
13+
Input,
14+
OnDestroy
15+
} from 'angular2/core';
16+
import {RouterOutletMap, Router} from '../router';
17+
import {RouteSegment, UrlSegment, Tree} from '../segments';
18+
import {link} from '../link';
19+
import {isString} from 'angular2/src/facade/lang';
20+
import {ObservableWrapper} from 'angular2/src/facade/async';
21+
22+
@Directive({selector: '[routerLink]'})
23+
export class RouterLink implements OnDestroy {
24+
@Input() target: string;
25+
private _changes: any[] = [];
26+
private _targetUrl: Tree<UrlSegment>;
27+
private _subscription: any;
28+
29+
@HostBinding() private href: string;
30+
31+
constructor(private _router: Router, private _segment: RouteSegment) {
32+
this._subscription = ObservableWrapper.subscribe(_router.changes, (_) => {
33+
this._targetUrl = _router.urlTree;
34+
this._updateTargetUrlAndHref();
35+
});
36+
}
37+
38+
ngOnDestroy() { ObservableWrapper.dispose(this._subscription); }
39+
40+
@Input()
41+
set routerLink(data: any[]) {
42+
this._changes = data;
43+
this._updateTargetUrlAndHref();
44+
}
45+
46+
@HostListener("click")
47+
onClick(): boolean {
48+
if (!isString(this.target) || this.target == '_self') {
49+
this._router.navigate(this._targetUrl);
50+
return false;
51+
}
52+
return true;
53+
}
54+
55+
private _updateTargetUrlAndHref(): void {
56+
this._targetUrl = link(this._segment, this._router.urlTree, this._changes);
57+
this.href = this._router.serializeUrl(this._targetUrl);
58+
}
59+
}

modules/angular2/src/alt_router/router.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import {provide, ReflectiveInjector, ComponentResolver} from 'angular2/core';
22
import {RouterOutlet} from './directives/router_outlet';
33
import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
4+
import {EventEmitter, Observable} from 'angular2/src/facade/async';
45
import {StringMapWrapper} from 'angular2/src/facade/collection';
56
import {BaseException} from 'angular2/src/facade/exceptions';
6-
import {RouterUrlParser} from './router_url_parser';
7+
import {RouterUrlSerializer} from './router_url_serializer';
78
import {recognize} from './recognize';
89
import {
910
equalSegments,
1011
routeSegmentComponentFactory,
1112
RouteSegment,
1213
Tree,
1314
rootNode,
14-
TreeNode
15+
TreeNode,
16+
UrlSegment,
17+
serializeRouteSegmentTree
1518
} from './segments';
1619
import {hasLifecycleHook} from './lifecycle_reflector';
1720
import {DEFAULT_OUTLET_NAME} from './constants';
@@ -23,23 +26,39 @@ export class RouterOutletMap {
2326
}
2427

2528
export class Router {
26-
private prevTree: Tree<RouteSegment>;
29+
private _prevTree: Tree<RouteSegment>;
30+
private _urlTree: Tree<UrlSegment>;
31+
32+
private _changes: EventEmitter<void> = new EventEmitter<void>();
33+
2734
constructor(private _componentType: Type, private _componentResolver: ComponentResolver,
28-
private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {}
35+
private _urlSerializer: RouterUrlSerializer,
36+
private _routerOutletMap: RouterOutletMap) {}
2937

30-
navigateByUrl(url: string): Promise<void> {
31-
let urlSegmentTree = this._urlParser.parse(url);
32-
return recognize(this._componentResolver, this._componentType, urlSegmentTree)
38+
get urlTree(): Tree<UrlSegment> { return this._urlTree; }
39+
40+
navigate(url: Tree<UrlSegment>): Promise<void> {
41+
this._urlTree = url;
42+
return recognize(this._componentResolver, this._componentType, url)
3343
.then(currTree => {
34-
let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null;
35-
new _SegmentLoader(currTree, this.prevTree)
44+
let prevRoot = isPresent(this._prevTree) ? rootNode(this._prevTree) : null;
45+
new _LoadSegments(currTree, this._prevTree)
3646
.loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap);
37-
this.prevTree = currTree;
47+
this._prevTree = currTree;
48+
this._changes.emit(null);
3849
});
3950
}
51+
52+
serializeUrl(url: Tree<UrlSegment>): string { return this._urlSerializer.serialize(url); }
53+
54+
navigateByUrl(url: string): Promise<void> {
55+
return this.navigate(this._urlSerializer.parse(url));
56+
}
57+
58+
get changes(): Observable<void> { return this._changes; }
4059
}
4160

42-
class _SegmentLoader {
61+
class _LoadSegments {
4362
constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
4463

4564
loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,

modules/angular2/test/alt_router/integration_spec.ts

Lines changed: 118 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -12,116 +12,143 @@ import {
1212
inject,
1313
beforeEachProviders,
1414
it,
15-
xit
15+
xit,
16+
fakeAsync,
17+
tick
1618
} from 'angular2/testing_internal';
1719
import {provide, Component, ComponentResolver} from 'angular2/core';
1820

21+
1922
import {
2023
Router,
2124
RouterOutletMap,
2225
RouteSegment,
2326
Route,
2427
ROUTER_DIRECTIVES,
2528
Routes,
26-
RouterUrlParser,
27-
DefaultRouterUrlParser,
29+
RouterUrlSerializer,
30+
DefaultRouterUrlSerializer,
2831
OnActivate
2932
} from 'angular2/alt_router';
33+
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
3034

3135
export function main() {
3236
describe('navigation', () => {
3337
beforeEachProviders(() => [
34-
provide(RouterUrlParser, {useClass: DefaultRouterUrlParser}),
38+
provide(RouterUrlSerializer, {useClass: DefaultRouterUrlSerializer}),
3539
RouterOutletMap,
3640
provide(Router,
3741
{
3842
useFactory: (resolver, urlParser, outletMap) =>
3943
new Router(RootCmp, resolver, urlParser, outletMap),
40-
deps: [ComponentResolver, RouterUrlParser, RouterOutletMap]
44+
deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap]
4145
})
4246
]);
4347

4448
it('should support nested routes',
45-
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
46-
let fixture;
47-
compileRoot(tcb)
48-
.then((rtc) => {fixture = rtc})
49-
.then((_) => router.navigateByUrl('/team/22/user/victor'))
50-
.then((_) => {
51-
fixture.detectChanges();
52-
expect(fixture.debugElement.nativeElement)
53-
.toHaveText('team 22 { hello victor, aux: }');
54-
async.done();
55-
});
56-
}));
49+
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
50+
let fixture = tcb.createFakeAsync(RootCmp);
51+
52+
router.navigateByUrl('/team/22/user/victor');
53+
advance(fixture);
54+
55+
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello victor, aux: }');
56+
})));
5757

5858
it('should support aux routes',
59-
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
60-
let fixture;
61-
compileRoot(tcb)
62-
.then((rtc) => {fixture = rtc})
63-
.then((_) => router.navigateByUrl('/team/22/user/victor(/simple)'))
64-
.then((_) => {
65-
fixture.detectChanges();
66-
expect(fixture.debugElement.nativeElement)
67-
.toHaveText('team 22 { hello victor, aux: simple }');
68-
async.done();
69-
});
70-
}));
71-
72-
it('should unload outlets',
73-
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
74-
let fixture;
75-
compileRoot(tcb)
76-
.then((rtc) => {fixture = rtc})
77-
.then((_) => router.navigateByUrl('/team/22/user/victor(/simple)'))
78-
.then((_) => router.navigateByUrl('/team/22/user/victor'))
79-
.then((_) => {
80-
fixture.detectChanges();
81-
expect(fixture.debugElement.nativeElement)
82-
.toHaveText('team 22 { hello victor, aux: }');
83-
async.done();
84-
});
85-
}));
59+
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
60+
let fixture = tcb.createFakeAsync(RootCmp);
61+
62+
router.navigateByUrl('/team/22/user/victor(/simple)');
63+
advance(fixture);
64+
65+
expect(fixture.debugElement.nativeElement)
66+
.toHaveText('team 22 { hello victor, aux: simple }');
67+
})));
68+
69+
it('should unload outlets', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
70+
let fixture = tcb.createFakeAsync(RootCmp);
71+
72+
router.navigateByUrl('/team/22/user/victor(/simple)');
73+
advance(fixture);
74+
75+
router.navigateByUrl('/team/22/user/victor');
76+
advance(fixture);
77+
78+
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello victor, aux: }');
79+
})));
8680

8781
it('should unload nested outlets',
88-
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
89-
let fixture;
90-
compileRoot(tcb)
91-
.then((rtc) => {fixture = rtc})
92-
.then((_) => router.navigateByUrl('/team/22/user/victor(/simple)'))
93-
.then((_) => router.navigateByUrl('/'))
94-
.then((_) => {
95-
fixture.detectChanges();
96-
expect(fixture.debugElement.nativeElement).toHaveText('');
97-
async.done();
98-
});
99-
}));
82+
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
83+
let fixture = tcb.createFakeAsync(RootCmp);
84+
85+
router.navigateByUrl('/team/22/user/victor(/simple)');
86+
advance(fixture);
87+
88+
router.navigateByUrl('/');
89+
advance(fixture);
90+
91+
expect(fixture.debugElement.nativeElement).toHaveText('');
92+
})));
10093

10194
it('should update nested routes when url changes',
102-
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
103-
let fixture;
104-
let team1;
105-
let team2;
106-
compileRoot(tcb)
107-
.then((rtc) => {fixture = rtc})
108-
.then((_) => router.navigateByUrl('/team/22/user/victor'))
109-
.then((_) => { team1 = fixture.debugElement.children[1].componentInstance; })
110-
.then((_) => router.navigateByUrl('/team/22/user/fedor'))
111-
.then((_) => { team2 = fixture.debugElement.children[1].componentInstance; })
112-
.then((_) => {
113-
fixture.detectChanges();
114-
expect(team1).toBe(team2);
115-
expect(fixture.debugElement.nativeElement)
116-
.toHaveText('team 22 { hello fedor, aux: }');
117-
async.done();
118-
});
119-
}));
120-
121-
// unload unused nodes
95+
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
96+
let fixture = tcb.createFakeAsync(RootCmp);
97+
98+
router.navigateByUrl('/team/22/user/victor');
99+
advance(fixture);
100+
let team1 = fixture.debugElement.children[1].componentInstance;
101+
102+
router.navigateByUrl('/team/22/user/fedor');
103+
advance(fixture);
104+
let team2 = fixture.debugElement.children[1].componentInstance;
105+
106+
expect(team1).toBe(team2);
107+
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor, aux: }');
108+
})));
109+
110+
it("should support router links",
111+
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
112+
let fixture = tcb.createFakeAsync(RootCmp);
113+
advance(fixture);
114+
115+
router.navigateByUrl('/team/22/link');
116+
advance(fixture);
117+
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, aux: }');
118+
119+
let native = DOM.querySelector(fixture.debugElement.nativeElement, "a");
120+
expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple");
121+
DOM.dispatchEvent(native, DOM.createMouseEvent('click'));
122+
advance(fixture);
123+
124+
expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, aux: }');
125+
})));
126+
127+
it("should update router links when router changes",
128+
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
129+
let fixture = tcb.createFakeAsync(RootCmp);
130+
advance(fixture);
131+
132+
router.navigateByUrl('/team/22/link(simple)');
133+
advance(fixture);
134+
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, aux: simple }');
135+
136+
let native = DOM.querySelector(fixture.debugElement.nativeElement, "a");
137+
expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple(aux:simple)");
138+
139+
router.navigateByUrl('/team/22/link(simple2)');
140+
advance(fixture);
141+
142+
expect(DOM.getAttribute(native, "href")).toEqual("/team/33/simple(aux:simple2)");
143+
})));
122144
});
123145
}
124146

147+
function advance(fixture: ComponentFixture): void {
148+
tick();
149+
fixture.detectChanges();
150+
}
151+
125152
function compileRoot(tcb: TestComponentBuilder): Promise<ComponentFixture> {
126153
return tcb.createAsync(RootCmp);
127154
}
@@ -136,14 +163,28 @@ class UserCmp implements OnActivate {
136163
class SimpleCmp {
137164
}
138165

166+
@Component({selector: 'simple2-cmp', template: `simple2`})
167+
class Simple2Cmp {
168+
}
169+
170+
@Component({
171+
selector: 'link-cmp',
172+
template: `<a [routerLink]="['team', '33', 'simple']">link</a>`,
173+
directives: ROUTER_DIRECTIVES
174+
})
175+
class LinkCmp {
176+
}
177+
139178
@Component({
140179
selector: 'team-cmp',
141180
template: `team {{id}} { <router-outlet></router-outlet>, aux: <router-outlet name="aux"></router-outlet> }`,
142181
directives: [ROUTER_DIRECTIVES]
143182
})
144183
@Routes([
145184
new Route({path: 'user/:name', component: UserCmp}),
146-
new Route({path: 'simple', component: SimpleCmp})
185+
new Route({path: 'simple', component: SimpleCmp}),
186+
new Route({path: 'simple2', component: Simple2Cmp}),
187+
new Route({path: 'link', component: LinkCmp})
147188
])
148189
class TeamCmp implements OnActivate {
149190
id: string;

0 commit comments

Comments
 (0)