Skip to content

Commit 6e1fed4

Browse files
committed
feat(router): add Router and RouterOutlet to support aux routes
1 parent d35c109 commit 6e1fed4

3 files changed

Lines changed: 141 additions & 30 deletions

File tree

modules/angular2/src/alt_router/directives/router_outlet.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,35 @@ import {
33
Directive,
44
DynamicComponentLoader,
55
ViewContainerRef,
6-
Input,
6+
Attribute,
77
ComponentRef,
88
ComponentFactory,
9-
ReflectiveInjector
9+
ReflectiveInjector,
10+
OnInit
1011
} from 'angular2/core';
1112
import {RouterOutletMap} from '../router';
12-
import {isPresent} from 'angular2/src/facade/lang';
13+
import {DEFAULT_OUTLET_NAME} from '../constants';
14+
import {isPresent, isBlank} from 'angular2/src/facade/lang';
1315

1416
@Directive({selector: 'router-outlet'})
1517
export class RouterOutlet {
1618
private _loaded: ComponentRef;
1719
public outletMap: RouterOutletMap;
18-
@Input() name: string = "";
1920

20-
constructor(parentOutletMap: RouterOutletMap, private _location: ViewContainerRef) {
21-
parentOutletMap.registerOutlet("", this);
21+
constructor(parentOutletMap: RouterOutletMap, private _location: ViewContainerRef,
22+
@Attribute('name') name: string) {
23+
parentOutletMap.registerOutlet(isBlank(name) ? DEFAULT_OUTLET_NAME : name, this);
24+
}
25+
26+
unload(): void {
27+
this._loaded.destroy();
28+
this._loaded = null;
2229
}
2330

2431
load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[],
2532
outletMap: RouterOutletMap): ComponentRef {
2633
if (isPresent(this._loaded)) {
27-
this._loaded.destroy();
34+
this.unload();
2835
}
2936
this.outletMap = outletMap;
3037
let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector);
Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +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 {StringMapWrapper} from 'angular2/src/facade/collection';
5+
import {BaseException} from 'angular2/src/facade/exceptions';
46
import {RouterUrlParser} from './router_url_parser';
57
import {recognize} from './recognize';
6-
import {equalSegments, routeSegmentComponentFactory, RouteSegment, Tree} from './segments';
8+
import {
9+
equalSegments,
10+
routeSegmentComponentFactory,
11+
RouteSegment,
12+
Tree,
13+
rootNode,
14+
TreeNode
15+
} from './segments';
716
import {hasLifecycleHook} from './lifecycle_reflector';
17+
import {DEFAULT_OUTLET_NAME} from './constants';
818

919
export class RouterOutletMap {
1020
/** @internal */
@@ -18,38 +28,78 @@ export class Router {
1828
private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {}
1929

2030
navigateByUrl(url: string): Promise<void> {
21-
let urlSegmentTree = this._urlParser.parse(url.substring(1));
31+
let urlSegmentTree = this._urlParser.parse(url);
2232
return recognize(this._componentResolver, this._componentType, urlSegmentTree)
2333
.then(currTree => {
24-
let prevRoot = isPresent(this.prevTree) ? this.prevTree.root : null;
25-
_loadSegments(currTree, currTree.root, this.prevTree, prevRoot, this,
26-
this._routerOutletMap);
34+
let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null;
35+
new _SegmentLoader(currTree, this.prevTree)
36+
.loadSegments(rootNode(currTree), prevRoot, this._routerOutletMap);
2737
this.prevTree = currTree;
2838
});
2939
}
3040
}
3141

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];
42+
class _SegmentLoader {
43+
constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
3644

37-
let outletMap;
38-
if (equalSegments(curr, prev)) {
39-
outletMap = outlet.outletMap;
40-
} else {
41-
outletMap = new RouterOutletMap();
45+
loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
46+
parentOutletMap: RouterOutletMap): void {
47+
let curr = currNode.value;
48+
let prev = isPresent(prevNode) ? prevNode.value : null;
49+
let outlet = this.getOutlet(parentOutletMap, currNode.value);
50+
51+
if (equalSegments(curr, prev)) {
52+
this.loadChildSegments(currNode, prevNode, outlet.outletMap);
53+
} else {
54+
let outletMap = new RouterOutletMap();
55+
this.loadNewSegment(outletMap, curr, prev, outlet);
56+
this.loadChildSegments(currNode, prevNode, outletMap);
57+
}
58+
}
59+
60+
private loadNewSegment(outletMap: RouterOutletMap, curr: RouteSegment, prev: RouteSegment,
61+
outlet: RouterOutlet): void {
4262
let resolved = ReflectiveInjector.resolve(
4363
[provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]);
4464
let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap);
4565
if (hasLifecycleHook("routerOnActivate", ref.instance)) {
46-
ref.instance.routerOnActivate(curr, prev, currTree, prevTree);
66+
ref.instance.routerOnActivate(curr, prev, this.currTree, this.prevTree);
67+
}
68+
}
69+
70+
private loadChildSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
71+
outletMap: RouterOutletMap): void {
72+
let prevChildren = isPresent(prevNode) ?
73+
prevNode.children.reduce(
74+
(m, c) => {
75+
m[c.value.outlet] = c;
76+
return m;
77+
},
78+
{}) :
79+
{};
80+
81+
currNode.children.forEach(c => {
82+
this.loadSegments(c, prevChildren[c.value.outlet], outletMap);
83+
StringMapWrapper.delete(prevChildren, c.value.outlet);
84+
});
85+
86+
StringMapWrapper.forEach(prevChildren, (v, k) => this.unloadOutlet(outletMap._outlets[k]));
87+
}
88+
89+
private getOutlet(outletMap: RouterOutletMap, segment: RouteSegment): RouterOutlet {
90+
let outlet = outletMap._outlets[segment.outlet];
91+
if (isBlank(outlet)) {
92+
if (segment.outlet == DEFAULT_OUTLET_NAME) {
93+
throw new BaseException(`Cannot find default outlet`);
94+
} else {
95+
throw new BaseException(`Cannot find the outlet ${segment.outlet}`);
96+
}
4797
}
98+
return outlet;
4899
}
49100

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);
101+
private unloadOutlet(outlet: RouterOutlet): void {
102+
StringMapWrapper.forEach(outlet.outletMap._outlets, (v, k) => { this.unloadOutlet(v); });
103+
outlet.unload();
54104
}
55105
}

modules/angular2/test/alt_router/integration_spec.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,51 @@ export function main() {
4949
.then((_) => router.navigateByUrl('/team/22/user/victor'))
5050
.then((_) => {
5151
fixture.detectChanges();
52-
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello victor }');
52+
expect(fixture.debugElement.nativeElement)
53+
.toHaveText('team 22 { hello victor, aux: }');
54+
async.done();
55+
});
56+
}));
57+
58+
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+
}));
86+
87+
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('');
5397
async.done();
5498
});
5599
}));
@@ -68,10 +112,13 @@ export function main() {
68112
.then((_) => {
69113
fixture.detectChanges();
70114
expect(team1).toBe(team2);
71-
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor }');
115+
expect(fixture.debugElement.nativeElement)
116+
.toHaveText('team 22 { hello fedor, aux: }');
72117
async.done();
73118
});
74119
}));
120+
121+
// unload unused nodes
75122
});
76123
}
77124

@@ -85,12 +132,19 @@ class UserCmp implements OnActivate {
85132
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); }
86133
}
87134

135+
@Component({selector: 'simple-cmp', template: `simple`})
136+
class SimpleCmp {
137+
}
138+
88139
@Component({
89140
selector: 'team-cmp',
90-
template: `team {{id}} { <router-outlet></router-outlet> }`,
141+
template: `team {{id}} { <router-outlet></router-outlet>, aux: <router-outlet name="aux"></router-outlet> }`,
91142
directives: [ROUTER_DIRECTIVES]
92143
})
93-
@Routes([new Route({path: 'user/:name', component: UserCmp})])
144+
@Routes([
145+
new Route({path: 'user/:name', component: UserCmp}),
146+
new Route({path: 'simple', component: SimpleCmp})
147+
])
94148
class TeamCmp implements OnActivate {
95149
id: string;
96150
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.id = s.getParam('id'); }

0 commit comments

Comments
 (0)