Skip to content

Instantly share code, notes, and snippets.

@waeljammal
Last active June 21, 2021 06:35
Show Gist options
  • Save waeljammal/467286d64f59f8340a93 to your computer and use it in GitHub Desktop.
Save waeljammal/467286d64f59f8340a93 to your computer and use it in GitHub Desktop.
Angular 2 Persistent Router
import * as hookMod from 'angular2/src/router/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/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.componentType)) {
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.componentType, 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.componentType);
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.componentType);
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.componentType);
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.componentType);
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 = (<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);
}
}
@waeljammal
Copy link
Author

Please note that this is still a work in progress!

Things still incomplete:

  • Dispose persistent component if routerCanReuse returns false
  • Implement data: {persist: true/false} in router config routes
  • Implement data: {animate: true/false} in router config routes
  • Clean up and document
  • Proper caching registry for components, this is just a temp one for storing the references.

@Namek
Copy link

Namek commented Jan 22, 2016

@waeljammal $ <-- is that jQuery?

  1. $(el).show() could be replaced by el.hidden = false and $(el).hide() by el.hidden = true
  2. TypeScript compiler throws errors at me because of those overwritten private fields like _elementRef.

@waeljammal
Copy link
Author

I have removed the JQuery stuff, we have JQuery running in our project and so just used it out of habbit lol.

@Namek
Copy link

Namek commented Jan 25, 2016

@waeljammal I have multiple dynamically added tabs based on single component type. I have implemented CanReuse to return false but the component's constructor is called only once and actually the first created component is reused for every tab. Am I missing something or is that a bug?

EDIT: OK, found it. It's because you cache references by component type. I'm gonna need to differ by component type + params probably.
EDIT 2: here's my modification https://gist.github.com/Namek/6658d08b539f81c7fe88/revisions

@waeljammal
Copy link
Author

It's incomplete, if you read my comment above you will see in my list of things to do I have stated that dispose still need's to be called when canReuse returns false. But it's easy enough to do :)

@danrasmuson
Copy link

this worked for me! Thank you!

@martinnormark
Copy link

In case someone needs a way to reuse components based on the URL, I forked this gist and modified to do just that: https://gist.github.com/martinnormark/a27f8b7eecb89904fbf8

Very useful in a tabbed app, where the tab items you click are just route links, and the tab content is then reused based in the urlPath and injected into the persistent-router-outlet.

@danrasmuson
Copy link

I forked this gist and made three changes.

  1. Implemented routerCanReuse. If false is returned the component will be destroyed.
  2. Allowed for nested <persistent-router-outlet>
  3. Changed persistent components to be display: none instead of hidden so they won't require compute time when the page is redrawn.

https://gist.github.com/danielrasmuson/89cde9ce22d89167cec6d7f9a9240558

@anaratz
Copy link

anaratz commented May 25, 2016

Hello, I'm trying to port this code from Angular2 Beta to RC as a first step to get my app working again.
(PersistentRouterOutlet worked very well for me before I upgraded)

Changing the import statements was okay, but not sure how to get rid of the last errors to get it to build, has anyone achieved this, or can shed any light on what I need to do?

routererrors

import * as hookMod from "@angular/router-deprecated/src/lifecycle/lifecycle_annotations"; import * as routerMod from "@angular/router-deprecated/src/router"; import {isBlank, isPresent} from "@angular/router-deprecated/src/facade/lang"; import {StringMapWrapper} from "@angular/router-deprecated/src/facade/collection"; import {PromiseWrapper} from "@angular/router-deprecated/src/facade/async"; import {BaseException} from "@angular/router-deprecated/src/facade/exceptions"; import {ElementRef, DynamicComponentLoader, Directive, Injector, provide, ComponentRef, Attribute} from "@angular/core"; import {ComponentInstruction, CanReuse, OnReuse, CanDeactivate, RouterOutlet, OnActivate, Router, RouteData, RouteParams, OnDeactivate} from "@angular/router-deprecated"; import {hasLifecycleHook} from "@angular/router-deprecated/src/lifecycle/route_lifecycle_reflector";

@sergey-koretsky
Copy link

hi, any progress for RC 1 ?

@vinaysoni
Copy link

Hi Wael,
It appears that the RouterAPI has changed significantly.

The RouterOutlet methods look very different:

constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string)
outletMap : RouterOutletMap
activateEvents : EventEmitter
deactivateEvents : EventEmitter
ngOnDestroy() : void
locationInjector : Injector
locationFactoryResolver : ComponentFactoryResolver
isActivated : boolean
component : Object
activatedRoute : ActivatedRoute
detach() : ComponentRef
attach(ref: ComponentRef, activatedRoute: ActivatedRoute)
deactivate() : void
activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap) : void
activateWith(activatedRoute: ActivatedRoute, resolver?: ComponentFactoryResolver|, outletMap: RouterOutletMap)

Can you please post an updated version of your solution. This would be immensely helpful to everyone using Angular2.
Thanks,
Vinay

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