X Tutup
Skip to content

Commit 5a897cf

Browse files
committed
feat(router): add Router and RouterOutlet
Closes angular#8173
1 parent ef67a0c commit 5a897cf

File tree

7 files changed

+230
-0
lines changed

7 files changed

+230
-0
lines changed

modules/angular2/alt_router.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @module
3+
* @description
4+
* Alternative implementation of the router. Experimental.
5+
*/
6+
7+
export {Router, RouterOutletMap} from './src/alt_router/router';
8+
export {RouteSegment} from './src/alt_router/segments';
9+
export {Routes} from './src/alt_router/metadata/decorators';
10+
export {Route} from './src/alt_router/metadata/metadata';
11+
export {RouterUrlParser, DefaultRouterUrlParser} from './src/alt_router/router_url_parser';
12+
export {OnActivate} from './src/alt_router/interfaces';
13+
14+
import {RouterOutlet} from './src/alt_router/directives/router_outlet';
15+
import {CONST_EXPR} from './src/facade/lang';
16+
17+
export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet]);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
ResolvedReflectiveProvider,
3+
Directive,
4+
DynamicComponentLoader,
5+
ViewContainerRef,
6+
Input,
7+
ComponentRef,
8+
ComponentFactory,
9+
ReflectiveInjector
10+
} from 'angular2/core';
11+
import {RouterOutletMap} from '../router';
12+
import {isPresent} from 'angular2/src/facade/lang';
13+
14+
@Directive({selector: 'router-outlet'})
15+
export class RouterOutlet {
16+
private _loaded: ComponentRef;
17+
public outletMap: RouterOutletMap;
18+
@Input() name: string = "";
19+
20+
constructor(parentOutletMap: RouterOutletMap, private _location: ViewContainerRef) {
21+
parentOutletMap.registerOutlet("", this);
22+
}
23+
24+
load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[],
25+
outletMap: RouterOutletMap): ComponentRef {
26+
if (isPresent(this._loaded)) {
27+
this._loaded.destroy();
28+
}
29+
this.outletMap = outletMap;
30+
let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector);
31+
this._loaded = this._location.createComponent(factory, this._location.length, inj, []);
32+
return this._loaded;
33+
}
34+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {RouteSegment, Tree} from './segments';
2+
3+
export interface OnActivate {
4+
routerOnActivate(curr: RouteSegment, prev?: RouteSegment, currTree?: Tree<RouteSegment>,
5+
prevTree?: Tree<RouteSegment>): void;
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import './interfaces.dart';
2+
bool hasLifecycleHook(String name, Object obj) {
3+
if (name == "routerOnActivate") return obj is OnActivate;
4+
return false;
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {Type} from 'angular2/src/facade/lang';
2+
3+
export function hasLifecycleHook(name: string, obj: Object): boolean {
4+
let type = obj.constructor;
5+
if (!(type instanceof Type)) return false;
6+
return name in(<any>type).prototype;
7+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {provide, ReflectiveInjector, ComponentResolver} from 'angular2/core';
2+
import {RouterOutlet} from './directives/router_outlet';
3+
import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
4+
import {RouterUrlParser} from './router_url_parser';
5+
import {recognize} from './recognize';
6+
import {equalSegments, routeSegmentComponentFactory, RouteSegment, Tree} from './segments';
7+
import {hasLifecycleHook} from './lifecycle_reflector';
8+
9+
export class RouterOutletMap {
10+
/** @internal */
11+
_outlets: {[name: string]: RouterOutlet} = {};
12+
registerOutlet(name: string, outlet: RouterOutlet): void { this._outlets[name] = outlet; }
13+
}
14+
15+
export class Router {
16+
private prevTree: Tree<RouteSegment>;
17+
constructor(private _componentType: Type, private _componentResolver: ComponentResolver,
18+
private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {}
19+
20+
navigateByUrl(url: string): Promise<void> {
21+
let urlSegmentTree = this._urlParser.parse(url.substring(1));
22+
return recognize(this._componentResolver, this._componentType, urlSegmentTree)
23+
.then(currTree => {
24+
let prevRoot = isPresent(this.prevTree) ? this.prevTree.root : null;
25+
_loadSegments(currTree, currTree.root, this.prevTree, prevRoot, this,
26+
this._routerOutletMap);
27+
this.prevTree = currTree;
28+
});
29+
}
30+
}
31+
32+
function _loadSegments(currTree: Tree<RouteSegment>, curr: RouteSegment,
33+
prevTree: Tree<RouteSegment>, prev: RouteSegment, router: Router,
34+
parentOutletMap: RouterOutletMap): void {
35+
let outlet = parentOutletMap._outlets[curr.outlet];
36+
37+
let outletMap;
38+
if (equalSegments(curr, prev)) {
39+
outletMap = outlet.outletMap;
40+
} else {
41+
outletMap = new RouterOutletMap();
42+
let resolved = ReflectiveInjector.resolve(
43+
[provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]);
44+
let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap);
45+
if (hasLifecycleHook("routerOnActivate", ref.instance)) {
46+
ref.instance.routerOnActivate(curr, prev, currTree, prevTree);
47+
}
48+
}
49+
50+
if (isPresent(currTree.firstChild(curr))) {
51+
let cc = currTree.firstChild(curr);
52+
let pc = isBlank(prevTree) ? null : prevTree.firstChild(prev);
53+
_loadSegments(currTree, cc, prevTree, pc, router, outletMap);
54+
}
55+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {
2+
ComponentFixture,
3+
AsyncTestCompleter,
4+
TestComponentBuilder,
5+
beforeEach,
6+
ddescribe,
7+
xdescribe,
8+
describe,
9+
el,
10+
expect,
11+
iit,
12+
inject,
13+
beforeEachProviders,
14+
it,
15+
xit
16+
} from 'angular2/testing_internal';
17+
import {provide, Component, ComponentResolver} from 'angular2/core';
18+
19+
import {
20+
Router,
21+
RouterOutletMap,
22+
RouteSegment,
23+
Route,
24+
ROUTER_DIRECTIVES,
25+
Routes,
26+
RouterUrlParser,
27+
DefaultRouterUrlParser,
28+
OnActivate
29+
} from 'angular2/alt_router';
30+
31+
export function main() {
32+
describe('navigation', () => {
33+
beforeEachProviders(() => [
34+
provide(RouterUrlParser, {useClass: DefaultRouterUrlParser}),
35+
RouterOutletMap,
36+
provide(Router,
37+
{
38+
useFactory: (resolver, urlParser, outletMap) =>
39+
new Router(RootCmp, resolver, urlParser, outletMap),
40+
deps: [ComponentResolver, RouterUrlParser, RouterOutletMap]
41+
})
42+
]);
43+
44+
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).toHaveText('team 22 { hello victor }');
53+
async.done();
54+
});
55+
}));
56+
57+
it('should update nested routes when url changes',
58+
inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => {
59+
let fixture;
60+
let team1;
61+
let team2;
62+
compileRoot(tcb)
63+
.then((rtc) => {fixture = rtc})
64+
.then((_) => router.navigateByUrl('/team/22/user/victor'))
65+
.then((_) => { team1 = fixture.debugElement.children[1].componentInstance; })
66+
.then((_) => router.navigateByUrl('/team/22/user/fedor'))
67+
.then((_) => { team2 = fixture.debugElement.children[1].componentInstance; })
68+
.then((_) => {
69+
fixture.detectChanges();
70+
expect(team1).toBe(team2);
71+
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor }');
72+
async.done();
73+
});
74+
}));
75+
});
76+
}
77+
78+
function compileRoot(tcb: TestComponentBuilder): Promise<ComponentFixture> {
79+
return tcb.createAsync(RootCmp);
80+
}
81+
82+
@Component({selector: 'user-cmp', template: `hello {{user}}`})
83+
class UserCmp implements OnActivate {
84+
user: string;
85+
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); }
86+
}
87+
88+
@Component({
89+
selector: 'team-cmp',
90+
template: `team {{id}} { <router-outlet></router-outlet> }`,
91+
directives: [ROUTER_DIRECTIVES]
92+
})
93+
@Routes([new Route({path: 'user/:name', component: UserCmp})])
94+
class TeamCmp implements OnActivate {
95+
id: string;
96+
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.id = s.getParam('id'); }
97+
}
98+
99+
@Component({
100+
selector: 'root-cmp',
101+
template: `<router-outlet></router-outlet>`,
102+
directives: [ROUTER_DIRECTIVES]
103+
})
104+
@Routes([new Route({path: 'team/:id', component: TeamCmp})])
105+
class RootCmp {
106+
}

0 commit comments

Comments
 (0)
X Tutup