Display a loading screen while transitioning between routes in Angular 2

Is there a way to implement a loading screen for route changes in Angular 2?

Answer №1

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>

Answer №2

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();
  }
}

Answer №3

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.

Answer №4

For custom logic specific to the initial route, you can implement the following approach:

MainComponent

    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.

Answer №5

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.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Python code for clicking a button using Selenium

I'm having trouble closing popup windows in Selenium with Python. The button labeled "joyride-close-tip" is functioning correctly and closes the window, but the other two buttons are not working. What could be causing this issue? Even though I copied ...

What is the best way to automatically hide the Materialize CSS mobile navbar?

Recently, I completed a website called Link. Using only Materialize CSS, Vanilla JS, and plain CSS, I developed a single-page application that effectively hides and reveals different sections based on event listeners. Everything functions smoothly except ...

Is there a way to deactivate a tab when it's active and reactivate it upon clicking another tab in Angular?

<a class="nav-link" routerLink="/books" routerLinkActive="active (click)="bookTabIsClicked()" > Books </a> I am currently teaching myself Angular. I need help with disabling this tab when it is active ...

Angular 11 reactive form not reflecting real-time changes in input field

I'm currently working on an Angular project and I need to dynamically update a reactive form field with data retrieved from an API called getNextCode(). I have a function that calls the API service like this: ngOnInit(): void { this.NextCodeService.g ...

Angular allows for dynamic sourcing of iframes

I have encountered an issue while trying to integrate a payment system with Angular. The payment gateway API I am using provides the 3D Secure Page as html within a JSON response. My approach is to display this html content within an iframe, however, the h ...

Displaying the current time and total time of a custom video player using Javascript

Currently, I'm in the process of creating an html5 video player and have incorporated javascript to update the current time as a fraction of the total time. The script I've written so far is as follows: function updateTime() { var curTime = ...

Running a JavaScript function within a designated div element

I'm currently facing an issue with executing a javascript function - onload only in a specific div. I seem to be stuck on this problem, so if anyone could offer some assistance, I would greatly appreciate it. Thank you very much in advance! functio ...

Lazy-loaded modules in Angular that contain services provided within the module

Currently, I am facing a challenge with lazy-loaded modules and services that are provided in these modules. My folder structure looks like this: app -> featureModule1 (lazy loaded) -> featureModule2 (lazy loaded) -->services --->servi ...

using Angular and RxJS to filter out an element from an array

One of the functions in my service is a delete function. This function calls an API that returns either true or false. If the response is true, I then proceed to find the index of the item in my array, splice it out, and return the updated array. Here&apos ...

Which takes precedence: the end of the script tag or the backtick?

Currently, I am working on developing a page builder widget. My goal is to save the entirety of the HTML code for the edited page to both local storage and a database. The PHP script will load the saved HTML from the database, while JavaScript will handle ...

Designing a versatile Angular component for inputting data (Mailing Address)

Currently, I am in the process of developing an Angular 11 application that requires input for three distinct mailing addresses. Initially, I thought I had a clear understanding of what needed to be done, only to encounter warnings about elements with non- ...

Organizing AngularJS Data by Initial Letter with the <select></select> HTML Element

I'm trying to figure out how to filter an ng-repeat function based on the first letter of each option. For example, I want to filter a set of array or string so that only options starting with "A" or "B" are displayed. Can anyone help me with this? H ...

JQuery is failing to locate elements that have dynamic data titles assigned to them

I've been attempting to locate an element using a jQuery regex comparison in the data title. My situation involves having divs with the class .textbox, each containing a dynamically generated data title. To target specific boxes with that particular d ...

What is the best way to maintain the current position in a component while interacting with another component?

I have a component that displays a collection of cards with images. There is a button that toggles between showing another component and returning to the original list of cards. The issue I am encountering is that every time I return to the list of cards, ...

problem with adjusting the form field depending on the circumstances

I am facing an issue with a form that displays a dropdown containing values such as users, receipts, companies, and reviewer. The visibility of these values should be based on the user's role. For example, if the role is admin, only users, receipts, a ...

What is the appropriate method for passing parameters in PHP and how can you handle returned empty values

I'm looking to pass parameters in the action method because I am unable to do so via the header. <form name="mailinglist1" method="post" action="report1.php" > In this form, I'm using a download button to connect my report (HTML). ...

Angular (TypeScript) time format in the AM and PM style

Need help formatting time in 12-hour AM PM format for a subscription form. The Date and Time are crucial for scheduling purposes. How can I achieve the desired 12-hour AM PM time display? private weekday = ['Sunday', 'Monday', &apos ...

Problem with fixed element on mobile due to viewport width

I am experiencing an issue with the fixed element's width. Here is a code snippet for reference: https://codepen.io/highfield/pen/PKpXGG <body> <nav class="navbar"> <div style="position:absolute; left:20px; top:12px"> & ...

Converting SVG circle animation into textual form - a step-by-step guide!

<svg width="1000" height="100"> <circle id="orange-circle" r="30" cx="50" cy="50" fill="orange" /> <animate xlink:href="#orange-circle" attributeName="cx" from="50" to="900" dur="2s" begin="0s" ...

Position the div in the center, but for smaller sizes, switch to aligning

My current layout setup is as follows: Left side bar with a width of 200px and positioned at left: 0; Center section with a width of 700px and positioned at left: 250px; Right side bar with a width of 200px and positioned at right: 10px; While this arra ...