chore: router move-only
This commit is contained in:
@ -1,578 +1,226 @@
|
||||
import {PromiseWrapper, EventEmitter, ObservableWrapper} from '../src/facade/async';
|
||||
import {Map, StringMapWrapper} from '../src/facade/collection';
|
||||
import {isBlank, isPresent, Type} from '../src/facade/lang';
|
||||
import {BaseException} from '../src/facade/exceptions';
|
||||
import {Location} from '@angular/common';
|
||||
import {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from './route_registry';
|
||||
import {ComponentInstruction, Instruction} from './instruction';
|
||||
import {OnInit, provide, ReflectiveInjector, ComponentResolver} from '@angular/core';
|
||||
import {RouterOutlet} from './directives/router_outlet';
|
||||
import {getCanActivateHook} from './lifecycle/route_lifecycle_reflector';
|
||||
import {RouteDefinition} from './route_config/route_config_impl';
|
||||
import {Injectable, Inject} from '@angular/core';
|
||||
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';
|
||||
|
||||
let _resolveToTrue = PromiseWrapper.resolve(true);
|
||||
let _resolveToFalse = PromiseWrapper.resolve(false);
|
||||
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; }
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Router` is responsible for mapping URLs to components.
|
||||
*
|
||||
* You can see the state of the router by inspecting the read-only field `router.navigating`.
|
||||
* This may be useful for showing a spinner, for instance.
|
||||
*
|
||||
* ## Concepts
|
||||
*
|
||||
* Routers and component instances have a 1:1 correspondence.
|
||||
*
|
||||
* The router holds reference to a number of {@link RouterOutlet}.
|
||||
* An outlet is a placeholder that the router dynamically fills in depending on the current URL.
|
||||
*
|
||||
* When the router navigates from a URL, it must first recognize it and serialize it into an
|
||||
* `Instruction`.
|
||||
* The router uses the `RouteRegistry` to get an `Instruction`.
|
||||
*/
|
||||
@Injectable()
|
||||
export class Router {
|
||||
navigating: boolean = false;
|
||||
lastNavigationAttempt: string;
|
||||
/**
|
||||
* The current `Instruction` for the router
|
||||
*/
|
||||
public currentInstruction: Instruction = null;
|
||||
private _prevTree: RouteTree;
|
||||
private _urlTree: UrlTree;
|
||||
private _locationSubscription: any;
|
||||
private _changes: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
private _currentNavigation: Promise<any> = _resolveToTrue;
|
||||
private _outlet: RouterOutlet = null;
|
||||
|
||||
private _auxRouters = new Map<string, Router>();
|
||||
private _childRouter: Router;
|
||||
|
||||
private _subject: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
|
||||
constructor(public registry: RouteRegistry, public parent: Router, public hostComponent: any,
|
||||
public root?: Router) {}
|
||||
|
||||
/**
|
||||
* Constructs a child router. You probably don't need to use this unless you're writing a reusable
|
||||
* component.
|
||||
*/
|
||||
childRouter(hostComponent: any): Router {
|
||||
return this._childRouter = new ChildRouter(this, hostComponent);
|
||||
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());
|
||||
}
|
||||
|
||||
get urlTree(): UrlTree { return this._urlTree; }
|
||||
|
||||
/**
|
||||
* Constructs a child router. You probably don't need to use this unless you're writing a reusable
|
||||
* component.
|
||||
*/
|
||||
auxRouter(hostComponent: any): Router { return new ChildRouter(this, hostComponent); }
|
||||
|
||||
/**
|
||||
* Register an outlet to be notified of primary route changes.
|
||||
*
|
||||
* You probably don't need to use this unless you're writing a reusable component.
|
||||
*/
|
||||
registerPrimaryOutlet(outlet: RouterOutlet): Promise<any> {
|
||||
if (isPresent(outlet.name)) {
|
||||
throw new BaseException(`registerPrimaryOutlet expects to be called with an unnamed outlet.`);
|
||||
}
|
||||
|
||||
if (isPresent(this._outlet)) {
|
||||
throw new BaseException(`Primary outlet is already registered.`);
|
||||
}
|
||||
|
||||
this._outlet = outlet;
|
||||
if (isPresent(this.currentInstruction)) {
|
||||
return this.commit(this.currentInstruction, false);
|
||||
}
|
||||
return _resolveToTrue;
|
||||
navigateByUrl(url: string): Promise<void> {
|
||||
return this._navigate(this._urlSerializer.parse(url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an outlet (because it was destroyed, etc).
|
||||
*
|
||||
* You probably don't need to use this unless you're writing a custom outlet implementation.
|
||||
*/
|
||||
unregisterPrimaryOutlet(outlet: RouterOutlet): void {
|
||||
if (isPresent(outlet.name)) {
|
||||
throw new BaseException(`registerPrimaryOutlet expects to be called with an unnamed outlet.`);
|
||||
}
|
||||
this._outlet = null;
|
||||
navigate(changes: any[], segment?: RouteSegment): Promise<void> {
|
||||
return this._navigate(this.createUrlTree(changes, segment));
|
||||
}
|
||||
|
||||
dispose(): void { ObservableWrapper.dispose(this._locationSubscription); }
|
||||
|
||||
/**
|
||||
* Register an outlet to notified of auxiliary route changes.
|
||||
*
|
||||
* You probably don't need to use this unless you're writing a reusable component.
|
||||
*/
|
||||
registerAuxOutlet(outlet: RouterOutlet): Promise<any> {
|
||||
var outletName = outlet.name;
|
||||
if (isBlank(outletName)) {
|
||||
throw new BaseException(`registerAuxOutlet expects to be called with an outlet with a name.`);
|
||||
}
|
||||
|
||||
var router = this.auxRouter(this.hostComponent);
|
||||
|
||||
this._auxRouters.set(outletName, router);
|
||||
router._outlet = outlet;
|
||||
|
||||
var auxInstruction;
|
||||
if (isPresent(this.currentInstruction) &&
|
||||
isPresent(auxInstruction = this.currentInstruction.auxInstruction[outletName])) {
|
||||
return router.commit(auxInstruction);
|
||||
}
|
||||
return _resolveToTrue;
|
||||
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, []));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given an instruction, returns `true` if the instruction is currently active,
|
||||
* otherwise `false`.
|
||||
*/
|
||||
isRouteActive(instruction: Instruction): boolean {
|
||||
var router: Router = this;
|
||||
|
||||
if (isBlank(this.currentInstruction)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// `instruction` corresponds to the root router
|
||||
while (isPresent(router.parent) && isPresent(instruction.child)) {
|
||||
router = router.parent;
|
||||
instruction = instruction.child;
|
||||
}
|
||||
|
||||
if (isBlank(instruction.component) || isBlank(this.currentInstruction.component) ||
|
||||
this.currentInstruction.component.routeName != instruction.component.routeName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let paramEquals = true;
|
||||
|
||||
if (isPresent(this.currentInstruction.component.params)) {
|
||||
StringMapWrapper.forEach(instruction.component.params, (value, key) => {
|
||||
if (this.currentInstruction.component.params[key] !== value) {
|
||||
paramEquals = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return paramEquals;
|
||||
private _setUpLocationChangeListener(): void {
|
||||
this._locationSubscription = this._location.subscribe(
|
||||
(change) => { this._navigate(this._urlSerializer.parse(change['url'])); });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dynamically update the routing configuration and trigger a navigation.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* ```
|
||||
* router.config([
|
||||
* { 'path': '/', 'component': IndexComp },
|
||||
* { 'path': '/user/:id', 'component': UserComp },
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
config(definitions: RouteDefinition[]): Promise<any> {
|
||||
definitions.forEach(
|
||||
(routeDefinition) => { this.registry.config(this.hostComponent, routeDefinition); });
|
||||
return this.renavigate();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Navigate based on the provided Route Link DSL. It's preferred to navigate with this method
|
||||
* over `navigateByUrl`.
|
||||
*
|
||||
* ### Usage
|
||||
*
|
||||
* This method takes an array representing the Route Link DSL:
|
||||
* ```
|
||||
* ['./MyCmp', {param: 3}]
|
||||
* ```
|
||||
* See the {@link RouterLink} directive for more.
|
||||
*/
|
||||
navigate(linkParams: any[]): Promise<any> {
|
||||
var instruction = this.generate(linkParams);
|
||||
return this.navigateByInstruction(instruction, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Navigate to a URL. Returns a promise that resolves when navigation is complete.
|
||||
* It's preferred to navigate with `navigate` instead of this method, since URLs are more brittle.
|
||||
*
|
||||
* If the given URL begins with a `/`, router will navigate absolutely.
|
||||
* If the given URL does not begin with `/`, the router will navigate relative to this component.
|
||||
*/
|
||||
navigateByUrl(url: string, _skipLocationChange: boolean = false): Promise<any> {
|
||||
return this._currentNavigation = this._currentNavigation.then((_) => {
|
||||
this.lastNavigationAttempt = url;
|
||||
this._startNavigating();
|
||||
return this._afterPromiseFinishNavigating(this.recognize(url).then((instruction) => {
|
||||
if (isBlank(instruction)) {
|
||||
return false;
|
||||
}
|
||||
return this._navigate(instruction, _skipLocationChange);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Navigate via the provided instruction. Returns a promise that resolves when navigation is
|
||||
* complete.
|
||||
*/
|
||||
navigateByInstruction(instruction: Instruction,
|
||||
_skipLocationChange: boolean = false): Promise<any> {
|
||||
if (isBlank(instruction)) {
|
||||
return _resolveToFalse;
|
||||
}
|
||||
return this._currentNavigation = this._currentNavigation.then((_) => {
|
||||
this._startNavigating();
|
||||
return this._afterPromiseFinishNavigating(this._navigate(instruction, _skipLocationChange));
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_settleInstruction(instruction: Instruction): Promise<any> {
|
||||
return instruction.resolveComponent().then((_) => {
|
||||
var unsettledInstructions: Array<Promise<any>> = [];
|
||||
|
||||
if (isPresent(instruction.component)) {
|
||||
instruction.component.reuse = false;
|
||||
}
|
||||
|
||||
if (isPresent(instruction.child)) {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction.child));
|
||||
}
|
||||
|
||||
StringMapWrapper.forEach(instruction.auxInstruction, (instruction: Instruction, _) => {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction));
|
||||
});
|
||||
return PromiseWrapper.all(unsettledInstructions);
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_navigate(instruction: Instruction, _skipLocationChange: boolean): Promise<any> {
|
||||
return this._settleInstruction(instruction)
|
||||
.then((_) => this._routerCanReuse(instruction))
|
||||
.then((_) => this._canActivate(instruction))
|
||||
.then((result: boolean) => {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
return this._routerCanDeactivate(instruction)
|
||||
.then((result: boolean) => {
|
||||
if (result) {
|
||||
return this.commit(instruction, _skipLocationChange)
|
||||
.then((_) => {
|
||||
this._emitNavigationFinish(instruction.toRootUrl());
|
||||
return true;
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _emitNavigationFinish(url): void { ObservableWrapper.callEmit(this._subject, url); }
|
||||
/** @internal */
|
||||
_emitNavigationFail(url): void { ObservableWrapper.callError(this._subject, url); }
|
||||
|
||||
private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> {
|
||||
return PromiseWrapper.catchError(promise.then((_) => this._finishNavigating()), (err) => {
|
||||
this._finishNavigating();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Recursively set reuse flags
|
||||
*/
|
||||
/** @internal */
|
||||
_routerCanReuse(instruction: Instruction): Promise<any> {
|
||||
if (isBlank(this._outlet)) {
|
||||
return _resolveToFalse;
|
||||
}
|
||||
if (isBlank(instruction.component)) {
|
||||
return _resolveToTrue;
|
||||
}
|
||||
return this._outlet.routerCanReuse(instruction.component)
|
||||
.then((result) => {
|
||||
instruction.component.reuse = result;
|
||||
if (result && isPresent(this._childRouter) && isPresent(instruction.child)) {
|
||||
return this._childRouter._routerCanReuse(instruction.child);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _canActivate(nextInstruction: Instruction): Promise<boolean> {
|
||||
return canActivateOne(nextInstruction, this.currentInstruction);
|
||||
}
|
||||
|
||||
private _routerCanDeactivate(instruction: Instruction): Promise<boolean> {
|
||||
if (isBlank(this._outlet)) {
|
||||
return _resolveToTrue;
|
||||
}
|
||||
var next: Promise<boolean>;
|
||||
var childInstruction: Instruction = null;
|
||||
var reuse: boolean = false;
|
||||
var componentInstruction: ComponentInstruction = null;
|
||||
if (isPresent(instruction)) {
|
||||
childInstruction = instruction.child;
|
||||
componentInstruction = instruction.component;
|
||||
reuse = isBlank(instruction.component) || instruction.component.reuse;
|
||||
}
|
||||
if (reuse) {
|
||||
next = _resolveToTrue;
|
||||
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 {
|
||||
next = this._outlet.routerCanDeactivate(componentInstruction);
|
||||
return null;
|
||||
}
|
||||
// TODO: aux route lifecycle hooks
|
||||
return next.then<boolean>((result): boolean | Promise<boolean> => {
|
||||
if (result == false) {
|
||||
return false;
|
||||
}
|
||||
if (isPresent(this._childRouter)) {
|
||||
// TODO: ideally, this closure would map to async-await in Dart.
|
||||
// For now, casting to any to suppress an error.
|
||||
return <any>this._childRouter._routerCanDeactivate(childInstruction);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this router and all descendant routers according to the given instruction
|
||||
*/
|
||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||
this.currentInstruction = instruction;
|
||||
serializeUrl(url: UrlTree): string { return this._urlSerializer.serialize(url); }
|
||||
|
||||
var next: Promise<any> = _resolveToTrue;
|
||||
if (isPresent(this._outlet) && isPresent(instruction.component)) {
|
||||
var componentInstruction = instruction.component;
|
||||
if (componentInstruction.reuse) {
|
||||
next = this._outlet.reuse(componentInstruction);
|
||||
} else {
|
||||
next =
|
||||
this.deactivate(instruction).then((_) => this._outlet.activate(componentInstruction));
|
||||
}
|
||||
if (isPresent(instruction.child)) {
|
||||
next = next.then((_) => {
|
||||
if (isPresent(this._childRouter)) {
|
||||
return this._childRouter.commit(instruction.child);
|
||||
get changes(): Observable<void> { return this._changes; }
|
||||
|
||||
get routeTree(): RouteTree { return this._prevTree; }
|
||||
}
|
||||
|
||||
|
||||
class _LoadSegments {
|
||||
private deactivations: Object[][] = [];
|
||||
private performMutation: boolean = true;
|
||||
|
||||
constructor(private currTree: RouteTree, private prevTree: RouteTree) {}
|
||||
|
||||
load(parentOutletMap: RouterOutletMap, rootComponent: Object): Promise<boolean> {
|
||||
let prevRoot = isPresent(this.prevTree) ? rootNode(this.prevTree) : null;
|
||||
let currRoot = rootNode(this.currTree);
|
||||
|
||||
return this.canDeactivate(currRoot, prevRoot, parentOutletMap, rootComponent)
|
||||
.then(res => {
|
||||
this.performMutation = true;
|
||||
if (res) {
|
||||
this.loadChildSegments(currRoot, prevRoot, parentOutletMap, [rootComponent]);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var promises: Promise<any>[] = [];
|
||||
this._auxRouters.forEach((router, name) => {
|
||||
if (isPresent(instruction.auxInstruction[name])) {
|
||||
promises.push(router.commit(instruction.auxInstruction[name]));
|
||||
}
|
||||
private canDeactivate(currRoot: TreeNode<RouteSegment>, prevRoot: TreeNode<RouteSegment>,
|
||||
outletMap: RouterOutletMap, rootComponent: Object): Promise<boolean> {
|
||||
this.performMutation = false;
|
||||
this.loadChildSegments(currRoot, prevRoot, outletMap, [rootComponent]);
|
||||
|
||||
let allPaths = PromiseWrapper.all(this.deactivations.map(r => this.checkCanDeactivatePath(r)));
|
||||
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 {
|
||||
return _;
|
||||
}
|
||||
});
|
||||
}
|
||||
return curr;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
{}) :
|
||||
{};
|
||||
|
||||
currNode.children.forEach(c => {
|
||||
this.loadSegments(c, prevChildren[c.value.outlet], outletMap, components);
|
||||
StringMapWrapper.delete(prevChildren, c.value.outlet);
|
||||
});
|
||||
|
||||
return next.then((_) => PromiseWrapper.all(promises));
|
||||
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);
|
||||
|
||||
/** @internal */
|
||||
_startNavigating(): void { this.navigating = true; }
|
||||
|
||||
/** @internal */
|
||||
_finishNavigating(): void { this.navigating = false; }
|
||||
|
||||
|
||||
/**
|
||||
* Subscribe to URL updates from the router
|
||||
*/
|
||||
subscribe(onNext: (value: any) => void, onError?: (value: any) => void): Object {
|
||||
return ObservableWrapper.subscribe(this._subject, onNext, onError);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the contents of this router's outlet and all descendant outlets
|
||||
*/
|
||||
deactivate(instruction: Instruction): Promise<any> {
|
||||
var childInstruction: Instruction = null;
|
||||
var componentInstruction: ComponentInstruction = null;
|
||||
if (isPresent(instruction)) {
|
||||
childInstruction = instruction.child;
|
||||
componentInstruction = instruction.component;
|
||||
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]));
|
||||
}
|
||||
}
|
||||
var next: Promise<any> = _resolveToTrue;
|
||||
if (isPresent(this._childRouter)) {
|
||||
next = this._childRouter.deactivate(childInstruction);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (isPresent(this._outlet)) {
|
||||
next = next.then((_) => this._outlet.deactivate(componentInstruction));
|
||||
return ref.instance;
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle aux routes
|
||||
|
||||
return next;
|
||||
return outlet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a URL, returns an instruction representing the component graph
|
||||
*/
|
||||
recognize(url: string): Promise<Instruction> {
|
||||
var ancestorComponents = this._getAncestorInstructions();
|
||||
return this.registry.recognize(url, ancestorComponents);
|
||||
}
|
||||
|
||||
private _getAncestorInstructions(): Instruction[] {
|
||||
var ancestorInstructions: Instruction[] = [this.currentInstruction];
|
||||
var ancestorRouter: Router = this;
|
||||
while (isPresent(ancestorRouter = ancestorRouter.parent)) {
|
||||
ancestorInstructions.unshift(ancestorRouter.currentInstruction);
|
||||
}
|
||||
return ancestorInstructions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Navigates to either the last URL successfully navigated to, or the last URL requested if the
|
||||
* router has yet to successfully navigate.
|
||||
*/
|
||||
renavigate(): Promise<any> {
|
||||
if (isBlank(this.lastNavigationAttempt)) {
|
||||
return this._currentNavigation;
|
||||
}
|
||||
return this.navigateByUrl(this.lastNavigationAttempt);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate an `Instruction` based on the provided Route Link DSL.
|
||||
*/
|
||||
generate(linkParams: any[]): Instruction {
|
||||
var ancestorInstructions = this._getAncestorInstructions();
|
||||
return this.registry.generate(linkParams, ancestorInstructions);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RootRouter extends Router {
|
||||
/** @internal */
|
||||
_location: Location;
|
||||
/** @internal */
|
||||
_locationSub: Object;
|
||||
|
||||
constructor(registry: RouteRegistry, location: Location,
|
||||
@Inject(ROUTER_PRIMARY_COMPONENT) primaryComponent: Type) {
|
||||
super(registry, null, primaryComponent);
|
||||
this.root = this;
|
||||
this._location = location;
|
||||
this._locationSub = this._location.subscribe((change) => {
|
||||
// we call recognize ourselves
|
||||
this.recognize(change['url'])
|
||||
.then((instruction) => {
|
||||
if (isPresent(instruction)) {
|
||||
this.navigateByInstruction(instruction, isPresent(change['pop']))
|
||||
.then((_) => {
|
||||
// this is a popstate event; no need to change the URL
|
||||
if (isPresent(change['pop']) && change['type'] != 'hashchange') {
|
||||
return;
|
||||
}
|
||||
var emitPath = instruction.toUrlPath();
|
||||
var emitQuery = instruction.toUrlQuery();
|
||||
if (emitPath.length > 0 && emitPath[0] != '/') {
|
||||
emitPath = '/' + emitPath;
|
||||
}
|
||||
|
||||
// We've opted to use pushstate and popState APIs regardless of whether you
|
||||
// an app uses HashLocationStrategy or PathLocationStrategy.
|
||||
// However, apps that are migrating might have hash links that operate outside
|
||||
// angular to which routing must respond.
|
||||
// Therefore we know that all hashchange events occur outside Angular.
|
||||
// To support these cases where we respond to hashchanges and redirect as a
|
||||
// result, we need to replace the top item on the stack.
|
||||
if (change['type'] == 'hashchange') {
|
||||
if (instruction.toRootUrl() != this._location.path()) {
|
||||
this._location.replaceState(emitPath, emitQuery);
|
||||
}
|
||||
} else {
|
||||
this._location.go(emitPath, emitQuery);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._emitNavigationFail(change['url']);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.registry.configFromComponent(primaryComponent);
|
||||
this.navigateByUrl(location.path());
|
||||
}
|
||||
|
||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||
var emitPath = instruction.toUrlPath();
|
||||
var emitQuery = instruction.toUrlQuery();
|
||||
if (emitPath.length > 0 && emitPath[0] != '/') {
|
||||
emitPath = '/' + emitPath;
|
||||
}
|
||||
var promise = super.commit(instruction);
|
||||
if (!_skipLocationChange) {
|
||||
promise = promise.then((_) => { this._location.go(emitPath, emitQuery); });
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (isPresent(this._locationSub)) {
|
||||
ObservableWrapper.dispose(this._locationSub);
|
||||
this._locationSub = null;
|
||||
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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChildRouter extends Router {
|
||||
constructor(parent: Router, hostComponent) {
|
||||
super(parent.registry, parent, hostComponent, parent.root);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
|
||||
navigateByUrl(url: string, _skipLocationChange: boolean = false): Promise<any> {
|
||||
// Delegate navigation to the root router
|
||||
return this.parent.navigateByUrl(url, _skipLocationChange);
|
||||
}
|
||||
|
||||
navigateByInstruction(instruction: Instruction,
|
||||
_skipLocationChange: boolean = false): Promise<any> {
|
||||
// Delegate navigation to the root router
|
||||
return this.parent.navigateByInstruction(instruction, _skipLocationChange);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function canActivateOne(nextInstruction: Instruction,
|
||||
prevInstruction: Instruction): Promise<boolean> {
|
||||
var next = _resolveToTrue;
|
||||
if (isBlank(nextInstruction.component)) {
|
||||
return next;
|
||||
}
|
||||
if (isPresent(nextInstruction.child)) {
|
||||
next = canActivateOne(nextInstruction.child,
|
||||
isPresent(prevInstruction) ? prevInstruction.child : null);
|
||||
}
|
||||
return next.then<boolean>((result: boolean): boolean => {
|
||||
if (result == false) {
|
||||
return false;
|
||||
}
|
||||
if (nextInstruction.component.reuse) {
|
||||
return true;
|
||||
}
|
||||
var hook = getCanActivateHook(nextInstruction.component.componentType);
|
||||
if (isPresent(hook)) {
|
||||
return hook(nextInstruction.component,
|
||||
isPresent(prevInstruction) ? prevInstruction.component : null);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user