htmlcssangularangular2-routing

Show loading screen when navigating between routes in Angular 2


How do I show a loading screen when I change a route in Angular 2?


Solution

  • The current Angular Router provides Navigation Events. You can subscribe to these and make UI changes accordingly. Remember to count in other Events such as NavigationCancel and NavigationError to stop your spinner in case router transitions fail.

    app.component.ts - your root component

    ...
    import {
      Router,
      // import as RouterEvent to avoid confusion with the DOM Event
      Event as RouterEvent,
      NavigationStart,
      NavigationEnd,
      NavigationCancel,
      NavigationError
    } from '@angular/router'
    
    @Component({})
    export class AppComponent {
    
      // Sets initial value to true to show loading spinner on first load
      loading = true
    
      constructor(private router: Router) {
        this.router.events.subscribe((e : RouterEvent) => {
           this.navigationInterceptor(e);
         })
      }
    
      // Shows and hides the loading spinner during RouterEvent changes
      navigationInterceptor(event: RouterEvent): void {
        if (event instanceof NavigationStart) {
          this.loading = true
        }
        if (event instanceof NavigationEnd) {
          this.loading = false
        }
    
        // Set loading state to false in both of the below events to hide the spinner in case a request fails
        if (event instanceof NavigationCancel) {
          this.loading = false
        }
        if (event instanceof NavigationError) {
          this.loading = false
        }
      }
    }
    

    app.component.html - your root view

    <div class="loading-overlay" *ngIf="loading">
        <!-- show something fancy here, here with Angular 2 Material's loading bar or circle -->
        <md-progress-bar mode="indeterminate"></md-progress-bar>
    </div>
    

    Performance Improved Answer: If you care about performance there is a better method, it is slightly more tedious to implement but the performance improvement will be worth the extra work. Instead of using *ngIf to conditionally show the spinner, we could leverage Angular's NgZone and Renderer to switch on / off the spinner which will bypass Angular's change detection when we change the spinner's state. I found this to make the animation smoother compared to using *ngIf or an async pipe.

    This is similar to my previous answer with some tweaks:

    app.component.ts - your root component

    ...
    import {
      Router,
      // import as RouterEvent to avoid confusion with the DOM Event
      Event as RouterEvent,
      NavigationStart,
      NavigationEnd,
      NavigationCancel,
      NavigationError
    } from '@angular/router'
    import {NgZone, Renderer, ElementRef, ViewChild} from '@angular/core'
    
    
    @Component({})
    export class AppComponent {
    
      // Instead of holding a boolean value for whether the spinner
      // should show or not, we store a reference to the spinner element,
      // see template snippet below this script
      @ViewChild('spinnerElement')
      spinnerElement: ElementRef
    
      constructor(private router: Router,
                  private ngZone: NgZone,
                  private renderer: Renderer) {
        router.events.subscribe(this._navigationInterceptor)
      }
    
      // Shows and hides the loading spinner during RouterEvent changes
      private _navigationInterceptor(event: RouterEvent): void {
        if (event instanceof NavigationStart) {
          // We wanna run this function outside of Angular's zone to
          // bypass change detection
          this.ngZone.runOutsideAngular(() => {
            // For simplicity we are going to turn opacity on / off
            // you could add/remove a class for more advanced styling
            // and enter/leave animation of the spinner
            this.renderer.setElementStyle(
              this.spinnerElement.nativeElement,
              'opacity',
              '1'
            )
          })
        }
        if (event instanceof NavigationEnd) {
          this._hideSpinner()
        }
        // Set loading state to false in both of the below events to
        // hide the spinner in case a request fails
        if (event instanceof NavigationCancel) {
          this._hideSpinner()
        }
        if (event instanceof NavigationError) {
          this._hideSpinner()
        }
      }
    
      private _hideSpinner(): void {
        // We wanna run this function outside of Angular's zone to
        // bypass change detection,
        this.ngZone.runOutsideAngular(() => {
          // For simplicity we are going to turn opacity on / off
          // you could add/remove a class for more advanced styling
          // and enter/leave animation of the spinner
          this.renderer.setElementStyle(
            this.spinnerElement.nativeElement,
            'opacity',
            '0'
          )
        })
      }
    }
    

    app.component.html - your root view

    <div class="loading-overlay" #spinnerElement style="opacity: 0;">
        <!-- md-spinner is short for <md-progress-circle mode="indeterminate"></md-progress-circle> -->
        <md-spinner></md-spinner>
    </div>