New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular2 Routing: persisting route tabs and child routes #6634

Open
Namek opened this Issue Jan 22, 2016 · 36 comments

Comments

Projects
None yet
@Namek
Copy link

Namek commented Jan 22, 2016

(I posted the problem on SO: http://stackoverflow.com/questions/34925782/angular2-routing-persisting-route-tabs-and-child-routes but decided to ask here, too, hopefully more specific this time)

So, basically Router in Angular 2 destroys inactive components (my tabs!). The problem is I don't want this behaviour. Reason is I have some components like charts and data grid and want to switch between them fast, I don't want to recreate them.

I've found some workaround for one-level routing but it's totally not a solution.

app.ts:

    @Component({ /*...*/ })
    @RouteConfig([
        {path: '/**', redirectTo: ['Dashboard']},

        {path: '/dashboard', name: 'Dashboard', component: EmptyRoute},
        {path: '/products/...', name: 'Products', component: EmptyRoute},
        {path: '/sales', name: 'Sales', component: EmptyRoute},
        {path: '/reports', name: 'Reports', component: EmptyRoute},
    ])
    export class App {
        constructor(private router: Router) {
        }

        public isRouteActive(route) {
            return this.router.isRouteActive(this.router.generate(route))
        }
    }

As you can see component is set to EmptyRoute (just an empty view @Component with no methods and no data) and I don't use <router-outlet> but instead I instantiate my route components manually:

app.html:

    <dashboard [hidden]="!isRouteActive(['/Dashboard'])"></dashboard>
    <products-management [hidden]="!isRouteActive(['/Products'])"></products-management>
    <sales [hidden]="!isRouteActive(['/Sales'])"></sales>
    <reports [hidden]="!isRouteActive(['/Reports'])"></reports>

but of course it doesn't work for child routes. In my (existing - written with Angular 1.x) application I have /products/product/21/pricing or /products/product/21 etc. (could get rid of products/ part in the middle but whatever).

So, going back to Router. It instantiates and destroys. Ideally, I'd like to:

  1. instantiate components only once - or better - decide when I want to do this (some Strategy?)
  2. decide whether to destroy or hide components. When I hide a component I want to reuse it when route switches to the component.

EDIT on 14.11.2017: some people ask me about solution - yeah, I ditched Angular and use Elm for private projects and changed my job.

@waeljammal

This comment has been minimized.

Copy link

waeljammal commented Jan 22, 2016

I solved this problem using a custom outlet, please be aware that even though my outlet works it's not finished! I'm adding support for animation, ability to do something like data: {persist: false/true} in the routes and the ability to actually dispose the component if CanReuse returns false.

Just add it to the directives and use persistent-router-outlet instead of router-outlet. If you use it on a child route then only the children of that route would persist and if you navigate away from the root then everything gets destroyed, so if you use it everywhere then your entire route structure should persist but I have not tested that part yet, I am only using it on child routes so navigating between tab's etc. persist.

https://gist.github.com/waeljammal/467286d64f59f8340a93

@Namek

This comment has been minimized.

Copy link

Namek commented Jan 22, 2016

Wow, thanks! In the meantime I've found there's already an issue for this: #5275 - it seems I duplicate issues. Found it there: http://stackoverflow.com/questions/33648355/angular-2-swapping-between-different-components-without-destroying-them

I'll try your solution and repost how it works with my case.

@Namek

This comment has been minimized.

Copy link

Namek commented Jan 22, 2016

First of all, it indeed works for me. But:

  1. TypeScript compiler throws errors at me because of those overwritten private fields like _elementRef.
    2 I would suggest not including jQuery into that
    $(el).show() could be replaced by el.hidden = false and $(el).hide() by el.hidden = true
@waeljammal

This comment has been minimized.

Copy link

waeljammal commented Jan 22, 2016

Oh feel free to change it, I used jquery because we use a bunch of libs that require it so just ended up using it as it's there already. Force of habit from NG1 lol..

It's strange I don't get the errors, but you are right should not be overriding those properties, I'll probably update the gist shortly.

@Namek

This comment has been minimized.

Copy link

Namek commented Jan 22, 2016

It's strange I don't get the errors, but you are right should not be overriding those properties, I'll probably update the gist shortly.

Aah, tslint.

@waeljammal

This comment has been minimized.

Copy link

waeljammal commented Jan 22, 2016

I updated that gist anyway, I'll probably update it again once I finish working on the outlet.

@johnvalai

This comment has been minimized.

Copy link

johnvalai commented Jan 25, 2016

Any updates to this? I have a similar problem with Router killing my components.

@waeljammal

This comment has been minimized.

Copy link

waeljammal commented Jan 25, 2016

I'm still working on my outlet at work but I will respond here when I'm done with it, the gist I posed was my prototyping it to see if it would work for the project I'm working on, now I'm actually doing it properly but will take some time..

@Namek

This comment has been minimized.

Copy link

Namek commented Jan 25, 2016

See my little fork of that gist (or my comment on the bottom) https://gist.github.com/waeljammal/467286d64f59f8340a93. I made a small change to it for my case: I have Product component which is instantiated multiple times but shouldn't be reused.

@Namek

This comment has been minimized.

Copy link

Namek commented Jan 28, 2016

@waeljammal any news on this? Currently I have problem to distinct child components. It seems that simply identifying by componentInstruction.params is not enough since I need all params (including parent and parent's parents and so on). Or maybe URL. Couldn't find a way to get those, ideas?

@Namek

This comment has been minimized.

Copy link

Namek commented Jan 28, 2016

OK, I managed to distinct instruction path this hacky way:

private determineCurrentInstructionPath(): string {
    let key = '[]';
    let router = this.parentRouter;
    while (router !== null) {
        if (key.length > 0) {
            key = '-->' + key;
        }

        let instr = (<any>(<any>router)._currentInstruction);
        if (instr) {
            key = Json.stringify(instr.urlPath) + key;
        }

        router = router.parent;
    }

    return key;
}
@Namek

This comment has been minimized.

Copy link

Namek commented Jan 31, 2016

Anyway, it doesn't work for me. I'm not sure why but seems that wrong outlet gets notified about the route. Here's my plnkr example: http://plnkr.co/edit/MMy3azc4ksQOH6ezZIG5?p=preview
Scenario to test:

  1. Click Product 1
  2. Click Product 2
  3. Click Product 1 again
    Expected to see: "product info here" text on the bottom.
    Result: that text is not visible

Clicking on "Show Info" or "Go Buy" won't work on Product 1 tab. But when you'll look into inspector you'll see that clicking "Show Info" or "Go Buy" will make some work on instances of Product 2 tab. To visualize the bug you can comment out line 171 in persistent-router-outlet.ts:
ref.location.nativeElement.hidden = true; and repeat test scenario.

@jpleclerc

This comment has been minimized.

Copy link

jpleclerc commented Feb 29, 2016

Anything new on this issue?

@Namek

This comment has been minimized.

Copy link

Namek commented Feb 29, 2016

I gave up and ended up on ngIfs.

@chasemgray

This comment has been minimized.

Copy link

chasemgray commented Mar 11, 2016

+1. We ended up having to use the gist for now but we hope Angular provides this soon.

@Namek

This comment has been minimized.

Copy link

Namek commented May 9, 2016

If anyone is still waiting on this, it seems that @danielrasmuson made it on this gist which works for my plunker case posted before.

Angular 2 RC introduced new Router which has got much shorter code for RouterOutlet:
https://github.com/angular/angular/blob/master/modules/%40angular/router/src/directives/router_outlet.ts

than the old one: https://github.com/angular/angular/blob/master/modules/%40angular/router-deprecated/src/directives/router_outlet.ts

I anticipate it will be much somehow easier to develop component caching.

@ravinderpayal

This comment has been minimized.

Copy link

ravinderpayal commented May 12, 2016

Same problem here
StackOverflow Question

@anaratz

This comment has been minimized.

Copy link

anaratz commented May 26, 2016

Is anyone working on a port of PersistentRouterOutlet.ts so it works with Angular 2 RC, or is there a new way component caching/re-use can be achieved for routing?

@MadUser

This comment has been minimized.

Copy link

MadUser commented Jul 10, 2016

I see that router 3 beta still suffers from the same problem. No solution yet?

@vicb

This comment has been minimized.

Copy link
Contributor

vicb commented Sep 26, 2016

Please re-open an issue with a reproduction if this is still applicable to the latest router.

@vicb vicb closed this Sep 26, 2016

@Namek

This comment has been minimized.

Copy link

Namek commented Sep 26, 2016

@vicb This is a lack of feature, not a bug. Please reopen.

@zoechi

This comment has been minimized.

Copy link
Contributor

zoechi commented Sep 27, 2016

See also #7757 (comment)
Seems to be planned.

@DzmitryShylovich

This comment has been minimized.

Copy link
Contributor

DzmitryShylovich commented Jan 15, 2017

@vicb RouteReuseStrategy is already implemented. can be closed

@Namek

This comment has been minimized.

Copy link

Namek commented Jan 15, 2017

@DzmitryShylovich are you sure? This looks to be a different feature.

@DzmitryShylovich

This comment has been minimized.

Copy link
Contributor

DzmitryShylovich commented Jan 15, 2017

@Namek

1. instantiate components only once - or better - decide when I want to do this (some Strategy?)
2. decide whether to destroy or hide components. When I hide a component I want to reuse it when route switches to the component.

RouteReuseStrategy is the requested strategy.

@Namek

This comment has been minimized.

Copy link

Namek commented Jan 15, 2017

@DzmitryShylovich Thank you for your response. "Reuse" is a misleading word to me. Does that Strategy allow to toggle visibility of component (without losing it's internal state)?

@Namek

This comment has been minimized.

Copy link

Namek commented Mar 1, 2017

As far as I can understand, RouteReuseStrategy from Router 3.x still doesn't prevent destruction but just avoids instantiation. It means it won't enable me to store multiple component instances of same component type. Instead, it tries to reuse instantiated component for different data. I was hoping for simple hiding component so all <input>:s would remain their state.

"reuse" is a very bad word to describe anything in this situation, I can see it's misleading for multiple people here on GitHub and StackOverflow.

EDIT: some people ask me about solution - yeah, I ditched Angular and use Elm for private projects and changed my job.

@zh99998

This comment has been minimized.

Copy link

zh99998 commented Apr 20, 2017

I'm in same trouble.
my scene is just like tabs, some of them contains <iframe> tag.
when routing to another component, the iframe is removed from DOM tree and when back, it have to reload with lost states.

RouteReuseStrategy not works here.

@QuentinPetel

This comment has been minimized.

Copy link

QuentinPetel commented Apr 25, 2017

Same as @zh99998 ! I'm loading really heavy stuff and can't loose state each time I switch tab. Really need this feature !

@hstarorg

This comment has been minimized.

Copy link

hstarorg commented Aug 26, 2017

Same as @zh99998 too, Have a good idea to resolve it?

@zxkurama

This comment has been minimized.

Copy link

zxkurama commented Nov 6, 2017

@Namek I agree with you.,Do you have some solution to prevent navigate and hide/show tabs when I click the link tab

@todoubaba

This comment has been minimized.

Copy link
Contributor

todoubaba commented Nov 7, 2017

The following code support lazy modules and sibling navigation:

import {
    RouteReuseStrategy,
    ActivatedRouteSnapshot,
    DetachedRouteHandle,
} from '@angular/router';

function routeToUrl(route: ActivatedRouteSnapshot): string {
    if (route.url) {
        if (route.url.length) {
            return route.url.join('/');
        } else {
            if (typeof route.component === 'function') {
                return `[${route.component.name}]`;
            } else if (typeof route.component === 'string') {
                return `[${route.component}]`;
            } else {
                return `[null]`;
            }
        }
    } else {
        return '(null)';
    }
}

function calcKey(route: ActivatedRouteSnapshot) {
    let next = route;
    let url = route.pathFromRoot.map(it => routeToUrl(it)).join('/') + '*';
    while (next.firstChild) {
        next = next.firstChild;
        url += '/' + routeToUrl(next);
    }
    return url;
}

export class CustomReuseStrategy implements RouteReuseStrategy {

    handlers: { [key: string]: DetachedRouteHandle } = {};

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        console.log('CustomReuseStrategy:shouldDetach', calcKey(route));
        if (!route.routeConfig || route.routeConfig.loadChildren) {
            return false;
        } else {
            return true;
        }
    }

    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        const key = calcKey(route);
        console.log('CustomReuseStrategy:store', key, route, handle);
        this.handlers[key] = handle;
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        const key = calcKey(route);
        console.log('CustomReuseStrategy:shouldAttach', key, route);
        return !!route.routeConfig && !!this.handlers[key];
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (!route.routeConfig) {
            return null;
        }
        const key = calcKey(route);
        console.log('CustomReuseStrategy:retrieve', key, route);
        return this.handlers[calcKey(route)];
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        const furtureKey = calcKey(future);
        const currentKey = calcKey(curr);
        console.log('CustomReuseStrategy:shouldReuseRoute', furtureKey, currentKey);
        return furtureKey === currentKey;
    }
}
@nickwinger

This comment has been minimized.

Copy link

nickwinger commented Mar 13, 2018

@todoubaba Thank you, your code works flawlessly, however i have to problem that the parent (lazy) route gets constructed when i switch between child routes.
This is a huge problem, i don't know how to solve it at the moment.
My parent route is lazy loaded (via loadChildren) and all the child routes in the router-outlet are also lazy loaded.
Reusing is working, but as i have said, the constructor of the parent route gets called every time a child route changes...

@nickwinger

This comment has been minimized.

Copy link

nickwinger commented Mar 13, 2018

ok, if i comment the

while (next.firstChild) {
      next = next.firstChild;
      url += '/' + routeToUrl(next);
    }

out, it is working as i would expect it, it stores the parent components now also...
for what is this while loop ?
with what sideeffects to i have to calculate now ?

Thanks for any insight and light on this topic.

@nickwinger

This comment has been minimized.

Copy link

nickwinger commented Mar 13, 2018

ok, the problem with parent routes beeing re-regenerated lies in the shouldReuseRoute Method.
Only in this method i'm ignoring now the firstChild while loop, and it is working.
Angular is reusing the parent routes out-of-the box now

I have made the calcKey function now more flexibel:

function calcKey(route: ActivatedRouteSnapshot, withChilds: boolean = true) {
  let next = route;
  let url = route.pathFromRoot.map(it => routeToUrl(it)).join('/') + '*';

  if (withChilds) {
    while (next.firstChild) {
      next = next.firstChild;
      url += '/' + routeToUrl(next);
    }
  }

  return url;
}

and this is my shouldReuseRoute function:

shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    const furtureKey = calcKey(future, false);
    const currentKey = calcKey(curr, false);
    console.debug('CustomReuseStrategy:shouldReuseRoute', furtureKey, currentKey);
    return furtureKey === currentKey;
  }
@dmitrimaltsev

This comment has been minimized.

Copy link

dmitrimaltsev commented Mar 27, 2018

@nickwinger
It seems that with that approach child routes are regenerated every time a parent route changes.

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