Reputation: 554
I have an application written in javascript using mostly jquery. It's ugly, hard to maintain and desperately needs a framework, so I'm porting it over to be used with Angular 2
I've set up my Angular 2 app with two routes:
Route A: Contains a component with a WebGL canvas Route B: Contains a component which shows other data
Depending on the users machine, initialization of the webgl component can take quite some time.
When the user navigates from Route A to Route B and then back to Route A again, Angular 2 is re-creating and re-initializing the WebGL component... I'd like to reuse the component as it was to avoid this.
In jquery, I simply used hide and show on the canvas.
My question is very similar to:
Angular 2 keep route alive even when not navigated to it
However the solution of using CanReuse appears to be invalid as indicated here:
routerCanReuse and routerOnReuse is not called when route changes
As that is designed to handle routing to and from components of the same type.
Upvotes: 3
Views: 7528
Reputation: 1854
It is possible to keep component alive during routing by overriding the RouteReuseStrategy.
Thanks to this great post Angular 2 Component Reuse Strategy
Upvotes: 2
Reputation: 23506
This is currently not possible with what Angular 2 provides us. You can however implement your own RouterOutlet which saves and restores Components on routing.
The following is for the Beta release of Angular 2:
There was a discussion on the angular github a few days ago and they came up with this workaround.
In your root component just replace the <router-outlet></router-outlet>
with <persistent-router-outlet></persistent-router-outlet>
, import PersistentRouterOutlet
and add it to the directives.
Pasted here for backup reasons:
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]);
}
public removeRef(type: any){
delete this.cache[type];
}
public remove(ref){
// remove self from cache
this.removeRef(ref.componentType);
// remove children
var parent = ref.location.nativeElement;
Object.keys(this.cache).forEach((key)=>{
var child = this.cache[key].componentRef.location.nativeElement;
if (parent.contains(child)){
this.removeRef(key)
}
})
// will destroy component and all children
// sense this is an angular call, all lifecycle methods will be called
ref.dispose();
}
}
const refCache = new RefCache();
/**
* 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 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 (!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) => {
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 = refCache.getRef(nextInstruction.componentType);
ref.componentRef.location.nativeElement.style.display = 'initial';
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 = 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)) {
if (ref.instance.routerCanReuse && ref.instance.routerCanReuse(nextInstruction, ref.componentType)){
ref.location.nativeElement.style.display = 'none';
} else{
refCache.remove(ref);
}
}
});
}
/**
* 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 = 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 = 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);
}
}
Upvotes: 0