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

JavaScript: Creating Custom IDs for Element Generation

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"> ...

What is the best way to set up an anchor element to execute a JavaScript function when clicked on the left, but open a new page when clicked in

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 ...

Validation of passwords containing special characters in Angular2

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 ...

Can you explain how this particular web service uses JSON to display slide images?

{ "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 ...

What is the best way to ensure that the columns are the same height, regardless of the varying content they contain

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 ...

Issue with jQuery UI: Accordion not functioning properly

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 ...

verify access with mysql database

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 ...

The feature to swap out the thumb of a range slider with an image is currently not

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: ...

The scrollbar located within a table cell is malfunctioning and unresponsive

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 ...

Animate hovering over a specific element within a group of similar elements using jQuery

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 ...

Maximizing HTML5 Game Performance through requestAnimationFrame Refresh Rate

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 ...

Displaying a list within a circle that elegantly animates, and then, after two full rotations, magically morphs

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 ...

Is there an issue with JQM plugin due to jQuery's append() function?

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 ...

What could be causing the "Cannot POST /api/" error to occur when attempting to submit a form?

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 ...

What are the main differences between Fluid Layout and Grid?

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 ...

How do browsers typically prioritize loading files into the cache?

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! ...

Tips for resizing images to fit different screen sizes on your website

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 ...

Show JSON information in an angular-data-table

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 ...

angular-cli is throwing an error indicating that the current version is outdated

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& ...

Tips for organizing AJAX code to show images and videos in HTML with a switch case approach

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 ...