feat(router): implement relative navigation
This commit is contained in:
parent
d097784d57
commit
e5b87e55da
|
@ -12,7 +12,7 @@ export {
|
||||||
RouterUrlSerializer,
|
RouterUrlSerializer,
|
||||||
DefaultRouterUrlSerializer
|
DefaultRouterUrlSerializer
|
||||||
} from './src/alt_router/router_url_serializer';
|
} 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';
|
export {ROUTER_PROVIDERS} from './src/alt_router/router_providers';
|
||||||
|
|
||||||
import {RouterOutlet} from './src/alt_router/directives/router_outlet';
|
import {RouterOutlet} from './src/alt_router/directives/router_outlet';
|
||||||
|
|
|
@ -11,28 +11,25 @@ import {
|
||||||
HostListener,
|
HostListener,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy
|
OnDestroy,
|
||||||
|
Optional
|
||||||
} from 'angular2/core';
|
} from 'angular2/core';
|
||||||
import {RouterOutletMap, Router} from '../router';
|
import {RouterOutletMap, Router} from '../router';
|
||||||
import {RouteSegment, UrlSegment, Tree} from '../segments';
|
import {RouteSegment, UrlSegment, Tree} from '../segments';
|
||||||
import {link} from '../link';
|
import {isString, isPresent} from 'angular2/src/facade/lang';
|
||||||
import {isString} from 'angular2/src/facade/lang';
|
|
||||||
import {ObservableWrapper} from 'angular2/src/facade/async';
|
import {ObservableWrapper} from 'angular2/src/facade/async';
|
||||||
|
|
||||||
@Directive({selector: '[routerLink]'})
|
@Directive({selector: '[routerLink]'})
|
||||||
export class RouterLink implements OnDestroy {
|
export class RouterLink implements OnDestroy {
|
||||||
@Input() target: string;
|
@Input() target: string;
|
||||||
private _changes: any[] = [];
|
private _changes: any[] = [];
|
||||||
private _targetUrl: Tree<UrlSegment>;
|
|
||||||
private _subscription: any;
|
private _subscription: any;
|
||||||
|
|
||||||
@HostBinding() private href: string;
|
@HostBinding() private href: string;
|
||||||
|
|
||||||
constructor(private _router: Router) {
|
constructor(@Optional() private _routeSegment: RouteSegment, private _router: Router) {
|
||||||
this._subscription = ObservableWrapper.subscribe(_router.changes, (_) => {
|
this._subscription =
|
||||||
this._targetUrl = _router.urlTree;
|
ObservableWrapper.subscribe(_router.changes, (_) => { this._updateTargetUrlAndHref(); });
|
||||||
this._updateTargetUrlAndHref();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() { ObservableWrapper.dispose(this._subscription); }
|
ngOnDestroy() { ObservableWrapper.dispose(this._subscription); }
|
||||||
|
@ -46,14 +43,16 @@ export class RouterLink implements OnDestroy {
|
||||||
@HostListener("click")
|
@HostListener("click")
|
||||||
onClick(): boolean {
|
onClick(): boolean {
|
||||||
if (!isString(this.target) || this.target == '_self') {
|
if (!isString(this.target) || this.target == '_self') {
|
||||||
this._router.navigate(this._targetUrl);
|
this._router.navigate(this._changes, this._routeSegment);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateTargetUrlAndHref(): void {
|
private _updateTargetUrlAndHref(): void {
|
||||||
this._targetUrl = link(null, this._router.urlTree, this._changes);
|
let tree = this._router.createUrlTree(this._changes, this._routeSegment);
|
||||||
this.href = this._router.serializeUrl(this._targetUrl);
|
if (isPresent(tree)) {
|
||||||
|
this.href = this._router.serializeUrl(tree);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,11 +28,12 @@ export class RouterOutlet {
|
||||||
this._loaded = null;
|
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[],
|
load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[],
|
||||||
outletMap: RouterOutletMap): ComponentRef {
|
outletMap: RouterOutletMap): ComponentRef {
|
||||||
if (isPresent(this._loaded)) {
|
|
||||||
this.unload();
|
|
||||||
}
|
|
||||||
this.outletMap = outletMap;
|
this.outletMap = outletMap;
|
||||||
let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector);
|
let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector);
|
||||||
this._loaded = this._location.createComponent(factory, this._location.length, inj, []);
|
this._loaded = this._location.createComponent(factory, this._location.length, inj, []);
|
||||||
|
|
|
@ -1,12 +1,64 @@
|
||||||
import {Tree, TreeNode, UrlSegment, RouteSegment, rootNode} from './segments';
|
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';
|
import {ListWrapper} from 'angular2/src/facade/collection';
|
||||||
|
|
||||||
export function link(segment: RouteSegment, tree: Tree<UrlSegment>,
|
export function link(segment: RouteSegment, routeTree: Tree<RouteSegment>,
|
||||||
change: any[]): Tree<UrlSegment> {
|
urlTree: Tree<UrlSegment>, change: any[]): Tree<UrlSegment> {
|
||||||
if (change.length === 0) return tree;
|
if (change.length === 0) return urlTree;
|
||||||
let normalizedChange = (change.length === 1 && change[0] == "/") ? change : ["/"].concat(change);
|
|
||||||
return new Tree<UrlSegment>(_update(rootNode(tree), normalizedChange));
|
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> {
|
function _update(node: TreeNode<UrlSegment>, changes: any[]): TreeNode<UrlSegment> {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {ComponentResolver} from 'angular2/core';
|
||||||
import {DEFAULT_OUTLET_NAME} from './constants';
|
import {DEFAULT_OUTLET_NAME} from './constants';
|
||||||
import {reflector} from 'angular2/src/core/reflection/reflection';
|
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,
|
export function recognize(componentResolver: ComponentResolver, type: Type,
|
||||||
url: Tree<UrlSegment>): Promise<Tree<RouteSegment>> {
|
url: Tree<UrlSegment>): Promise<Tree<RouteSegment>> {
|
||||||
let matched = new _MatchResult(type, [url.root], null, rootNode(url).children, []);
|
let matched = new _MatchResult(type, [url.root], null, rootNode(url).children, []);
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import {OnInit, provide, ReflectiveInjector, ComponentResolver} from 'angular2/core';
|
import {OnInit, provide, ReflectiveInjector, ComponentResolver} from 'angular2/core';
|
||||||
import {RouterOutlet} from './directives/router_outlet';
|
import {RouterOutlet} from './directives/router_outlet';
|
||||||
import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
|
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 {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||||
import {RouterUrlSerializer} from './router_url_serializer';
|
import {RouterUrlSerializer} from './router_url_serializer';
|
||||||
|
import {CanDeactivate} from './interfaces';
|
||||||
import {recognize} from './recognize';
|
import {recognize} from './recognize';
|
||||||
import {Location} from 'angular2/platform/common';
|
import {Location} from 'angular2/platform/common';
|
||||||
|
import {link} from './link';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
equalSegments,
|
equalSegments,
|
||||||
routeSegmentComponentFactory,
|
routeSegmentComponentFactory,
|
||||||
|
@ -32,7 +36,8 @@ export class Router {
|
||||||
|
|
||||||
private _changes: EventEmitter<void> = new EventEmitter<void>();
|
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 _urlSerializer: RouterUrlSerializer,
|
||||||
private _routerOutletMap: RouterOutletMap, private _location: Location) {
|
private _routerOutletMap: RouterOutletMap, private _location: Location) {
|
||||||
this.navigateByUrl(this._location.path());
|
this.navigateByUrl(this._location.path());
|
||||||
|
@ -40,62 +45,89 @@ export class Router {
|
||||||
|
|
||||||
get urlTree(): Tree<UrlSegment> { return this._urlTree; }
|
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;
|
this._urlTree = url;
|
||||||
return recognize(this._componentResolver, this._componentType, url)
|
return recognize(this._componentResolver, this._rootComponentType, url)
|
||||||
.then(currTree => {
|
.then(currTree => {
|
||||||
new _LoadSegments(currTree, this._prevTree).load(this._routerOutletMap);
|
return new _LoadSegments(currTree, this._prevTree)
|
||||||
|
.load(this._routerOutletMap, this._rootComponent)
|
||||||
|
.then(_ => {
|
||||||
this._prevTree = currTree;
|
this._prevTree = currTree;
|
||||||
this._location.go(this._urlSerializer.serialize(this._urlTree));
|
this._location.go(this._urlSerializer.serialize(this._urlTree));
|
||||||
this._changes.emit(null);
|
this._changes.emit(null);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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); }
|
serializeUrl(url: Tree<UrlSegment>): string { return this._urlSerializer.serialize(url); }
|
||||||
|
|
||||||
navigateByUrl(url: string): Promise<void> {
|
|
||||||
return this.navigate(this._urlSerializer.parse(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
get changes(): Observable<void> { return this._changes; }
|
get changes(): Observable<void> { return this._changes; }
|
||||||
|
|
||||||
|
get routeTree(): Tree<RouteSegment> { return this._prevTree; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _LoadSegments {
|
class _LoadSegments {
|
||||||
|
private deactivations: Object[][] = [];
|
||||||
|
private performMutation: boolean = true;
|
||||||
|
|
||||||
constructor(private currTree: Tree<RouteSegment>, private prevTree: Tree<RouteSegment>) {}
|
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 prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null;
|
||||||
let currRoot = rootNode(this.currTree);
|
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>,
|
private canDeactivate(currRoot: TreeNode<RouteSegment>, prevRoot: TreeNode<RouteSegment>,
|
||||||
parentOutletMap: RouterOutletMap): void {
|
outletMap: RouterOutletMap, rootComponent: Object): Promise<boolean> {
|
||||||
let curr = currNode.value;
|
this.performMutation = false;
|
||||||
let prev = isPresent(prevNode) ? prevNode.value : null;
|
this.loadChildSegments(currRoot, prevRoot, outletMap, [rootComponent]);
|
||||||
let outlet = this.getOutlet(parentOutletMap, currNode.value);
|
|
||||||
|
|
||||||
if (equalSegments(curr, prev)) {
|
let allPaths = PromiseWrapper.all(this.deactivations.map(r => this.checkCanDeactivatePath(r)));
|
||||||
this.loadChildSegments(currNode, prevNode, outlet.outletMap);
|
return allPaths.then((values: boolean[]) => values.filter(v => v).length === values.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
let outletMap = new RouterOutletMap();
|
return _;
|
||||||
this.loadNewSegment(outletMap, curr, prev, outlet);
|
|
||||||
this.loadChildSegments(currNode, prevNode, outletMap);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return curr;
|
||||||
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 loadChildSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
|
private loadChildSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
|
||||||
outletMap: RouterOutletMap): void {
|
outletMap: RouterOutletMap, components: Object[]): void {
|
||||||
let prevChildren = isPresent(prevNode) ?
|
let prevChildren = isPresent(prevNode) ?
|
||||||
prevNode.children.reduce(
|
prevNode.children.reduce(
|
||||||
(m, c) => {
|
(m, c) => {
|
||||||
|
@ -106,11 +138,42 @@ class _LoadSegments {
|
||||||
{};
|
{};
|
||||||
|
|
||||||
currNode.children.forEach(c => {
|
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.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 {
|
private getOutlet(outletMap: RouterOutletMap, segment: RouteSegment): RouterOutlet {
|
||||||
|
@ -125,8 +188,15 @@ class _LoadSegments {
|
||||||
return outlet;
|
return outlet;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unloadOutlet(outlet: RouterOutlet): void {
|
private unloadOutlet(outlet: RouterOutlet, components: Object[]): void {
|
||||||
StringMapWrapper.forEach(outlet.outletMap._outlets, (v, k) => { this.unloadOutlet(v); });
|
if (outlet.isLoaded) {
|
||||||
|
StringMapWrapper.forEach(outlet.outletMap._outlets,
|
||||||
|
(v, k) => this.unloadOutlet(v, components));
|
||||||
|
if (this.performMutation) {
|
||||||
outlet.unload();
|
outlet.unload();
|
||||||
|
} else {
|
||||||
|
this.deactivations.push(components.concat([outlet.loadedComponent]));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -23,6 +23,7 @@ function routerFactory(app: ApplicationRef, componentResolver: ComponentResolver
|
||||||
if (app.componentTypes.length == 0) {
|
if (app.componentTypes.length == 0) {
|
||||||
throw new BaseException("Bootstrap at least one component before injecting Router.");
|
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);
|
location);
|
||||||
}
|
}
|
|
@ -33,6 +33,8 @@ export function rootNode<T>(tree: Tree<T>): TreeNode<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _findNode<T>(expected: T, c: TreeNode<T>): TreeNode<T> {
|
function _findNode<T>(expected: T, c: TreeNode<T>): TreeNode<T> {
|
||||||
|
// TODO: vsavkin remove it once recognize is fixed
|
||||||
|
if (expected instanceof RouteSegment && equalSegments(<any>expected, <any>c.value)) return c;
|
||||||
if (expected === c.value) return c;
|
if (expected === c.value) return c;
|
||||||
for (let cc of c.children) {
|
for (let cc of c.children) {
|
||||||
let r = _findNode(expected, cc);
|
let r = _findNode(expected, cc);
|
||||||
|
@ -44,6 +46,9 @@ function _findNode<T>(expected: T, c: TreeNode<T>): TreeNode<T> {
|
||||||
function _findPath<T>(expected: T, c: TreeNode<T>, collected: TreeNode<T>[]): TreeNode<T>[] {
|
function _findPath<T>(expected: T, c: TreeNode<T>, collected: TreeNode<T>[]): TreeNode<T>[] {
|
||||||
collected.push(c);
|
collected.push(c);
|
||||||
|
|
||||||
|
// TODO: vsavkin remove it once recognize is fixed
|
||||||
|
if (expected instanceof RouteSegment && equalSegments(<any>expected, <any>c.value))
|
||||||
|
return collected;
|
||||||
if (expected === c.value) return collected;
|
if (expected === c.value) return collected;
|
||||||
for (let cc of c.children) {
|
for (let cc of c.children) {
|
||||||
let r = _findPath(expected, cc, ListWrapper.clone(collected));
|
let r = _findPath(expected, cc, ListWrapper.clone(collected));
|
||||||
|
@ -114,6 +119,7 @@ export function equalSegments(a: RouteSegment, b: RouteSegment): boolean {
|
||||||
if (a._type !== b._type) return false;
|
if (a._type !== b._type) return false;
|
||||||
if (isBlank(a.parameters) && !isBlank(b.parameters)) return false;
|
if (isBlank(a.parameters) && !isBlank(b.parameters)) return false;
|
||||||
if (!isBlank(a.parameters) && isBlank(b.parameters)) return false;
|
if (!isBlank(a.parameters) && isBlank(b.parameters)) return false;
|
||||||
|
if (isBlank(a.parameters) && isBlank(b.parameters)) return true;
|
||||||
return StringMapWrapper.equals(a.parameters, b.parameters);
|
return StringMapWrapper.equals(a.parameters, b.parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
tick
|
tick
|
||||||
} from 'angular2/testing_internal';
|
} from 'angular2/testing_internal';
|
||||||
import {provide, Component, ComponentResolver} from 'angular2/core';
|
import {provide, Component, ComponentResolver} from 'angular2/core';
|
||||||
|
import {PromiseWrapper} from 'angular2/src/facade/async';
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -28,7 +29,8 @@ import {
|
||||||
Routes,
|
Routes,
|
||||||
RouterUrlSerializer,
|
RouterUrlSerializer,
|
||||||
DefaultRouterUrlSerializer,
|
DefaultRouterUrlSerializer,
|
||||||
OnActivate
|
OnActivate,
|
||||||
|
CanDeactivate
|
||||||
} from 'angular2/alt_router';
|
} from 'angular2/alt_router';
|
||||||
import {SpyLocation} from 'angular2/src/mock/location_mock';
|
import {SpyLocation} from 'angular2/src/mock/location_mock';
|
||||||
import {Location} from 'angular2/platform/common';
|
import {Location} from 'angular2/platform/common';
|
||||||
|
@ -42,8 +44,8 @@ export function main() {
|
||||||
provide(Location, {useClass: SpyLocation}),
|
provide(Location, {useClass: SpyLocation}),
|
||||||
provide(Router,
|
provide(Router,
|
||||||
{
|
{
|
||||||
useFactory: (resolver, urlParser, outletMap, location) =>
|
useFactory: (resolver, urlParser, outletMap, location) => new Router(
|
||||||
new Router(RootCmp, resolver, urlParser, outletMap, location),
|
"RootComponent", RootCmp, resolver, urlParser, outletMap, location),
|
||||||
deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap, Location]
|
deps: [ComponentResolver, RouterUrlSerializer, RouterOutletMap, Location]
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
@ -124,9 +126,22 @@ export function main() {
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor, aux: }');
|
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor, aux: }');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
if (DOM.supportsDOMEvents()) { // this is required to use fakeAsync
|
it('should not unload the route if can deactivate returns false',
|
||||||
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
let fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
|
||||||
it("should support router links",
|
router.navigateByUrl('/team/22/cannotDeactivate');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/22/user/fedor');
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(fixture.debugElement.nativeElement)
|
||||||
|
.toHaveText('team 22 { cannotDeactivate, aux: }');
|
||||||
|
})));
|
||||||
|
|
||||||
|
if (DOM.supportsDOMEvents()) {
|
||||||
|
it("should support absolute router links",
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
let fixture = tcb.createFakeAsync(RootCmp);
|
let fixture = tcb.createFakeAsync(RootCmp);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
@ -143,6 +158,25 @@ export function main() {
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, aux: }');
|
expect(fixture.debugElement.nativeElement).toHaveText('team 33 { simple, aux: }');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
it("should support relative router links",
|
||||||
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
let fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
router.navigateByUrl('/team/22/relativelink');
|
||||||
|
advance(fixture);
|
||||||
|
expect(fixture.debugElement.nativeElement)
|
||||||
|
.toHaveText('team 22 { relativelink { }, aux: }');
|
||||||
|
|
||||||
|
let native = DOM.querySelector(fixture.debugElement.nativeElement, "a");
|
||||||
|
expect(DOM.getAttribute(native, "href")).toEqual("/team/22/relativelink/simple");
|
||||||
|
DOM.dispatchEvent(native, DOM.createMouseEvent('click'));
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
|
expect(fixture.debugElement.nativeElement)
|
||||||
|
.toHaveText('team 22 { relativelink { simple }, aux: }');
|
||||||
|
})));
|
||||||
|
|
||||||
it("should update router links when router changes",
|
it("should update router links when router changes",
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
let fixture = tcb.createFakeAsync(RootCmp);
|
let fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
@ -179,6 +213,11 @@ class UserCmp implements OnActivate {
|
||||||
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); }
|
routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'cannot-deactivate', template: `cannotDeactivate`})
|
||||||
|
class CanDeactivateCmp implements CanDeactivate {
|
||||||
|
routerCanDeactivate(a?, b?): Promise<boolean> { return PromiseWrapper.resolve(false); }
|
||||||
|
}
|
||||||
|
|
||||||
@Component({selector: 'simple-cmp', template: `simple`})
|
@Component({selector: 'simple-cmp', template: `simple`})
|
||||||
class SimpleCmp {
|
class SimpleCmp {
|
||||||
}
|
}
|
||||||
|
@ -189,12 +228,21 @@ class Simple2Cmp {
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'link-cmp',
|
selector: 'link-cmp',
|
||||||
template: `<a [routerLink]="['team', '33', 'simple']">link</a>`,
|
template: `<a [routerLink]="['/team', '33', 'simple']">link</a>`,
|
||||||
directives: ROUTER_DIRECTIVES
|
directives: ROUTER_DIRECTIVES
|
||||||
})
|
})
|
||||||
class LinkCmp {
|
class LinkCmp {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'link-cmp',
|
||||||
|
template: `<a [routerLink]="['./simple']">relativelink</a> { <router-outlet></router-outlet> }`,
|
||||||
|
directives: ROUTER_DIRECTIVES
|
||||||
|
})
|
||||||
|
@Routes([new Route({path: 'simple', component: SimpleCmp})])
|
||||||
|
class RelativeLinkCmp {
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'team-cmp',
|
selector: 'team-cmp',
|
||||||
template: `team {{id}} { <router-outlet></router-outlet>, aux: <router-outlet name="aux"></router-outlet> }`,
|
template: `team {{id}} { <router-outlet></router-outlet>, aux: <router-outlet name="aux"></router-outlet> }`,
|
||||||
|
@ -204,7 +252,9 @@ class LinkCmp {
|
||||||
new Route({path: 'user/:name', component: UserCmp}),
|
new Route({path: 'user/:name', component: UserCmp}),
|
||||||
new Route({path: 'simple', component: SimpleCmp}),
|
new Route({path: 'simple', component: SimpleCmp}),
|
||||||
new Route({path: 'simple2', component: Simple2Cmp}),
|
new Route({path: 'simple2', component: Simple2Cmp}),
|
||||||
new Route({path: 'link', component: LinkCmp})
|
new Route({path: 'link', component: LinkCmp}),
|
||||||
|
new Route({path: 'relativelink', component: RelativeLinkCmp}),
|
||||||
|
new Route({path: 'cannotDeactivate', component: CanDeactivateCmp})
|
||||||
])
|
])
|
||||||
class TeamCmp implements OnActivate {
|
class TeamCmp implements OnActivate {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
xit
|
xit
|
||||||
} from 'angular2/testing_internal';
|
} from 'angular2/testing_internal';
|
||||||
|
|
||||||
import {RouteSegment, UrlSegment, Tree} from 'angular2/src/alt_router/segments';
|
import {RouteSegment, UrlSegment, Tree, TreeNode} from 'angular2/src/alt_router/segments';
|
||||||
import {link} from 'angular2/src/alt_router/link';
|
import {link} from 'angular2/src/alt_router/link';
|
||||||
import {DefaultRouterUrlSerializer} from 'angular2/src/alt_router/router_url_serializer';
|
import {DefaultRouterUrlSerializer} from 'angular2/src/alt_router/router_url_serializer';
|
||||||
|
|
||||||
|
@ -25,43 +25,78 @@ export function main() {
|
||||||
|
|
||||||
it("should return the original tree when given an empty array", () => {
|
it("should return the original tree when given an empty array", () => {
|
||||||
let p = parser.parse("/");
|
let p = parser.parse("/");
|
||||||
let t = link(s(p.root), p, []);
|
let tree = s(p.root);
|
||||||
|
let t = link(tree.root, tree, p, []);
|
||||||
expect(t).toBe(p);
|
expect(t).toBe(p);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support going to root", () => {
|
it("should support going to root", () => {
|
||||||
let p = parser.parse("/");
|
let p = parser.parse("/");
|
||||||
let t = link(s(p.root), p, ["/"]);
|
let tree = s(p.root);
|
||||||
|
let t = link(tree.root, tree, p, ["/"]);
|
||||||
expect(parser.serialize(t)).toEqual("");
|
expect(parser.serialize(t)).toEqual("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support positional params", () => {
|
it("should support positional params", () => {
|
||||||
let p = parser.parse("/");
|
let p = parser.parse("/a/b");
|
||||||
let t = link(s(p.root), p, ["/one", 11, "two", 22]);
|
let tree = s(p.firstChild(p.root));
|
||||||
|
let t = link(tree.root, tree, p, ["/one", 11, "two", 22]);
|
||||||
expect(parser.serialize(t)).toEqual("/one/11/two/22");
|
expect(parser.serialize(t)).toEqual("/one/11/two/22");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve route siblings when changing the main route", () => {
|
it("should preserve route siblings when changing the main route", () => {
|
||||||
let p = parser.parse("/a/11/b(c)");
|
let p = parser.parse("/a/11/b(c)");
|
||||||
let t = link(s(p.root), p, ["/a", 11, 'd']);
|
let tree = s(p.root);
|
||||||
|
let t = link(tree.root, tree, p, ["/a", 11, 'd']);
|
||||||
expect(parser.serialize(t)).toEqual("/a/11/d(aux:c)");
|
expect(parser.serialize(t)).toEqual("/a/11/d(aux:c)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve route siblings when changing a aux route", () => {
|
it("should preserve route siblings when changing a aux route", () => {
|
||||||
let p = parser.parse("/a/11/b(c)");
|
let p = parser.parse("/a/11/b(c)");
|
||||||
let t = link(s(p.root), p, ["/a", 11, 'aux:d']);
|
let tree = s(p.root);
|
||||||
|
let t = link(tree.root, tree, p, ["/a", 11, 'aux:d']);
|
||||||
expect(parser.serialize(t)).toEqual("/a/11/b(aux:d)");
|
expect(parser.serialize(t)).toEqual("/a/11/b(aux:d)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should update parameters', () => {
|
it('should update parameters', () => {
|
||||||
let p = parser.parse("/a;aa=11");
|
let p = parser.parse("/a;aa=11");
|
||||||
let t = link(s(p.root), p, ["/a", {aa: 22, bb: 33}]);
|
let tree = s(p.root);
|
||||||
|
let t = link(tree.root, tree, p, ["/a", {aa: 22, bb: 33}]);
|
||||||
expect(parser.serialize(t)).toEqual("/a;aa=22;bb=33");
|
expect(parser.serialize(t)).toEqual("/a;aa=22;bb=33");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should update relative subtree (when starts with ./)", () => {
|
||||||
|
let p = parser.parse("/a(ap)/c(cp)");
|
||||||
|
let c = p.firstChild(p.root);
|
||||||
|
let tree = s(c);
|
||||||
|
let t = link(tree.root, tree, p, ["./c2"]);
|
||||||
|
expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update relative subtree (when does not start with ./)", () => {
|
||||||
|
let p = parser.parse("/a(ap)/c(cp)");
|
||||||
|
let c = p.firstChild(p.root);
|
||||||
|
let tree = s(c);
|
||||||
|
let t = link(tree.root, tree, p, ["c2"]);
|
||||||
|
expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update relative subtree when the provided segment doesn't have url segments", () => {
|
||||||
|
let p = parser.parse("/a(ap)/c(cp)");
|
||||||
|
let c = p.firstChild(p.root);
|
||||||
|
|
||||||
|
let child = new RouteSegment([], null, null, null, null);
|
||||||
|
let root = new TreeNode<RouteSegment>(new RouteSegment([c], {}, null, null, null),
|
||||||
|
[new TreeNode<RouteSegment>(child, [])]);
|
||||||
|
let tree = new Tree<RouteSegment>(root);
|
||||||
|
|
||||||
|
let t = link(child, tree, p, ["./c2"]);
|
||||||
|
expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function s(u: UrlSegment): RouteSegment {
|
function s(u: UrlSegment): Tree<RouteSegment> {
|
||||||
return new RouteSegment([u], {}, null, null, null);
|
let root = new TreeNode<RouteSegment>(new RouteSegment([u], {}, null, null, null), []);
|
||||||
|
return new Tree<RouteSegment>(root);
|
||||||
}
|
}
|
Loading…
Reference in New Issue