Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save martinnormark/a27f8b7eecb89904fbf8 to your computer and use it in GitHub Desktop.
Save martinnormark/a27f8b7eecb89904fbf8 to your computer and use it in GitHub Desktop.
Angular 2 Persistent Router
"use strict";
import * as hookMod from 'angular2/src/router/lifecycle/lifecycle_annotations';
import * as routerMod from 'angular2/src/router/router';
import {isBlank, isPresent} 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/lifecycle/route_lifecycle_reflector';
/**
* Reference Cache Entry
*/
class RefCacheItem {
constructor(public componentRef: ComponentRef) {
}
}
/**
* Reference Cache
*/
class RefCache {
private cache: any = {};
public getRef(type: any) {
return this.cache[type];
}
public addRef(type: any, ref: RefCacheItem) {
this.cache[type] = ref;
}
public hasRef(type: any): boolean {
return !isBlank(this.cache[type]);
}
}
/**
* 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<any> {
let previousInstruction = this.currentInstruction;
this.currentInstruction = nextInstruction;
if (!this.refCache.hasRef(nextInstruction.urlPath)) {
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(nextInstruction.urlPath, new RefCacheItem(componentRef));
this.currentComponentRef = componentRef;
if (hasLifecycleHook(hookMod.routerOnActivate, componentType)) {
return (<OnActivate>componentRef.instance)
.routerOnActivate(nextInstruction, previousInstruction);
}
});
}
else {
let ref = this.refCache.getRef(nextInstruction.urlPath);
ref.componentRef.location.nativeElement.hidden = false;
this.currentComponentRef = ref.componentRef;
return PromiseWrapper.resolve(
hasLifecycleHook(hookMod.routerOnReuse, this.currentInstruction.componentType) ?
(<OnReuse>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<any> {
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 ref = this.refCache.getRef(nextInstruction.urlPath);
let currentRef = ref ? ref.componentRef : null;
return PromiseWrapper.resolve(
hasLifecycleHook(hookMod.routerOnReuse, this.currentInstruction.componentType) ?
(<OnReuse>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<any> {
let next = this.resolveToTrue;
let ref = this.currentComponentRef;
if (isPresent(ref) && isPresent(this.currentInstruction) &&
hasLifecycleHook(hookMod.routerOnDeactivate, this.currentInstruction.componentType)) {
next = PromiseWrapper.resolve(
(<OnDeactivate>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<boolean> {
if (isBlank(this.currentInstruction)) {
return this.resolveToTrue;
}
let ref = this.currentComponentRef;
if (!ref) {
let foundRef = this.refCache.getRef(this.currentInstruction.urlPath);
ref = foundRef ? foundRef.componentRef : null;
}
if (hasLifecycleHook(hookMod.routerCanDeactivate, this.currentInstruction.componentType)) {
return PromiseWrapper.resolve(
(<CanDeactivate>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<boolean> {
let result;
let ref = this.currentComponentRef;
if (!ref) {
let foundRef = this.refCache.getRef(nextInstruction.urlPath);
ref = foundRef ? foundRef.componentRef : null;
}
if (isBlank(this.currentInstruction) || !this.refCache.hasRef(nextInstruction.urlPath) || this.currentInstruction.componentType !== nextInstruction.componentType) {
result = false;
} else if (hasLifecycleHook(hookMod.routerCanReuse, this.currentInstruction.componentType)) {
result = (<CanReuse>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);
}
}
@martinnormark
Copy link
Author

This will cache the component using the urlPath property of the ComponentInstruction type.

In the component types that needs to support reuse, implement the CanReuse interface as follows:

routerCanReuse(next: ComponentInstruction, prev: ComponentInstruction) {
    return next.urlPath === prev.urlPath;
}

@vinaysoni
Copy link

Also learnt some typescript sophistry from your post:

private cache: any = {};
cache can be used as a Hashmap:
this.cache[type] = ref;

type any can be used as a map - just so different. No need for Collections.Dictionary
Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment