Tovar
Tovar

Reputation: 455

Angular Router - how to destroy old component immediately when routerLink changes

In Angular, after a routerLink triggers a route change, the old component is only destroyed basically at the same time as the new component's initialization: timeline of the Angular Router's default behaviour

This is being a problem for me because I want to show a spinner after the old component is destroyed and before the new component is built -- this is relevant when a component/module is lazy-loaded. Since the old component is only destroyed as late as the new component is being built, my current spinner is just laying over the old component while the new component is loaded.

So is there a way to config the router so it destroys the old component as soon as routerLink is triggered? Like this: timeline of the behaviour I desire for the Angular Router in my use case

I should also mention that I don't want to hide the router-outlet in any way because I'm also applying animations to the entering/leaving components, including the spinner.

Here's what that code is looking like, if it matters (it's app.component and I'm using Tailwind, btw):

@Component({
  styles: `
    :host ::ng-deep router-outlet + * { // target the routed components and the spinner
      &, & + * {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
      }
    }

    .router-outlet-container {
      position: relative;
      height: 100%;
      width: 100%;
    }

    .spinner-outer-container {
      position: relative;
      height: 100%;
      width: 100%;
    }

    .spinner-inner-container {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translateX(-50%) translateY(-50%);
    }
  `,
  animations: [
    trigger('routeAnimations', [
      transition('* <=> *', [
        query(':enter, :leave', [ style({
          position: 'absolute', left: 0, width: '100%', overflow: 'hidden',
        }) ], { optional: true }),
        group([
          query(':enter', [
            style({ opacity: '0' }),
            animate(animation, style({ opacity: '1' })),
          ], { optional: true }),
          query(':leave', [
            style({ opacity: '1' }),
            animate(animation, style({ opacity: '0' })),
          ], { optional: true }),
        ]),
      ]),
    ]),
  ],
})
export class MyComponent {
  #router = inject(Router);

  protected route = new Subject<string>();
  #lazyLoadingStarted$ = this.#router.events.pipe(
      filter(v => v instanceof RouteConfigLoadStart),
      map(() => '__lazy_loading__'),
  );
  protected routingState = toSignal(this.#lazyLoadingStarted$.pipe(mergeWith(this.route)));
}
<div [@routeAnimations]=routingState() class="router-outlet-container">
    <router-outlet #outlet="outlet" (activate)="route.next(outlet.activatedRoute.snapshot.url[0]?.path || '')"/>
    @if (routingState() === '__lazy_loading__') {
        <div class="spinner-outer-container">
            <div class="spinner-inner-container">
                <mat-spinner/>
            </div>
        </div>
    }
</div>

Update

my current spinner is just laying over the old component

I found out a little way to circumvent this. Basically turned the spinner's container into an overlay. Only had to change styles and the spinner's container:

:host {
  height: 100vh;
  width: 100vw;
  display: flex;
  flex-direction: column:

  .spinner-overlay, ::ng-deep router-outlet + * { // target the spinner and the routed components
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
  }

  .spinner-overlay {
    width: 100%;
    z-index: 10;
    background: radial-gradient(hsla(0, 0%, 100%, 0.79), hsla(0, 0%, 100%, 0.17));
  }
}
<div class="spinner-overlay">
    <div class="spinner-inner-container">
        <mat-spinner/>
    </div>
</div>

Meaning this question no longer causes problems for my use case, so now I'm only looking for an answer out of curiosity because there are probably other use cases where fine-grained customization of Router behaviour may be important.

Upvotes: 1

Views: 223

Answers (2)

JohnnyDevNull
JohnnyDevNull

Reputation: 982

For me it sounds better to subscribe to Angular Router Events and build your spinner around that instead of the Lifecycle hooks.

For example, while NavigationStart you set somewhere a flag like showSpinner = true and at the NavigationEnd you set somewhere a flag like showSpinner = false. You could also build a service around that, that has some more logic about the current destructuring component and then the new one to maintain a robust solution.

Another alternative would be CanActivate und canDeactivate Route Guard to trigger a Subject, which sets the Spinner to true/false.

And last but not least another variant could be to write your own CustomRouteReuseStrategy and provide it globally in your app. With that you have full control over the router components lifecycle, but use it carefully, because it needs some core knowledge to do it properly.

Upvotes: 0

kemsky
kemsky

Reputation: 15279

Router may never destroy previous component, it can cache it for reuse. Previous component is not removed from DOM until router loads lazy module. You can wrap router outlet in custom component, subscribe to router events and show/hide outlet when needed e.g. apply visibility: hidden.

Upvotes: 1

Related Questions