In this post you’ll learn how to create a loading spinner that will show and hide based on your application’s loading state. To achieve this, we’ll be using the Angular Router and hooking into some of the events provided.
A loading spinner is typically best displayed in the root component, which is usually our AppComponent
.
First off let’s start with an empty component, import and inject our Router
. This will then enable us to subscribe to the Router events:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
template: `
<div class="app">
<h1>My App</h1>
<router-outlet></router-outlet>
</div>
`,
})
export class AppComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit() {}
}
Inside our OnInit lifecycle hook we’ll setup a subscription to the events
property on the Router:
@Component({...})
export class AppComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe(console.log);
}
}
In our developers tool console we’ll then see something like (with various metadata inside each class object):
▶ NavigationStart {...}
▶ RouteConfigLoadStart {...}
▶ RouteConfigLoadEnd {...}
▶ RoutesRecognized {...}
▶ GuardsCheckStart {...}
▶ ActivationStart {...}
▶ GuardsCheckEnd {...}
▶ ResolveStart {...}
▶ ResolveEnd {...}
▶ ActivationEnd {...}
▶ NavigationEnd {...}
That’s a lot of events! And this is a good thing.
To find the events that are relevant to our goal of creating a loading spinner, we’re going to follow a simple process of filtering out the events we do need and then toggling a loading
state accordingly.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
First, let’s define a new loading$
property which contains the $
suffix to denote an Observable type. We’ll then create a new Observable using of()
:
import { Observable, of } from 'rxjs';
@Component({...})
export class AppComponent implements OnInit {
loading$: Observable<boolean> = of(false);
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe(console.log);
}
}
We can then subscribe in the template to our loading$
Observable using the Async Pipe:
<div *ngIf="loading$ | async">Loading...</div>
Our next step is to pipe()
and filter()
on the events we’re interested in:
import { Observable, of } from 'rxjs';
import { filter } from 'rxjs/operators';
@Component({...})
export class AppComponent implements OnInit {
loading$: Observable<boolean> = of(false);
constructor(private router: Router) {}
ngOnInit() {
this.router.events.pipe(
filter(
(e) =>
e instanceof NavigationStart ||
e instanceof NavigationEnd ||
e instanceof NavigationCancel ||
e instanceof NavigationError
)
)
// ONLY runs on:
// NavigationStart, NavigationEnd, NavigationCancel, NavigationError
.subscribe(console.log);
}
}
As Observables are lazy by design, any further logic we write will only run when these navigation events are found, instead of running on any type of router event.
So now the last step is simply to set loading$
to true
when we are inside a NavigationStart
and as soon as NavigationEnd
, NavigationCancel
or NavigationError
fire we’ll set it back to false
.
To do this, we can simply map()
a new value back and check if the NavigationStart
event is the one we’ve received. If it is, we’ll return true
and false
if not:
import { Observable, of } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Component({...})
export class AppComponent implements OnInit {
loading$: Observable<boolean> = of(false);
constructor(private router: Router) {}
ngOnInit() {
this.router.events.pipe(
filter(
(e) =>
e instanceof NavigationStart ||
e instanceof NavigationEnd ||
e instanceof NavigationCancel ||
e instanceof NavigationError
),
map((e) => e instanceof NavigationStart)
)
// true or false
.subscribe(console.log);
}
}
As we are using map()
we’ll return a new value to our .subscribe()
each time it fires. To finish things off we’ll need to point our Observable to the this.loading$
and remove the manual subscription:
import { Observable, of } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Component({...})
export class AppComponent implements OnInit {
loading$: Observable<boolean> = of(false);
constructor(private router: Router) {}
ngOnInit() {
this.loading$ = this.router.events.pipe(
filter(
(e) =>
e instanceof NavigationStart ||
e instanceof NavigationEnd ||
e instanceof NavigationCancel ||
e instanceof NavigationError
),
map((e) => e instanceof NavigationStart)
);
}
}
Nice and clean! These events will fire when navigating between your application components. For best results use a Route Guard to load the data for a particular page, as this will force the NavigationEnd
event to only fire when the data has resolved.
If you are serious about your Angular skills, your next step is to take a look at my Angular courses where you’ll learn Angular, TypeScript, RxJS and state management principles from beginning to expert level.
Happy spinning!