Is there a way to implement a loading screen for route changes in Angular 2?
Is there a way to implement a loading screen for route changes in Angular 2?
Utilizing the latest features of the Angular Router, you now have access to Navigation Events that can be subscribed to for making UI adjustments as needed. It's important to consider additional Events like NavigationCancel
and NavigationError
in order to handle scenarios where router transitions fail.
app.component.ts - the main component of your application
...
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 {
// Initialize loading state as true to display a spinner on first load
loading = true
constructor(private router: Router) {
this.router.events.subscribe((e : RouterEvent) => {
this.navigationInterceptor(e);
})
}
// Manage visibility of loading spinner based on RouterEvents changes
navigationInterceptor(event: RouterEvent): void {
if (event instanceof NavigationStart) {
this.loading = true
}
if (event instanceof NavigationEnd) {
this.loading = false
}
// Hide the spinner if requests fail in either of the following events
if (event instanceof NavigationCancel) {
this.loading = false
}
if (event instanceof NavigationError) {
this.loading = false
}
}
}
app.component.html - the root view of your application
<div class="loading-overlay" *ngIf="loading">
<!-- Insert your preferred styling for the loading animation here -->
<md-progress-bar mode="indeterminate"></md-progress-bar>
</div>
Enhanced Performance Tip: For improved performance, consider implementing an advanced method using Angular's NgZone
and Renderer
instead of relying on *ngIf
for conditional rendering. This may require more effort but can lead to smoother animations by bypassing Angular's change detection system.
The script below outlines the modified approach:
app.component.ts - the main component of your application
...
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 {
// Rather than toggling a boolean value, store a reference to the spinner element
@ViewChild('spinnerElement')
spinnerElement: ElementRef
constructor(private router: Router,
private ngZone: NgZone,
private renderer: Renderer) {
router.events.subscribe(this._navigationInterceptor)
}
// Manage visibility of loading spinner based on RouterEvents changes
private _navigationInterceptor(event: RouterEvent): void {
if (event instanceof NavigationStart) {
this.ngZone.runOutsideAngular(() => {
this.renderer.setElementStyle(
this.spinnerElement.nativeElement,
'opacity',
'1'
)
})
}
if (event instanceof NavigationEnd) {
this._hideSpinner()
}
if (event instanceof NavigationCancel) {
this._hideSpinner()
}
if (event instanceof NavigationError) {
this._hideSpinner()
}
}
private _hideSpinner(): void {
this.ngZone.runOutsideAngular(() => {
this.renderer.setElementStyle(
this.spinnerElement.nativeElement,
'opacity',
'0'
)
})
}
}
app.component.html - the root view of your application
<div class="loading-overlay" #spinnerElement style="opacity: 0;">
<!-- Implement your custom loading animation here -->
<md-spinner></md-spinner>
</div>
UPDATE:3 After switching to a new router, I discovered that @borislemke's approach may not work with the CanDeactivate
guard. I've decided to revert back to my old method mentioned in this answer.
UPDATE2: The Router events in the new-router version seem promising and the solution provided by @borislemke appears to address the main aspects of spinner implementation. Although I haven't tested it yet, I highly recommend considering it.
UPDATE1: This answer was written during the time of the Old-Router
, when there was only one event route-changed
triggered through router.subscribe()
. Initially, I attempted to simplify the process by solely relying on router.subscribe()
but it resulted in issues as there was no way to detect a canceled navigation
. Consequently, I had to resort back to the longer approach (double work).
If you are familiar with Angular2, here is what you'll need to do:
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 (initialize this service)
Create an observable to be subscribed by spinner-component for changing the status on updates, and functions to determine and set the spinner as 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
(example):
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 perform this action on another event e.g., upon completion of data loading via xmlhttp request for the component
}
ngOnDestroy(){
this.spinner.start();
}
}
Have you considered using a straightforward CSS approach?
<router-outlet></router-outlet>
<div class="loading"></div>
In your stylesheet, you could have:
div.loading{
height: 100px;
background-color: red;
display: none;
}
router-outlet + div.loading{
display: block;
}
Alternatively, for a different solution:
<router-outlet></router-outlet>
<spinner-component></spinner-component>
Then you can add the following styles:
spinner-component{
display:none;
}
router-outlet + spinner-component{
display: block;
}
The key technique here is to ensure that new routes and components follow router-outlet, allowing us to easily toggle the loading indicator with CSS.
For custom logic specific to the initial route, you can implement the following approach:
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 encountered a situation where I needed to hide my page footer that was showing at the top of the page. This method can also be used if you want to display a loader only for the initial page load.
Additional Note for 2024
The solution provided as the accepted answer is effective, but requires some slight adjustments to function properly in newer versions of Angular:
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;
}
// To handle cases where requests fail, set loading state to false in NavigationCancel and NavigationError events
if (eventType === EventType.NavigationCancel) {
this.isNavigating = false;
}
if (eventType === EventType.NavigationError) {
this.isNavigating = false;
}
}
It's important to note that I have added a pipe with a destroy reference to the RxJs segment, which is a common practice when managing RxJs subscriptions. This specific syntax utilizing takeUntilDestroyed()
is compatible with Angular 16 and later versions.
I've been developing a jeopardy-style web application and I have a feature where users can create multiple teams with custom names. HTML <!--Score Boards--> <div id="teamBoards"> <div id="teams"> ...
One feature I've come across on certain websites, like the Jira site, is quite interesting. For instance, if we take a look at the timeline page with the following URL - When you click on the name of an issue (which is an anchor element), it triggers ...
I am working on setting up password validation requirements to ensure the field contains: Both uppercase letters, lowercase letters, numbers and special characters This is my current progress: passwordValueValidator(control) { if (control.value != u ...
{ "gallery_images": [ {"img":"http://www.jcwholesale.co.uk/slider_img/big/1_1433518577.jpg"}, {"img":"http://www.jcwholesale.co.uk/slider_img/big/1_1433519494.jpg"}, {"img":"http://www.jcwholesale.co.uk/slider_img ...
Visit my CodePen For this particular layout, I need to use only CSS. The columns are structured like so: <div class="col-md-12 no-border no-padding"> <h2 class="heading upper align-center">Headline</h2> <p class="subheading lower ...
I have created a nested sortable accordion, but there seems to be an issue. Within the 'accordion2' id, the heights of each item are too small and a vertical scroll bar appears. The content of the item labeled '1' is being cut off, show ...
Need urgent assistance, sorry if this is a repeated query due to my username! I am facing an issue with my login page. I want it to redirect to my home page when authentication fails by cross-checking input with a MySQL database which should output succes ...
I'm attempting to design a unique range slider using CSS. <style> .slide-container { width: 300px; } .slider { -webkit-appearance: none; width: 100%; height: 5px; border-radius: 5px; background: #FFFFFF; outline: none; opacity: ...
In a container with a fixed width of 500px, I have a table that contains a cell with potentially long text. To ensure the long text appears on a single line, I set the white-space: nowrap property. However, this causes the table to adjust its size to accom ...
Hi there! I recently started exploring Js and jQuery, and I was able to create a cool width change animation on a div when hovering over another. However, I ran into an issue when trying to implement this with multiple sets of similar divs. Whenever I hove ...
I am currently working on a HTML5 Canvas and JavaScript game. Initially, the frames per second (fps) are decent, but as the game progresses, the fps starts decreasing. It usually starts at around 45 fps and drops to only 5 fps. Here is my current game loo ...
Can icons be displayed in a circle and after 1,2 rotations, revert back to the list view? https://i.sstatic.net/p4Jxm.png If so, when it forms after 1,2 rotation, it should resemble a line. https://i.sstatic.net/uJW24.png In summary, looking for a CSS3 ...
I am just starting to learn about jQuery mobile as I develop an app using it. Whenever I click on the Add_details button, I want to add dynamic input fields only once. After saving, if I click on the Add_details button again, I need to append more dynamic ...
Having issues with my basic website and struggling to find a solution. As a complete beginner in this field, I am stuck and need some guidance. Accessing http://localhost:3000/class/create works perfectly fine when running the server. However, trying to a ...
It seems like there's some vital information I haven't come across yet. Are fluid layouts and grid layouts interchangeable? I keep hearing about this baseline concept, but I can't quite grasp its purpose. Does it serve as a guide for alignin ...
Out of curiosity, I wonder if the categorization is determined by file names or byte code? It's possible that it varies across different browsers. Thank you! ...
I have a website with an image that is currently responsive thanks to bootstrap. The width adjusts properly on different devices, but I want the image to crop to the left and right when it's enlarged in this specific case. I've tried various tec ...
I am trying to showcase the following JSON dataset within an angular-data-table {"_links":{"self":[{"href":"http://uni/api/v1/cycle1"},{"href":"http://uni/api/v1/cycle2"},{"href":"http://uni/api/v1/cycle3"}]}} This is what I have written so far in my cod ...
While working on one of my Angular 2 projects, I encountered an error when trying to run and test the app using the <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c7a6a9a0b2aba6b5eaa4abae87f6e9f7e9f7eaa5a2b3a6e9f6f2">[email& ...
I have 2 tables, one for images and one for videos. Within my HTML code, I have three buttons: slide_image, video, and single_image. Using iframes, I am able to load images and videos on the same page. When I click on the slide_image button, it displays im ...