Show loading screen when navigating between routes in Angular 2

How to show loading screen when changing route in Angular 2?

+72
html css angular angular2-routing
May 6 '16 at 10:04
source share
3 answers

The current Angular router provides navigation events. You can subscribe to them and accordingly make changes to the user interface. Remember to count in other events, such as NavigationCancel and NavigationError , to stop your counter in case of failed router transitions.

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) { router.events.subscribe((event: RouterEvent) => { this.navigationInterceptor(event) }) } // 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 loading bar or circle --> <md-progress-bar mode="indeterminate"></md-progress-bar> </div> 

Improved performance response . If you care about performance, there is a better way, but a little more tedious to implement, but improving performance will be worth the extra work. Instead of using *ngIf to conditionally display the counter, we could use Angular NgZone and Renderer to turn on / off the counter, which will bypass the detection of Angular changes when we change the state of the counter. I found this to make the animation smoother compared to using *ngIf or async .

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((event: RouterEvent) => { this._navigationInterceptor(event) }) } // 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 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 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> 
+124
Jul 27 '16 at 18:40
source share

UPDATE: 3 Now that I have upgraded to the new router, the @borislemke approach will not work if you use the CanDeactivate guard. I am humbled by my old method, ie: this answer

UPDATE2 : the router events in the new router look promising, and the @borislemke answer below (now above) seems to cover the main aspect of spinner implementation, I haven’t tested it, but I recommend it.

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




If you know your way in Angular2, this is what you 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 (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 (download this service)

Define the observable that will be signed by the spinner component to change the status when changing, and the function to know and set the counter 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 route 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(); } } 
+29
May 6 '16 at 10:38
source share

Why not just use plain 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 just just

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

The trick is that new routes and components will always be displayed after the router exits , so using a simple css selector we can display and hide the download.

+5
Dec 27 '16 at
source share



All Articles