Adrian Taylor
Adrian Taylor

Reputation: 554

Keeping a component alive during routing in Angular 2

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

Answers (2)

Artem Bozhko
Artem Bozhko

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

Maximilian Riegler
Maximilian Riegler

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

Related Questions