Skip to content

Commit e5b87e5

Browse files
committed
feat(router): implement relative navigation
1 parent d097784 commit e5b87e5

File tree

10 files changed

+295
-80
lines changed

10 files changed

+295
-80
lines changed

modules/angular2/alt_router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export {
1212
RouterUrlSerializer,
1313
DefaultRouterUrlSerializer
1414
} from './src/alt_router/router_url_serializer';
15-
export {OnActivate} from './src/alt_router/interfaces';
15+
export {OnActivate, CanDeactivate} from './src/alt_router/interfaces';
1616
export {ROUTER_PROVIDERS} from './src/alt_router/router_providers';
1717

1818
import {RouterOutlet} from './src/alt_router/directives/router_outlet';

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,25 @@ import {
1111
HostListener,
1212
HostBinding,
1313
Input,
14-
OnDestroy
14+
OnDestroy,
15+
Optional
1516
} from 'angular2/core';
1617
import {RouterOutletMap, Router} from '../router';
1718
import {RouteSegment, UrlSegment, Tree} from '../segments';
18-
import {link} from '../link';
19-
import {isString} from 'angular2/src/facade/lang';
19+
import {isString, isPresent} from 'angular2/src/facade/lang';
2020
import {ObservableWrapper} from 'angular2/src/facade/async';
2121

2222
@Directive({selector: '[routerLink]'})
2323
export class RouterLink implements OnDestroy {
2424
@Input() target: string;
2525
private _changes: any[] = [];
26-
private _targetUrl: Tree<UrlSegment>;
2726
private _subscription: any;
2827

2928
@HostBinding() private href: string;
3029

31-
constructor(private _router: Router) {
32-
this._subscription = ObservableWrapper.subscribe(_router.changes, (_) => {
33-
this._targetUrl = _router.urlTree;
34-
this._updateTargetUrlAndHref();
35-
});
30+
constructor(@Optional() private _routeSegment: RouteSegment, private _router: Router) {
31+
this._subscription =
32+
ObservableWrapper.subscribe(_router.changes, (_) => { this._updateTargetUrlAndHref(); });
3633
}
3734

3835
ngOnDestroy() { ObservableWrapper.dispose(this._subscription); }
@@ -46,14 +43,16 @@ export class RouterLink implements OnDestroy {
4643
@HostListener("click")
4744
onClick(): boolean {
4845
if (!isString(this.target) || this.target == '_self') {
49-
this._router.navigate(this._targetUrl);
46+
this._router.navigate(this._changes, this._routeSegment);
5047
return false;
5148
}
5249
return true;
5350
}
5451

5552
private _updateTargetUrlAndHref(): void {
56-
this._targetUrl = link(null, this._router.urlTree, this._changes);
57-
this.href = this._router.serializeUrl(this._targetUrl);
53+
let tree = this._router.createUrlTree(this._changes, this._routeSegment);
54+
if (isPresent(tree)) {
55+
this.href = this._router.serializeUrl(tree);
56+
}
5857
}
5958
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ export class RouterOutlet {
2828
this._loaded = null;
2929
}
3030

31+
get loadedComponent(): Object { return isPresent(this._loaded) ? this._loaded.instance : null; }
32+
33+
get isLoaded(): boolean { return isPresent(this._loaded); }
34+
3135
load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[],
3236
outletMap: RouterOutletMap): ComponentRef {
33-
if (isPresent(this._loaded)) {
34-
this.unload();
35-
}
3637
this.outletMap = outletMap;
3738
let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector);
3839
this._loaded = this._location.createComponent(factory, this._location.length, inj, []);

modules/angular2/src/alt_router/link.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,64 @@
11
import {Tree, TreeNode, UrlSegment, RouteSegment, rootNode} from './segments';
2-
import {isBlank, isString, isStringMap} from 'angular2/src/facade/lang';
2+
import {isBlank, isPresent, isString, isStringMap} from 'angular2/src/facade/lang';
33
import {ListWrapper} from 'angular2/src/facade/collection';
44

5-
export function link(segment: RouteSegment, tree: Tree<UrlSegment>,
6-
change: any[]): Tree<UrlSegment> {
7-
if (change.length === 0) return tree;
8-
let normalizedChange = (change.length === 1 && change[0] == "/") ? change : ["/"].concat(change);
9-
return new Tree<UrlSegment>(_update(rootNode(tree), normalizedChange));
5+
export function link(segment: RouteSegment, routeTree: Tree<RouteSegment>,
6+
urlTree: Tree<UrlSegment>, change: any[]): Tree<UrlSegment> {
7+
if (change.length === 0) return urlTree;
8+
9+
let startingNode;
10+
let normalizedChange;
11+
12+
if (isString(change[0]) && change[0].startsWith("./")) {
13+
normalizedChange = ["/", change[0].substring(2)].concat(change.slice(1));
14+
startingNode = _findStartingNode(_findUrlSegment(segment, routeTree), rootNode(urlTree));
15+
16+
} else if (isString(change[0]) && change.length === 1 && change[0] == "/") {
17+
normalizedChange = change;
18+
startingNode = rootNode(urlTree);
19+
20+
} else if (isString(change[0]) && !change[0].startsWith("/")) {
21+
normalizedChange = ["/"].concat(change);
22+
startingNode = _findStartingNode(_findUrlSegment(segment, routeTree), rootNode(urlTree));
23+
24+
} else {
25+
normalizedChange = ["/"].concat(change);
26+
startingNode = rootNode(urlTree);
27+
}
28+
29+
let updated = _update(startingNode, normalizedChange);
30+
let newRoot = _constructNewTree(rootNode(urlTree), startingNode, updated);
31+
32+
return new Tree<UrlSegment>(newRoot);
33+
}
34+
35+
function _findUrlSegment(segment: RouteSegment, routeTree: Tree<RouteSegment>): UrlSegment {
36+
let s = segment;
37+
let res = null;
38+
while (isBlank(res)) {
39+
res = ListWrapper.last(s.urlSegments);
40+
s = routeTree.parent(s);
41+
}
42+
return res;
43+
}
44+
45+
function _findStartingNode(segment: UrlSegment, node: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
46+
if (node.value === segment) return node;
47+
for (var c of node.children) {
48+
let r = _findStartingNode(segment, c);
49+
if (isPresent(r)) return r;
50+
}
51+
return null;
52+
}
53+
54+
function _constructNewTree(node: TreeNode<UrlSegment>, original: TreeNode<UrlSegment>,
55+
updated: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
56+
if (node === original) {
57+
return new TreeNode<UrlSegment>(node.value, updated.children);
58+
} else {
59+
return new TreeNode<UrlSegment>(
60+
node.value, node.children.map(c => _constructNewTree(c, original, updated)));
61+
}
1062
}
1163

1264
function _update(node: TreeNode<UrlSegment>, changes: any[]): TreeNode<UrlSegment> {

modules/angular2/src/alt_router/recognize.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {ComponentResolver} from 'angular2/core';
88
import {DEFAULT_OUTLET_NAME} from './constants';
99
import {reflector} from 'angular2/src/core/reflection/reflection';
1010

11+
// TODO: vsavkin: recognize should take the old tree and merge it
1112
export function recognize(componentResolver: ComponentResolver, type: Type,
1213
url: Tree<UrlSegment>): Promise<Tree<RouteSegment>> {
1314
let matched = new _MatchResult(type, [url.root], null, rootNode(url).children, []);
Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import {OnInit, 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';
4+
import {ListWrapper} from 'angular2/src/facade/collection';
5+
import {EventEmitter, Observable, PromiseWrapper} from 'angular2/src/facade/async';
56
import {StringMapWrapper} from 'angular2/src/facade/collection';
67
import {BaseException} from 'angular2/src/facade/exceptions';
78
import {RouterUrlSerializer} from './router_url_serializer';
9+
import {CanDeactivate} from './interfaces';
810
import {recognize} from './recognize';
911
import {Location} from 'angular2/platform/common';
12+
import {link} from './link';
13+
1014
import {
1115
equalSegments,
1216
routeSegmentComponentFactory,
@@ -32,70 +36,98 @@ export class Router {
3236

3337
private _changes: EventEmitter<void> = new EventEmitter<void>();
3438

35-
constructor(private _componentType: Type, private _componentResolver: ComponentResolver,
39+
constructor(private _rootComponent: Object, private _rootComponentType: Type,
40+
private _componentResolver: ComponentResolver,
3641
private _urlSerializer: RouterUrlSerializer,
3742
private _routerOutletMap: RouterOutletMap, private _location: Location) {
3843
this.navigateByUrl(this._location.path());
3944
}
4045

4146
get urlTree(): Tree<UrlSegment> { return this._urlTree; }
4247

43-
navigate(url: Tree<UrlSegment>): Promise<void> {
48+
navigateByUrl(url: string): Promise<void> {
49+
return this._navigate(this._urlSerializer.parse(url));
50+
}
51+
52+
navigate(changes: any[], segment?: RouteSegment): Promise<void> {
53+
return this._navigate(this.createUrlTree(changes, segment));
54+
}
55+
56+
private _navigate(url: Tree<UrlSegment>): Promise<void> {
4457
this._urlTree = url;
45-
return recognize(this._componentResolver, this._componentType, url)
58+
return recognize(this._componentResolver, this._rootComponentType, url)
4659
.then(currTree => {
47-
new _LoadSegments(currTree, this._prevTree).load(this._routerOutletMap);
48-
this._prevTree = currTree;
49-
this._location.go(this._urlSerializer.serialize(this._urlTree));
50-
this._changes.emit(null);
60+
return new _LoadSegments(currTree, this._prevTree)
61+
.load(this._routerOutletMap, this._rootComponent)
62+
.then(_ => {
63+
this._prevTree = currTree;
64+
this._location.go(this._urlSerializer.serialize(this._urlTree));
65+
this._changes.emit(null);
66+
});
5167
});
5268
}
5369

54-
serializeUrl(url: Tree<UrlSegment>): string { return this._urlSerializer.serialize(url); }
55-
56-
navigateByUrl(url: string): Promise<void> {
57-
return this.navigate(this._urlSerializer.parse(url));
70+
createUrlTree(changes: any[], segment?: RouteSegment): Tree<UrlSegment> {
71+
if (isPresent(this._prevTree)) {
72+
let s = isPresent(segment) ? segment : this._prevTree.root;
73+
return link(s, this._prevTree, this.urlTree, changes);
74+
} else {
75+
return null;
76+
}
5877
}
5978

79+
serializeUrl(url: Tree<UrlSegment>): string { return this._urlSerializer.serialize(url); }
80+
6081
get changes(): Observable<void> { return this._changes; }
82+
83+
get routeTree(): Tree<RouteSegment> { return this._prevTree; }
6184
}
6285

86+
6387
class _LoadSegments {
88+
private deactivations: Object[][] = [];
89+
private performMutation: boolean = true;
90+
6491
constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
6592

66-
load(parentOutletMap: RouterOutletMap): void {
93+
load(parentOutletMap: RouterOutletMap, rootComponent: Object): Promise<void> {
6794
let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null;
6895
let currRoot = rootNode(this.currTree);
69-
this.loadChildSegments(currRoot, prevRoot, parentOutletMap);
96+
97+
return this.canDeactivate(currRoot, prevRoot, parentOutletMap, rootComponent)
98+
.then(res => {
99+
this.performMutation = true;
100+
if (res) {
101+
this.loadChildSegments(currRoot, prevRoot, parentOutletMap, [rootComponent]);
102+
}
103+
});
70104
}
71105

72-
loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
73-
parentOutletMap: RouterOutletMap): void {
74-
let curr = currNode.value;
75-
let prev = isPresent(prevNode) ? prevNode.value : null;
76-
let outlet = this.getOutlet(parentOutletMap, currNode.value);
106+
private canDeactivate(currRoot: TreeNode<RouteSegment>, prevRoot: TreeNode<RouteSegment>,
107+
outletMap: RouterOutletMap, rootComponent: Object): Promise<boolean> {
108+
this.performMutation = false;
109+
this.loadChildSegments(currRoot, prevRoot, outletMap, [rootComponent]);
77110

78-
if (equalSegments(curr, prev)) {
79-
this.loadChildSegments(currNode, prevNode, outlet.outletMap);
80-
} else {
81-
let outletMap = new RouterOutletMap();
82-
this.loadNewSegment(outletMap, curr, prev, outlet);
83-
this.loadChildSegments(currNode, prevNode, outletMap);
84-
}
111+
let allPaths = PromiseWrapper.all(this.deactivations.map(r => this.checkCanDeactivatePath(r)));
112+
return allPaths.then((values: boolean[]) => values.filter(v => v).length === values.length);
85113
}
86114

87-
private loadNewSegment(outletMap: RouterOutletMap, curr: RouteSegment, prev: RouteSegment,
88-
outlet: RouterOutlet): void {
89-
let resolved = ReflectiveInjector.resolve(
90-
[provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]);
91-
let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap);
92-
if (hasLifecycleHook("routerOnActivate", ref.instance)) {
93-
ref.instance.routerOnActivate(curr, prev, this.currTree, this.prevTree);
115+
private checkCanDeactivatePath(path: Object[]): Promise<boolean> {
116+
let curr = PromiseWrapper.resolve(true);
117+
for (let p of ListWrapper.reversed(path)) {
118+
curr = curr.then(_ => {
119+
if (hasLifecycleHook("routerCanDeactivate", p)) {
120+
return (<CanDeactivate>p).routerCanDeactivate(this.prevTree, this.currTree);
121+
} else {
122+
return _;
123+
}
124+
});
94125
}
126+
return curr;
95127
}
96128

97129
private loadChildSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
98-
outletMap: RouterOutletMap): void {
130+
outletMap: RouterOutletMap, components: Object[]): void {
99131
let prevChildren = isPresent(prevNode) ?
100132
prevNode.children.reduce(
101133
(m, c) => {
@@ -106,11 +138,42 @@ class _LoadSegments {
106138
{};
107139

108140
currNode.children.forEach(c => {
109-
this.loadSegments(c, prevChildren[c.value.outlet], outletMap);
141+
this.loadSegments(c, prevChildren[c.value.outlet], outletMap, components);
110142
StringMapWrapper.delete(prevChildren, c.value.outlet);
111143
});
112144

113-
StringMapWrapper.forEach(prevChildren, (v, k) => this.unloadOutlet(outletMap._outlets[k]));
145+
StringMapWrapper.forEach(prevChildren,
146+
(v, k) => this.unloadOutlet(outletMap._outlets[k], components));
147+
}
148+
149+
loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
150+
parentOutletMap: RouterOutletMap, components: Object[]): void {
151+
let curr = currNode.value;
152+
let prev = isPresent(prevNode) ? prevNode.value : null;
153+
let outlet = this.getOutlet(parentOutletMap, currNode.value);
154+
155+
if (equalSegments(curr, prev)) {
156+
this.loadChildSegments(currNode, prevNode, outlet.outletMap,
157+
components.concat([outlet.loadedComponent]));
158+
} else {
159+
this.unloadOutlet(outlet, components);
160+
if (this.performMutation) {
161+
let outletMap = new RouterOutletMap();
162+
let loadedComponent = this.loadNewSegment(outletMap, curr, prev, outlet);
163+
this.loadChildSegments(currNode, prevNode, outletMap, components.concat([loadedComponent]));
164+
}
165+
}
166+
}
167+
168+
private loadNewSegment(outletMap: RouterOutletMap, curr: RouteSegment, prev: RouteSegment,
169+
outlet: RouterOutlet): Object {
170+
let resolved = ReflectiveInjector.resolve(
171+
[provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]);
172+
let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap);
173+
if (hasLifecycleHook("routerOnActivate", ref.instance)) {
174+
ref.instance.routerOnActivate(curr, prev, this.currTree, this.prevTree);
175+
}
176+
return ref.instance;
114177
}
115178

116179
private getOutlet(outletMap: RouterOutletMap, segment: RouteSegment): RouterOutlet {
@@ -125,8 +188,15 @@ class _LoadSegments {
125188
return outlet;
126189
}
127190

128-
private unloadOutlet(outlet: RouterOutlet): void {
129-
StringMapWrapper.forEach(outlet.outletMap._outlets, (v, k) => { this.unloadOutlet(v); });
130-
outlet.unload();
191+
private unloadOutlet(outlet: RouterOutlet, components: Object[]): void {
192+
if (outlet.isLoaded) {
193+
StringMapWrapper.forEach(outlet.outletMap._outlets,
194+
(v, k) => this.unloadOutlet(v, components));
195+
if (this.performMutation) {
196+
outlet.unload();
197+
} else {
198+
this.deactivations.push(components.concat([outlet.loadedComponent]));
199+
}
200+
}
131201
}
132202
}

modules/angular2/src/alt_router/router_providers_common.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function routerFactory(app: ApplicationRef, componentResolver: ComponentResolver
2323
if (app.componentTypes.length == 0) {
2424
throw new BaseException("Bootstrap at least one component before injecting Router.");
2525
}
26-
return new Router(app.componentTypes[0], componentResolver, urlSerializer, routerOutletMap,
26+
// TODO: vsavkin this should not be null
27+
return new Router(null, app.componentTypes[0], componentResolver, urlSerializer, routerOutletMap,
2728
location);
2829
}

0 commit comments

Comments
 (0)