Show loading screen when navigating between routes in Angular 2

113,950

Solution 1

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>

Solution 2

UPDATE:3 Now that I have upgraded to new Router, @borislemke's approach will not work if you use CanDeactivate guard. I'm degrading to my old method, ie: this answer

UPDATE2: Router events in new-router look promising and the answer by @borislemke seems to cover the main aspect of spinner implementation, I havent't tested it but I recommend it.

UPDATE1: I wrote this answer in the era of Old-Router, when there used to be only one event route-changed notified via router.subscribe(). I also felt overload of the below approach and tried to do it using only router.subscribe(), and it backfired because there was no way to detect canceled navigation. So I had to revert back to lengthy approach(double work).


If you know your way around in Angular2, this is what you'll need


Boot.ts

import {bootstrap} from '@angular/platform-browser-dynamic';
import {MyApp} from 'path/to/MyApp-Component';
import { SpinnerService} from 'path/to/spinner-service';

bootstrap(MyApp, [SpinnerService]);

Root Component- (MyApp)

import { Component } from '@angular/core';
import { SpinnerComponent} from 'path/to/spinner-component';
@Component({
  selector: 'my-app',
  directives: [SpinnerComponent],
  template: `
     <spinner-component></spinner-component>
     <router-outlet></router-outlet>
   `
})
export class MyApp { }

Spinner-Component (will subscribe to Spinner-service to change the value of active accordingly)

import {Component} from '@angular/core';
import { SpinnerService} from 'path/to/spinner-service';
@Component({
  selector: 'spinner-component',
  'template': '<div *ngIf="active" class="spinner loading"></div>'
})
export class SpinnerComponent {
  public active: boolean;

  public constructor(spinner: SpinnerService) {
    spinner.status.subscribe((status: boolean) => {
      this.active = status;
    });
  }
}

Spinner-Service (bootstrap this service)

Define an observable to be subscribed by spinner-component to change the status on change, and function to know and set the spinner active/inactive.

import {Injectable} from '@angular/core';
import {Subject} from 'rxjs/Subject';
import 'rxjs/add/operator/share';

@Injectable()
export class SpinnerService {
  public status: Subject<boolean> = new Subject();
  private _active: boolean = false;

  public get active(): boolean {
    return this._active;
  }

  public set active(v: boolean) {
    this._active = v;
    this.status.next(v);
  }

  public start(): void {
    this.active = true;
  }

  public stop(): void {
    this.active = false;
  }
}

All Other Routes' Components

(sample):

import { Component} from '@angular/core';
import { SpinnerService} from 'path/to/spinner-service';
@Component({
   template: `<div *ngIf="!spinner.active" id="container">Nothing is Loading Now</div>`
})
export class SampleComponent {

  constructor(public spinner: SpinnerService){} 

  ngOnInit(){
    this.spinner.stop(); // or do it on some other event eg: when xmlhttp request completes loading data for the component
  }

  ngOnDestroy(){
    this.spinner.start();
  }
}

Solution 3

Why not just using simple css :

<router-outlet></router-outlet>
<div class="loading"></div>

And in your styles :

div.loading{
    height: 100px;
    background-color: red;
    display: none;
}
router-outlet + div.loading{
    display: block;
}

Or even we can do this for the first answer:

<router-outlet></router-outlet>
<spinner-component></spinner-component>

And then simply just

spinner-component{
   display:none;
}
router-outlet + spinner-component{
    display: block;
}

The trick here is, the new routes and components will always appear after router-outlet , so with a simple css selector we can show and hide the loading.

Solution 4

If you have special logic required for the first route only you can do the following:

AppComponent

    loaded = false;

    constructor(private router: Router....) {
       router.events.pipe(filter(e => e instanceof NavigationEnd), take(1))
                    .subscribe((e) => {
                       this.loaded = true;
                       alert('loaded - this fires only once');
                   });

I had a need for this to hide my page footer, which was otherwise appearing at the top of the page. Also if you only want a loader for the first page you can use this.

Solution 5

You could also use this existing solution. The demo is here. It looks like youtube loading bar. I just found it and added it to my own project.

Share:
113,950
Lucas
Author by

Lucas

Updated on February 10, 2020

Comments

  • Lucas
    Lucas over 4 years

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

  • Ankit Singh
    Ankit Singh about 8 years
    see this also. I don't how much Animation does angular support by default.
  • Lucas
    Lucas about 8 years
    can you help me write up the service for the spinner? I can do the animations and whatnot in CSS myself, but it would be nice if you could help me with the service.
  • Ankit Singh
    Ankit Singh about 8 years
    see this implemetation also, same but not exactly
  • Ankit Singh
    Ankit Singh about 8 years
    I've added some code for spinner-service now you just need to other parts to make it work. And remember it's for angular2-rc-1
  • Lucas
    Lucas about 8 years
    is something else needed for the component page on which I want to include the spinner? It's currently not working for me, and I think it's because I need to include the spinner somehow...
  • Ankit Singh
    Ankit Singh about 8 years
    I didn't include any import paths, because that's just redundant without knowing your file structure. Add them as per your file structure
  • Lucas
    Lucas about 8 years
    hi, one more question - can you please check the code for all the other routes' components? how does it get the SpinnerService when it's included the SpinnerComponent?
  • Ankit Singh
    Ankit Singh about 8 years
    sorry, change it to service
  • Ankit Singh
    Ankit Singh over 7 years
    Great, I appreciate your approach. It has hit me before and I replaced my lengthy method with this appealing idea, than I had to revert becuase I lost control over router navigation and it triggering the spinner and then not being able to stop it. navigationInterceptor seems like a solution but I'm not sure if something else is waiting to break. If mixed with the async requests it will introduce the problem again, I think.
  • borislemke
    borislemke over 7 years
    @A_Singh did you set the loading state to false within NavigationCancel and NavigationError as well? You would want to set the variable to false within the other 2 conditions in case a HTTP request fails(in case of 404s etc.) By the way, I've used this approach in many different projects at our company with no issues at all. You could also use the navigationInterceptor function within components in case you want to manually set which pages need the loading spinner and which don't
  • Ankit Singh
    Ankit Singh over 7 years
    @borislemke I did it using the old-Router. I haven't tried the new one. I think your last edit will solve most of the issues I assumed. :)
  • Rodney
    Rodney over 7 years
    This is a good simple solution for navigation, however when the target route/page needs data then you will need to show the spinner again. Is there an event that can fire at navigation level AFTER the target page's OnInit event has completed? (ie. after the target data has been loaded?)
  • borislemke
    borislemke over 7 years
    @Rodney you could create a service and connect the above navigationInterceptor with any component through it. Have a look at "component interactions through a service" in this link: angular.io/docs/ts/latest/cookbook/…
  • Milad
    Milad over 7 years
    Creating an extra service to handle something simple and then adding it everywhere, and implementing ngOnInit and ngONDestroy just to stop and start animation , this is too much /
  • suryakiran
    suryakiran over 7 years
    @borislemke - The loading div is never getting displayed, but when I set the loading as true it is getting displayed. I have debugged and found that though the loading is set to true the loading div is not getting displayed. Can you tell me the problem. I have tried the similar way as provided in stackoverflow.com/questions/38637176/…
  • Persk
    Persk over 7 years
    I tried to use this but it doesn't wait css animation.You can't animate before route because it will route immediately
  • Rahul Singh
    Rahul Singh over 7 years
    great example and very simple , but writing this code in every component dosen't give us modularity can we make a directive out of the same ?
  • borislemke
    borislemke over 7 years
    @RahulSingh you don't have to write it in "every" component. Only the root component.
  • Rahul Singh
    Rahul Singh over 7 years
    @borislemke so where where we make use of router resolve , it will work there right? one more thing i am trying to use the md-progress-bar of material but it just dosen't show up, how to make that work , i am using webpack angular 2
  • techguy2000
    techguy2000 over 7 years
    Maybe this worked at some point? It's not working now with Angular 2.4.8. The page is synchronous. The Spinner doesn't get rendered until the whole page/parent component renders, and that the at NavigationEnd. That defeats the point of the spinner
  • Jhonatas Kleinkauff
    Jhonatas Kleinkauff over 7 years
    actually, this is awesome and works, my tip for testing purposes you can delay the stop of spinner with setTimeout(() => this.spinner.stop(), 5000) in ngOnInit
  • Praveen Rana
    Praveen Rana about 7 years
    <router-outlet> doesn't allow us to pass value from component to the parent component , so it will be a bit complex to hide the loading div.
  • mellis481
    mellis481 about 7 years
    Using opacity is not a great choice. Visually, it's fine, but in Chrome, if the loading image/icon is on top of a button or text, you cannot access the button. I changed it to use display either none or inline.
  • borislemke
    borislemke about 7 years
    @im1dermike as I said, "for simplicity..." of the example
  • mellis481
    mellis481 about 7 years
    How is display any less simple?
  • borislemke
    borislemke about 7 years
    @im1dermike I said "of the example". If you wanna use display, go ahead. If you want to remove the DOM element, go ahead, if you want to refresh the browser, go ahead. It's an example, not something you HAVE to do.
  • d123546
    d123546 about 7 years
    I like this approach, good job man! But I have an issue and I dont understand why, if I wont switch animation on NavigationEnd, I can see the spinner loading, but if I switch to false, routes change so fast so that I cant even see any animations :( I have tried with even network throteling to slow down the connection but its still remains the same :( no loading at all. Could you give me any suggestions on this please. I control the animations by adding and removing class at the loading element. Thanks
  • borislemke
    borislemke about 7 years
    I think this method is only good if you are using lazy loaded Modules or ResolveData to load data on router transitions. If you offload data fetches into components instead of ResolveData within the router, this method is no good.
  • Machado
    Machado over 6 years
    You should add the Angular version to the answer.
  • borislemke
    borislemke over 6 years
    @Machado this is the "Angular version". If you are talking about AngularJS, then that's out of the questions. They are 2 very different things.
  • Manu Chadha
    Manu Chadha about 6 years
    I got this error when I copied your code 'md-spinner' is not a known element:. I am quite new to Angular. Could you please tell me what might be the mistake?
  • borislemke
    borislemke about 6 years
    @ManuChadha you might need to use mat-spinner instead depending on the material version you're using. Also make sure you have the @angular/material package installed
  • johnstaveley
    johnstaveley over 5 years
    It's a useful answer as I used the Navigation interception code to show the spinners shown by this npm package: npmjs.com/package/ngx-loading
  • CularBytes
    CularBytes over 5 years
    With *ngIf it didn't work (Angular 7), the last performance suggestion works perfectly fine with mat-progress-bar
  • aruno
    aruno over 5 years
    Also it can be super annoying if your application has a lot of route changes to see the spinner every single time instantly. It’s better to use RxJs and set a debounced timer so it only appears after a short delay.
  • erikscandola
    erikscandola over 5 years
    @borislemke I tried your solution but it doesn't work for me. When I click on link (tag A with routerLink property) my app freeze. Any suggestions?
  • kuldeep
    kuldeep almost 5 years
    does it help with resolvers? My problem is that while resolvers bring back the data, I am unable to show spinner anywhere because the actual target component ngOninit has not yet been called !! My idea was to show the spinner in ngOnInit and hide it once resolved data is returned from route subscription