Skip to content

Commit

Permalink
feat(router): implement relative navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
vsavkin committed Apr 30, 2016
1 parent d097784 commit e5b87e5
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 80 deletions.
2 changes: 1 addition & 1 deletion modules/angular2/alt_router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export {
RouterUrlSerializer,
DefaultRouterUrlSerializer
} from './src/alt_router/router_url_serializer';
export {OnActivate} from './src/alt_router/interfaces';
export {OnActivate, CanDeactivate} from './src/alt_router/interfaces';
export {ROUTER_PROVIDERS} from './src/alt_router/router_providers';

import {RouterOutlet} from './src/alt_router/directives/router_outlet';
Expand Down
23 changes: 11 additions & 12 deletions modules/angular2/src/alt_router/directives/router_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,25 @@ import {
HostListener,
HostBinding,
Input,
OnDestroy
OnDestroy,
Optional
} from 'angular2/core';
import {RouterOutletMap, Router} from '../router';
import {RouteSegment, UrlSegment, Tree} from '../segments';
import {link} from '../link';
import {isString} from 'angular2/src/facade/lang';
import {isString, isPresent} from 'angular2/src/facade/lang';
import {ObservableWrapper} from 'angular2/src/facade/async';

@Directive({selector: '[routerLink]'})
export class RouterLink implements OnDestroy {
@Input() target: string;
private _changes: any[] = [];
private _targetUrl: Tree<UrlSegment>;
private _subscription: any;

@HostBinding() private href: string;

constructor(private _router: Router) {
this._subscription = ObservableWrapper.subscribe(_router.changes, (_) => {
this._targetUrl = _router.urlTree;
this._updateTargetUrlAndHref();
});
constructor(@Optional() private _routeSegment: RouteSegment, private _router: Router) {
this._subscription =
ObservableWrapper.subscribe(_router.changes, (_) => { this._updateTargetUrlAndHref(); });
}

ngOnDestroy() { ObservableWrapper.dispose(this._subscription); }
Expand All @@ -46,14 +43,16 @@ export class RouterLink implements OnDestroy {
@HostListener("click")
onClick(): boolean {
if (!isString(this.target) || this.target == '_self') {
this._router.navigate(this._targetUrl);
this._router.navigate(this._changes, this._routeSegment);
return false;
}
return true;
}

private _updateTargetUrlAndHref(): void {
this._targetUrl = link(null, this._router.urlTree, this._changes);
this.href = this._router.serializeUrl(this._targetUrl);
let tree = this._router.createUrlTree(this._changes, this._routeSegment);
if (isPresent(tree)) {
this.href = this._router.serializeUrl(tree);
}
}
}
7 changes: 4 additions & 3 deletions modules/angular2/src/alt_router/directives/router_outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ export class RouterOutlet {
this._loaded = null;
}

get loadedComponent(): Object { return isPresent(this._loaded) ? this._loaded.instance : null; }

get isLoaded(): boolean { return isPresent(this._loaded); }

load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[],
outletMap: RouterOutletMap): ComponentRef {
if (isPresent(this._loaded)) {
this.unload();
}
this.outletMap = outletMap;
let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector);
this._loaded = this._location.createComponent(factory, this._location.length, inj, []);
Expand Down
64 changes: 58 additions & 6 deletions modules/angular2/src/alt_router/link.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,64 @@
import {Tree, TreeNode, UrlSegment, RouteSegment, rootNode} from './segments';
import {isBlank, isString, isStringMap} from 'angular2/src/facade/lang';
import {isBlank, isPresent, isString, isStringMap} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';

export function link(segment: RouteSegment, tree: Tree<UrlSegment>,
change: any[]): Tree<UrlSegment> {
if (change.length === 0) return tree;
let normalizedChange = (change.length === 1 && change[0] == "/") ? change : ["/"].concat(change);
return new Tree<UrlSegment>(_update(rootNode(tree), normalizedChange));
export function link(segment: RouteSegment, routeTree: Tree<RouteSegment>,
urlTree: Tree<UrlSegment>, change: any[]): Tree<UrlSegment> {
if (change.length === 0) return urlTree;

let startingNode;
let normalizedChange;

if (isString(change[0]) && change[0].startsWith("./")) {
normalizedChange = ["/", change[0].substring(2)].concat(change.slice(1));
startingNode = _findStartingNode(_findUrlSegment(segment, routeTree), rootNode(urlTree));

} else if (isString(change[0]) && change.length === 1 && change[0] == "/") {
normalizedChange = change;
startingNode = rootNode(urlTree);

} else if (isString(change[0]) && !change[0].startsWith("/")) {
normalizedChange = ["/"].concat(change);
startingNode = _findStartingNode(_findUrlSegment(segment, routeTree), rootNode(urlTree));

} else {
normalizedChange = ["/"].concat(change);
startingNode = rootNode(urlTree);
}

let updated = _update(startingNode, normalizedChange);
let newRoot = _constructNewTree(rootNode(urlTree), startingNode, updated);

return new Tree<UrlSegment>(newRoot);
}

function _findUrlSegment(segment: RouteSegment, routeTree: Tree<RouteSegment>): UrlSegment {
let s = segment;
let res = null;
while (isBlank(res)) {
res = ListWrapper.last(s.urlSegments);
s = routeTree.parent(s);
}
return res;
}

function _findStartingNode(segment: UrlSegment, node: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
if (node.value === segment) return node;
for (var c of node.children) {
let r = _findStartingNode(segment, c);
if (isPresent(r)) return r;
}
return null;
}

function _constructNewTree(node: TreeNode<UrlSegment>, original: TreeNode<UrlSegment>,
updated: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
if (node === original) {
return new TreeNode<UrlSegment>(node.value, updated.children);
} else {
return new TreeNode<UrlSegment>(
node.value, node.children.map(c => _constructNewTree(c, original, updated)));
}
}

function _update(node: TreeNode<UrlSegment>, changes: any[]): TreeNode<UrlSegment> {
Expand Down
1 change: 1 addition & 0 deletions modules/angular2/src/alt_router/recognize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ComponentResolver} from 'angular2/core';
import {DEFAULT_OUTLET_NAME} from './constants';
import {reflector} from 'angular2/src/core/reflection/reflection';

// TODO: vsavkin: recognize should take the old tree and merge it
export function recognize(componentResolver: ComponentResolver, type: Type,
url: Tree<UrlSegment>): Promise<Tree<RouteSegment>> {
let matched = new _MatchResult(type, [url.root], null, rootNode(url).children, []);
Expand Down
148 changes: 109 additions & 39 deletions modules/angular2/src/alt_router/router.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import {OnInit, provide, ReflectiveInjector, ComponentResolver} from 'angular2/core';
import {RouterOutlet} from './directives/router_outlet';
import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
import {EventEmitter, Observable} from 'angular2/src/facade/async';
import {ListWrapper} from 'angular2/src/facade/collection';
import {EventEmitter, Observable, PromiseWrapper} from 'angular2/src/facade/async';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {BaseException} from 'angular2/src/facade/exceptions';
import {RouterUrlSerializer} from './router_url_serializer';
import {CanDeactivate} from './interfaces';
import {recognize} from './recognize';
import {Location} from 'angular2/platform/common';
import {link} from './link';

import {
equalSegments,
routeSegmentComponentFactory,
Expand All @@ -32,70 +36,98 @@ export class Router {

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

constructor(private _componentType: Type, private _componentResolver: ComponentResolver,
constructor(private _rootComponent: Object, private _rootComponentType: Type,
private _componentResolver: ComponentResolver,
private _urlSerializer: RouterUrlSerializer,
private _routerOutletMap: RouterOutletMap, private _location: Location) {
this.navigateByUrl(this._location.path());
}

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

navigate(url: Tree<UrlSegment>): Promise<void> {
navigateByUrl(url: string): Promise<void> {
return this._navigate(this._urlSerializer.parse(url));
}

navigate(changes: any[], segment?: RouteSegment): Promise<void> {
return this._navigate(this.createUrlTree(changes, segment));
}

private _navigate(url: Tree<UrlSegment>): Promise<void> {
this._urlTree = url;
return recognize(this._componentResolver, this._componentType, url)
return recognize(this._componentResolver, this._rootComponentType, url)
.then(currTree => {
new _LoadSegments(currTree, this._prevTree).load(this._routerOutletMap);
this._prevTree = currTree;
this._location.go(this._urlSerializer.serialize(this._urlTree));
this._changes.emit(null);
return new _LoadSegments(currTree, this._prevTree)
.load(this._routerOutletMap, this._rootComponent)
.then(_ => {
this._prevTree = currTree;
this._location.go(this._urlSerializer.serialize(this._urlTree));
this._changes.emit(null);
});
});
}

serializeUrl(url: Tree<UrlSegment>): string { return this._urlSerializer.serialize(url); }

navigateByUrl(url: string): Promise<void> {
return this.navigate(this._urlSerializer.parse(url));
createUrlTree(changes: any[], segment?: RouteSegment): Tree<UrlSegment> {
if (isPresent(this._prevTree)) {
let s = isPresent(segment) ? segment : this._prevTree.root;
return link(s, this._prevTree, this.urlTree, changes);
} else {
return null;
}
}

serializeUrl(url: Tree<UrlSegment>): string { return this._urlSerializer.serialize(url); }

get changes(): Observable<void> { return this._changes; }

get routeTree(): Tree<RouteSegment> { return this._prevTree; }
}


class _LoadSegments {
private deactivations: Object[][] = [];
private performMutation: boolean = true;

constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}

load(parentOutletMap: RouterOutletMap): void {
load(parentOutletMap: RouterOutletMap, rootComponent: Object): Promise<void> {
let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null;
let currRoot = rootNode(this.currTree);
this.loadChildSegments(currRoot, prevRoot, parentOutletMap);

return this.canDeactivate(currRoot, prevRoot, parentOutletMap, rootComponent)
.then(res => {
this.performMutation = true;
if (res) {
this.loadChildSegments(currRoot, prevRoot, parentOutletMap, [rootComponent]);
}
});
}

loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
parentOutletMap: RouterOutletMap): void {
let curr = currNode.value;
let prev = isPresent(prevNode) ? prevNode.value : null;
let outlet = this.getOutlet(parentOutletMap, currNode.value);
private canDeactivate(currRoot: TreeNode<RouteSegment>, prevRoot: TreeNode<RouteSegment>,
outletMap: RouterOutletMap, rootComponent: Object): Promise<boolean> {
this.performMutation = false;
this.loadChildSegments(currRoot, prevRoot, outletMap, [rootComponent]);

if (equalSegments(curr, prev)) {
this.loadChildSegments(currNode, prevNode, outlet.outletMap);
} else {
let outletMap = new RouterOutletMap();
this.loadNewSegment(outletMap, curr, prev, outlet);
this.loadChildSegments(currNode, prevNode, outletMap);
}
let allPaths = PromiseWrapper.all(this.deactivations.map(r => this.checkCanDeactivatePath(r)));
return allPaths.then((values: boolean[]) => values.filter(v => v).length === values.length);
}

private loadNewSegment(outletMap: RouterOutletMap, curr: RouteSegment, prev: RouteSegment,
outlet: RouterOutlet): void {
let resolved = ReflectiveInjector.resolve(
[provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]);
let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap);
if (hasLifecycleHook("routerOnActivate", ref.instance)) {
ref.instance.routerOnActivate(curr, prev, this.currTree, this.prevTree);
private checkCanDeactivatePath(path: Object[]): Promise<boolean> {
let curr = PromiseWrapper.resolve(true);
for (let p of ListWrapper.reversed(path)) {
curr = curr.then(_ => {
if (hasLifecycleHook("routerCanDeactivate", p)) {
return (<CanDeactivate>p).routerCanDeactivate(this.prevTree, this.currTree);
} else {
return _;
}
});
}
return curr;
}

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

currNode.children.forEach(c => {
this.loadSegments(c, prevChildren[c.value.outlet], outletMap);
this.loadSegments(c, prevChildren[c.value.outlet], outletMap, components);
StringMapWrapper.delete(prevChildren, c.value.outlet);
});

StringMapWrapper.forEach(prevChildren, (v, k) => this.unloadOutlet(outletMap._outlets[k]));
StringMapWrapper.forEach(prevChildren,
(v, k) => this.unloadOutlet(outletMap._outlets[k], components));
}

loadSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
parentOutletMap: RouterOutletMap, components: Object[]): void {
let curr = currNode.value;
let prev = isPresent(prevNode) ? prevNode.value : null;
let outlet = this.getOutlet(parentOutletMap, currNode.value);

if (equalSegments(curr, prev)) {
this.loadChildSegments(currNode, prevNode, outlet.outletMap,
components.concat([outlet.loadedComponent]));
} else {
this.unloadOutlet(outlet, components);
if (this.performMutation) {
let outletMap = new RouterOutletMap();
let loadedComponent = this.loadNewSegment(outletMap, curr, prev, outlet);
this.loadChildSegments(currNode, prevNode, outletMap, components.concat([loadedComponent]));
}
}
}

private loadNewSegment(outletMap: RouterOutletMap, curr: RouteSegment, prev: RouteSegment,
outlet: RouterOutlet): Object {
let resolved = ReflectiveInjector.resolve(
[provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]);
let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap);
if (hasLifecycleHook("routerOnActivate", ref.instance)) {
ref.instance.routerOnActivate(curr, prev, this.currTree, this.prevTree);
}
return ref.instance;
}

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

private unloadOutlet(outlet: RouterOutlet): void {
StringMapWrapper.forEach(outlet.outletMap._outlets, (v, k) => { this.unloadOutlet(v); });
outlet.unload();
private unloadOutlet(outlet: RouterOutlet, components: Object[]): void {
if (outlet.isLoaded) {
StringMapWrapper.forEach(outlet.outletMap._outlets,
(v, k) => this.unloadOutlet(v, components));
if (this.performMutation) {
outlet.unload();
} else {
this.deactivations.push(components.concat([outlet.loadedComponent]));
}
}
}
}
3 changes: 2 additions & 1 deletion modules/angular2/src/alt_router/router_providers_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function routerFactory(app: ApplicationRef, componentResolver: ComponentResolver
if (app.componentTypes.length == 0) {
throw new BaseException("Bootstrap at least one component before injecting Router.");
}
return new Router(app.componentTypes[0], componentResolver, urlSerializer, routerOutletMap,
// TODO: vsavkin this should not be null
return new Router(null, app.componentTypes[0], componentResolver, urlSerializer, routerOutletMap,
location);
}
Loading

0 comments on commit e5b87e5

Please sign in to comment.