How do I show a loading screen when I change a route in Angular 2?
-
stackoverflow.com/questions/21512893/…AndrewL64– AndrewL642016-05-06 10:08:01 +00:00Commented May 6, 2016 at 10:08
-
5That linked question above (AndrewL64's comment) is about AngularJS, not "Angular" (2+).Gary McGill– Gary McGill2019-06-06 13:42:01 +00:00Commented Jun 6, 2019 at 13:42
5 Answers
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>
24 Comments
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.display
either none
or inline
.'md-spinner' is not a known element:
. I am quite new to Angular. Could you please tell me what might be the mistake?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();
}
}
10 Comments
spinner-service
now you just need to other parts to make it work. And remember it's for angular2-rc-1
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.
2 Comments
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.
Comments
2024 addition to accepted answer
The accepted answer works like a charm, but needs a bit different typing to work in newer Angular versions:
private destroyRef = inject(DestroyRef);
constructor(private router: Router) {
this.router.events
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((e) => {
this.navigationInterceptor(e.type);
});
}
private navigationInterceptor(eventType: EventType): void {
if (eventType === EventType.NavigationStart) {
this.isNavigating = true;
}
if (eventType === EventType.NavigationEnd) {
this.isNavigating = false;
}
// Set loading state to false in both of the below events to hide the spinner in case a request fails
if (eventType === EventType.NavigationCancel) {
this.isNavigating = false;
}
if (eventType === EventType.NavigationError) {
this.isNavigating = false;
}
}
Note that i added a pipe with a destroy reference to the RxJs bit which should be a common thing to do when handling RxJs subscriptions. This particular syntax with the takeUntilDestroyed()
works from Angular 16 onwards.