227 lines
8.2 KiB
TypeScript
Raw Normal View History

2016-05-02 17:11:21 +00:00
import {OnInit, provide, ReflectiveInjector, ComponentResolver} from '@angular/core';
import {RouterOutlet} from './directives/router_outlet';
2016-05-02 17:11:21 +00:00
import {Type, isBlank, isPresent} from '@angular/facade/src/lang';
import {ListWrapper} from '@angular/facade/src/collection';
import {
EventEmitter,
Observable,
PromiseWrapper,
ObservableWrapper
} from '@angular/facade/src/async';
import {StringMapWrapper} from '@angular/facade/src/collection';
import {BaseException} from '@angular/core';
import {RouterUrlSerializer} from './router_url_serializer';
import {CanDeactivate} from './interfaces';
import {recognize} from './recognize';
import {Location} from '@angular/common';
import {link} from './link';
import {
equalSegments,
routeSegmentComponentFactory,
RouteSegment,
UrlTree,
RouteTree,
rootNode,
TreeNode,
UrlSegment,
serializeRouteSegmentTree
} from './segments';
import {hasLifecycleHook} from './lifecycle_reflector';
import {DEFAULT_OUTLET_NAME} from './constants';
export class RouterOutletMap {
/** @internal */
_outlets: {[name: string]: RouterOutlet} = {};
registerOutlet(name: string, outlet: RouterOutlet): void { this._outlets[name] = outlet; }
}
export class Router {
2016-05-02 17:11:21 +00:00
private _prevTree: RouteTree;
private _urlTree: UrlTree;
private _locationSubscription: any;
private _changes: EventEmitter<void> = new EventEmitter<void>();
2016-05-02 17:11:21 +00:00
constructor(private _rootComponent: Object, private _rootComponentType: Type,
private _componentResolver: ComponentResolver,
private _urlSerializer: RouterUrlSerializer,
private _routerOutletMap: RouterOutletMap, private _location: Location) {
this._prevTree = this._createInitialTree();
this._setUpLocationChangeListener();
this.navigateByUrl(this._location.path());
}
2016-05-02 17:11:21 +00:00
get urlTree(): UrlTree { return this._urlTree; }
refactor(router): improve recognition and generation pipeline This is a big change. @matsko also deserves much of the credit for the implementation. Previously, `ComponentInstruction`s held all the state for async components. Now, we introduce several subclasses for `Instruction` to describe each type of navigation. BREAKING CHANGE: Redirects now use the Link DSL syntax. Before: ``` @RouteConfig([ { path: '/foo', redirectTo: '/bar' }, { path: '/bar', component: BarCmp } ]) ``` After: ``` @RouteConfig([ { path: '/foo', redirectTo: ['Bar'] }, { path: '/bar', component: BarCmp, name: 'Bar' } ]) ``` BREAKING CHANGE: This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading and encapsulating large routes with sub-routes easier. Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`: @RouteConfig([ { path: '/tab', redirectTo: '/tab/users' } { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } Now the recommended way to handle this is case is to use `useAsDefault` like so: ``` @RouteConfig([ { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } @RouteConfig([ { path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' }, { path: '/users', component: UsersCmp, name: 'Users' } ]) TabsCmp { ... } ``` In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route. Closes #4728 Closes #4228 Closes #4170 Closes #4490 Closes #4694 Closes #5200 Closes #5475
2015-11-23 18:07:37 -08:00
2016-05-02 17:11:21 +00:00
navigateByUrl(url: string): Promise<void> {
return this._navigate(this._urlSerializer.parse(url));
}
2016-05-02 17:11:21 +00:00
navigate(changes: any[], segment?: RouteSegment): Promise<void> {
return this._navigate(this.createUrlTree(changes, segment));
}
2016-05-02 17:11:21 +00:00
dispose(): void { ObservableWrapper.dispose(this._locationSubscription); }
2016-05-02 17:11:21 +00:00
private _createInitialTree(): RouteTree {
let root = new RouteSegment([new UrlSegment("", null, null)], null, DEFAULT_OUTLET_NAME,
this._rootComponentType, null);
return new RouteTree(new TreeNode<RouteSegment>(root, []));
}
2016-05-02 17:11:21 +00:00
private _setUpLocationChangeListener(): void {
this._locationSubscription = this._location.subscribe(
(change) => { this._navigate(this._urlSerializer.parse(change['url'])); });
}
2016-05-02 17:11:21 +00:00
private _navigate(url: UrlTree): Promise<void> {
this._urlTree = url;
return recognize(this._componentResolver, this._rootComponentType, url)
.then(currTree => {
return new _LoadSegments(currTree, this._prevTree)
.load(this._routerOutletMap, this._rootComponent)
.then(updated => {
if (updated) {
this._prevTree = currTree;
this._location.go(this._urlSerializer.serialize(this._urlTree));
this._changes.emit(null);
}
});
});
}
2016-05-02 17:11:21 +00:00
createUrlTree(changes: any[], segment?: RouteSegment): UrlTree {
if (isPresent(this._prevTree)) {
let s = isPresent(segment) ? segment : this._prevTree.root;
return link(s, this._prevTree, this.urlTree, changes);
} else {
2016-05-02 17:11:21 +00:00
return null;
}
}
2016-05-02 17:11:21 +00:00
serializeUrl(url: UrlTree): string { return this._urlSerializer.serialize(url); }
2016-05-02 17:11:21 +00:00
get changes(): Observable<void> { return this._changes; }
2016-05-02 17:11:21 +00:00
get routeTree(): RouteTree { return this._prevTree; }
}
2016-05-02 17:11:21 +00:00
class _LoadSegments {
private deactivations: Object[][] = [];
private performMutation: boolean = true;
2016-05-02 17:11:21 +00:00
constructor(private currTree: RouteTree, private prevTree: RouteTree) {}
2016-05-02 17:11:21 +00:00
load(parentOutletMap: RouterOutletMap, rootComponent: Object): Promise<boolean> {
let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null;
let currRoot = rootNode(this.currTree);
2016-05-02 17:11:21 +00:00
return this.canDeactivate(currRoot, prevRoot, parentOutletMap, rootComponent)
.then(res => {
this.performMutation = true;
if (res) {
this.loadChildSegments(currRoot, prevRoot, parentOutletMap, [rootComponent]);
}
return res;
});
}
2016-05-02 17:11:21 +00:00
private canDeactivate(currRoot: TreeNode<RouteSegment>, prevRoot: TreeNode<RouteSegment>,
outletMap: RouterOutletMap, rootComponent: Object): Promise<boolean> {
this.performMutation = false;
this.loadChildSegments(currRoot, prevRoot, outletMap, [rootComponent]);
2016-05-02 17:11:21 +00:00
let allPaths = PromiseWrapper.all(this.deactivations.map(r => this.checkCanDeactivatePath(r)));
return allPaths.then((values: boolean[]) => values.filter(v => v).length === values.length);
refactor(router): improve recognition and generation pipeline This is a big change. @matsko also deserves much of the credit for the implementation. Previously, `ComponentInstruction`s held all the state for async components. Now, we introduce several subclasses for `Instruction` to describe each type of navigation. BREAKING CHANGE: Redirects now use the Link DSL syntax. Before: ``` @RouteConfig([ { path: '/foo', redirectTo: '/bar' }, { path: '/bar', component: BarCmp } ]) ``` After: ``` @RouteConfig([ { path: '/foo', redirectTo: ['Bar'] }, { path: '/bar', component: BarCmp, name: 'Bar' } ]) ``` BREAKING CHANGE: This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading and encapsulating large routes with sub-routes easier. Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`: @RouteConfig([ { path: '/tab', redirectTo: '/tab/users' } { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } Now the recommended way to handle this is case is to use `useAsDefault` like so: ``` @RouteConfig([ { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } @RouteConfig([ { path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' }, { path: '/users', component: UsersCmp, name: 'Users' } ]) TabsCmp { ... } ``` In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route. Closes #4728 Closes #4228 Closes #4170 Closes #4490 Closes #4694 Closes #5200 Closes #5475
2015-11-23 18:07:37 -08:00
}
2016-05-02 17:11:21 +00:00
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 _;
}
});
}
2016-05-02 17:11:21 +00:00
return curr;
}
2016-05-02 17:11:21 +00:00
private loadChildSegments(currNode: TreeNode<RouteSegment>, prevNode: TreeNode<RouteSegment>,
outletMap: RouterOutletMap, components: Object[]): void {
let prevChildren = isPresent(prevNode) ?
prevNode.children.reduce(
(m, c) => {
m[c.value.outlet] = c;
return m;
},
{}) :
{};
2016-05-02 17:11:21 +00:00
currNode.children.forEach(c => {
this.loadSegments(c, prevChildren[c.value.outlet], outletMap, components);
StringMapWrapper.delete(prevChildren, c.value.outlet);
});
2016-05-02 17:11:21 +00:00
StringMapWrapper.forEach(prevChildren,
(v, k) => this.unloadOutlet(outletMap._outlets[k], components));
}
2016-05-02 17:11:21 +00:00
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);
2016-05-02 17:11:21 +00:00
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]));
}
}
}
2016-05-02 17:11:21 +00:00
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;
}
2016-05-02 17:11:21 +00:00
private getOutlet(outletMap: RouterOutletMap, segment: RouteSegment): RouterOutlet {
let outlet = outletMap._outlets[segment.outlet];
if (isBlank(outlet)) {
if (segment.outlet == DEFAULT_OUTLET_NAME) {
throw new BaseException(`Cannot find default outlet`);
} else {
throw new BaseException(`Cannot find the outlet ${segment.outlet}`);
}
}
return outlet;
}
2016-05-02 17:11:21 +00:00
private unloadOutlet(outlet: RouterOutlet, components: Object[]): void {
if (isPresent(outlet) && 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]));
}
}
2016-05-02 17:11:21 +00:00
}
}