angularangular-routingangular-animationsangular-lazyloading

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.


Solution

  • 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.