Create a loading screen for Angular 7 apps

Posted by Harald Nezbeda on Mon 05 November 2018

The loading screen is a component that most applications require, especially when it comes to single page applications (SPA). It increases the user experience and sometimes it event gives the impression of a better performance.

How to implement it?

It is best to start with a generic markup for the loading screen which consists of a wrapping division, absolute positioned and covering the entire screen. Inside there will be the actual loading icon/animation placed in the middle.

<div style="position: absolute; background-color: rgba(255, 255, 255, 0.6); width: 100%; height: 100%; z-index: 100000">
    <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
      Loading...
    </div>
</div>

This approach can be used for any fronted application.

Initial Angular component

For Angular you would go ahead and add the code snippet above inside the root component. In an Angular CLI project this typically in the app.component.html file.

To make it clean, the code should be placed in a dedicated component where markup and styles are separated.

ng g c components/loading-screen

loading-screen.component.html

<div class="loading-screen-wrapper">
  <div class="loading-screen-icon">
    Loading...
  </div>
</div>

loading-screen.component.scss

.loading-screen-wrapper {
  z-index: 100000;
  position: absolute;
  background-color: rgba(255, 255, 255, 0.6);
  width: 100%;
  height: 100%;
  display: block;
}

.loading-screen-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

Now the new component must be added in the root component and remove the old markup.

Add some custom style

For the moment the screen is just a semitransparent white with the text "Loading...". To make this a bit more appealing you should replace the text with a logo and add some css animation or use a loader animation.

A good list of loaders can be found at freefrontend.com. Most of them are hosted public on codepen, which means they are under the MIT license: https://blog.codepen.io/legal/licensing/.

This is what you might end up with:

Angular Loading Screen

Component logic

It’s time to add some logic and decide when the loading screen should be displayed. First add an ngIf to the loading-screen-wrapper of loading-screen.component.html. The loading variable has to be declared in the loading-screen.component.ts.

loading-screen.component.html

<div class="loading-screen-wrapper" *ngIf="loading">
  ...
</div>

loading-screen.component.ts

...
export class LoadingScreenComponent implements OnInit, OnDestroy {
   loading: boolean = false;
...

Loading service

Moving on it is required to check if something is loading and mark it somewhere. This can be done in a global store (by using ngrx or NGXS) or in a service. For simplicity I will provide a service, as this solution can also be used parallel to a store.

Use the CLI to generate the service

ng g s services/loading-screen/loading-screen

This is the initial structure that the service will require.

loading-screen.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LoadingScreenService {

  private _loading: boolean = false;

  get loading():boolean {
    return this._loading;
  }

  set loading(value) {
    this._loading = value;
  }

  startLoading() {
    this.loading = true;
  }

  stopLoading() {
    this.loading = false;
  }
}

It will be provided as a singleton in the root application (this option is recommended and available since Angular 6) and consists of a property that holds the value for loading status and two methods to start and stop the loading.

Using it like this in a component will be of no use, as there is no way to check if the status has updated. This can be solved by using a Subject that will emit the value that is set in the service.

loading-screen.service.ts

import { Injectable } from '@angular/core';
import { Subject } from "rxjs";

@Injectable({
  providedIn: 'root'
})
export class LoadingScreenService {

  private _loading: boolean = false;
  loadingStatus: Subject = new Subject();

  get loading():boolean {
    return this._loading;
  }

  set loading(value) {
    this._loading = value;
    this.loadingStatus.next(value);
  }

  startLoading() {
    this.loading = true;
  }

  stopLoading() {
    this.loading = false;
  }
}

We can now bind the service to the component and use the provided subscribable.

loading-screen.component.ts

import { Component, OnDestroy, OnInit } from '@angular/core';
import { LoadingScreenService } from "../../services/loading-screen/loading-screen.service";
import { Subscription } from "rxjs";

@Component({
  selector: 'app-loading-screen',
  templateUrl: './loading-screen.component.html',
  styleUrls: ['./loading-screen.component.scss']
})
export class LoadingScreenComponent implements OnInit, OnDestroy {

  loading: boolean = false;
  loadingSubscription: Subscription;

  constructor(private loadingScreenService: LoadingScreenService) {
  }

  ngOnInit() {
    this.loadingSubscription = this.loadingScreenService.loadingStatus.subscribe((value) => {
      this.loading = value;
    });
  }

  ngOnDestroy() {
    this.loadingSubscription.unsubscribe();
  }

}

Make sure to add the subscription to a separate variable so it can be unsubscribed (as above) when the component is getting destroyed. Technically this won’t happened with the current setup, but it’s better to follow this best practice, just in case the component might be used on other places than the root component.

To test it you can go and call the .startLoading() from another component.

Intercept http calls

The final piece is to call this screen on a specific action. Typically this is done when calling an external API, but you can also call the .startLoading() and .stopLoading() during other tasks, it depends on your project.

The goal now is to display the loading screen whenever there are calls to the REST API that the application is using. This can be achieved with an interceptor.

loading.interceptor.ts

import { Injectable } from "@angular/core";
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs";
import { LoadingScreenService } from "../services/loading-screen/loading-screen.service";
import { finalize } from "rxjs/operators";


@Injectable()
export class LoadingScreenInterceptor implements HttpInterceptor {

  activeRequests: number = 0;

  constructor(private loadingScreenService: LoadingScreenService) {
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (this.activeRequests === 0) {
      this.loadingScreenService.startLoading();
    }

    this.activeRequests++;
    return next.handle(request).pipe(
      finalize(() => {
        this.activeRequests--;
        if (this.activeRequests === 0) {
          this.loadingScreenService.stopLoading();
        }
      })
    )
  };

}

Now the interceptor must be provided into the root module.

app.module.ts

@NgModule({
  declarations: [...],
  imports: [...],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LoadingScreenInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

This will now work on every interaction that causes an HTTP request. It will although cause some annoying behavior that must be corrected: • The screen will appear even fast API calls but just for a short time which will cause a flickering on the screen. • If your REST API has some special endpoints (like an Auth Refresh) you might not want the screen to load at all.

The first issue can be fixed by adding a threshold of about 200ms to call the load screen function. This requires to extend the subscription in the loading-screen component, by adding a debounceTime pipe.

loading-screen.component.ts


ngOnInit() {
    this.loadingSubscription = this.loadingScreenService.loadingStatus.pipe(
      debounceTime(200)
    ).subscribe((value) => {
      this.loading = value;
    });
  }

The second issue must be covered in the interceptor. It must include a list of URLs that have to be skipped:

loading-screen.interceptor.ts

import { Injectable } from "@angular/core";
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs";
import { LoadingScreenService } from "../services/loading-screen/loading-screen.service";
import { finalize } from "rxjs/operators";


@Injectable()
export class LoadingScreenInterceptor implements HttpInterceptor {

  activeRequests: number = 0;

  /**
   * URLs for which the loading screen should not be enabled
   */
  skippUrls = [
    '/authrefresh',
  ];

  constructor(private loadingScreenService: LoadingScreenService) {
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let displayLoadingScreen = true;

    for (const skippUrl of this.skippUrls) {
      if (new RegExp(skippUrl).test(request.url)) {
        displayLoadingScreen = false;
        break;
      }
    }

    if (displayLoadingScreen) {
      if (this.activeRequests === 0) {
        this.loadingScreenService.startLoading();
      }
      this.activeRequests++;

      return next.handle(request).pipe(
        finalize(() => {
          this.activeRequests--;
          if (this.activeRequests === 0) {
            this.loadingScreenService.stopLoading();
          }
        })
      )
    } else {
      return next.handle(request);
    }
  };
}

Here is the complete implantation into an Angular 7 project: https://github.com/nezhar/snypy-frontend/commit/3f4765f09aacc79669b117870cafbcc968e346a1

The approach can be used for Angular 6. For Angular 5 and earlier you have to use the old pattern for registering a Service and the old style syntax for RxJS.