import * as hookMod from 'angular2/src/router/lifecycle_annotations'; import * as routerMod from 'angular2/src/router/router'; import {isBlank, isPresent, Json} from 'angular2/src/facade/lang'; import {StringMapWrapper} from 'angular2/src/facade/collection'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {BaseException} from 'angular2/src/facade/exceptions'; import { ElementRef, DynamicComponentLoader, Directive, Injector, provide, ComponentRef, Attribute } from 'angular2/core'; import { ComponentInstruction, CanReuse, OnReuse, CanDeactivate, RouterOutlet, OnActivate, Router, RouteData, RouteParams, OnDeactivate } from 'angular2/router'; import {hasLifecycleHook} from 'angular2/src/router/route_lifecycle_reflector'; /** * Reference Cache Entry */ class RefCacheItem { constructor(public componentRef: ComponentRef) { } } /** * Reference Cache Entry Key to differentiate * same componentType with different parameters. */ interface RefKey extends String { } /** * Reference Cache */ class RefCache { private cache: any = {}; public static toKey(instr: ComponentInstruction): RefKey { return instr.componentType + Json.stringify(instr.params) } public getRef(key: RefKey) { return this.cache[key]; } public addRef(key: RefKey, ref: RefCacheItem) { this.cache[key] = ref; } public hasRef(key: RefKey): boolean { return !isBlank(this.cache[key]); } } /** * An outlet that persists the child views and re-uses their components. * * @author Wael Jammal */ @Directive({selector: 'persistent-router-outlet'}) export class PersistentRouterOutlet extends RouterOutlet { private currentInstruction: ComponentInstruction; private currentElementRef; private refCache: RefCache = new RefCache(); private resolveToTrue = PromiseWrapper.resolve(true); private currentComponentRef: ComponentRef; constructor(elementRef: ElementRef, private loader: DynamicComponentLoader, private parentRouter: Router, @Attribute('name') nameAttr: string) { super(elementRef, loader, parentRouter, nameAttr); this.currentElementRef = elementRef; } /** * Called by the Router to instantiate a new component during the commit phase of a navigation. * This method in turn is responsible for calling the `routerOnActivate` hook of its child. */ public activate(nextInstruction: ComponentInstruction): Promise { let previousInstruction = this.currentInstruction; this.currentInstruction = nextInstruction; let refKey = RefCache.toKey(nextInstruction) if (!this.refCache.hasRef(refKey)) { let componentType = nextInstruction.componentType; let childRouter = this.parentRouter.childRouter(componentType); let providers = Injector.resolve([ provide(RouteData, {useValue: nextInstruction.routeData}), provide(RouteParams, {useValue: new RouteParams(nextInstruction.params)}), provide(routerMod.Router, {useValue: childRouter}) ]); return this.loader.loadNextToLocation(componentType, this.currentElementRef, providers) .then((componentRef) => { this.refCache.addRef(refKey, new RefCacheItem(componentRef)); this.currentComponentRef = componentRef; if (hasLifecycleHook(hookMod.routerOnActivate, componentType)) { return (componentRef.instance) .routerOnActivate(nextInstruction, previousInstruction); } }); } else { let ref = this.refCache.getRef(refKey); ref.componentRef.location.nativeElement.hidden = false; this.currentComponentRef = ref.componentRef; return PromiseWrapper.resolve( hasLifecycleHook(hookMod.routerOnReuse, this.currentInstruction.componentType) ? (ref.componentRef.instance).routerOnReuse(nextInstruction, previousInstruction) : true ); } } /** * Called by the Router during the commit phase of a navigation when an outlet * reuses a component between different routes. * This method in turn is responsible for calling the `routerOnReuse` hook of its child. */ public reuse(nextInstruction: ComponentInstruction): Promise { let previousInstruction = this.currentInstruction; this.currentInstruction = nextInstruction; if (isBlank(this.currentComponentRef)) { throw new BaseException(`Cannot reuse an outlet that does not contain a component.`); } let refKey = RefCache.toKey(nextInstruction) let ref = this.refCache.getRef(refKey); let currentRef = ref ? ref.componentRef : null; return PromiseWrapper.resolve( hasLifecycleHook(hookMod.routerOnReuse, this.currentInstruction.componentType) ? (currentRef.instance).routerOnReuse(nextInstruction, previousInstruction) : true ); } /** * Called by the Router when an outlet disposes of a component's contents. * This method in turn is responsible for calling the `routerOnDeactivate` hook of its child. */ public deactivate(nextInstruction: ComponentInstruction): Promise { let next = this.resolveToTrue; let ref = this.currentComponentRef; if (isPresent(ref) && isPresent(this.currentInstruction) && hasLifecycleHook(hookMod.routerOnDeactivate, this.currentInstruction.componentType)) { next = PromiseWrapper.resolve( (ref.instance) .routerOnDeactivate(nextInstruction, this.currentInstruction)); } return next.then(() => { if (isPresent(ref)) { ref.location.nativeElement.hidden = true; } }); } /** * Called by the Router during recognition phase of a navigation. * * If this resolves to `false`, the given navigation is cancelled. * * This method delegates to the child component's `routerCanDeactivate` hook if it exists, * and otherwise resolves to true. */ public routerCanDeactivate(nextInstruction: ComponentInstruction): Promise { if (isBlank(this.currentInstruction)) { return this.resolveToTrue; } let ref = this.currentComponentRef; if (!ref) { let refKey = RefCache.toKey(nextInstruction) let foundRef = this.refCache.getRef(refKey); ref = foundRef ? foundRef.componentRef : null; } if (hasLifecycleHook(hookMod.routerCanDeactivate, this.currentInstruction.componentType)) { return PromiseWrapper.resolve( (ref.instance) .routerCanDeactivate(nextInstruction, this.currentInstruction)); } return this.resolveToTrue; } /** * Called by the Router during recognition phase of a navigation. * * If the new child component has a different Type than the existing child component, * this will resolve to `false`. You can't reuse an old component when the new component * is of a different Type. * * Otherwise, this method delegates to the child component's `routerCanReuse` hook if it exists, * or resolves to true if the hook is not present. */ public routerCanReuse(nextInstruction: ComponentInstruction): Promise { let result; let ref = this.currentComponentRef; if (!ref) { let refKey = RefCache.toKey(nextInstruction) let foundRef = this.refCache.getRef(refKey); ref = foundRef ? foundRef.componentRef : null; } if (isBlank(this.currentInstruction) || this.currentInstruction.componentType !== nextInstruction.componentType) { result = false; } else if (hasLifecycleHook(hookMod.routerCanReuse, this.currentInstruction.componentType)) { result = (ref.instance) .routerCanReuse(nextInstruction, this.currentInstruction); } else { result = nextInstruction === this.currentInstruction || (isPresent(nextInstruction.params) && isPresent(this.currentInstruction.params) && StringMapWrapper.equals(nextInstruction.params, this.currentInstruction.params)); } return PromiseWrapper.resolve(result); } }