This post is a complete guide on how to create a loading indicator widget in Angular.

We will explain step by step the design of this widget. We will create a global loading indicator component and the corresponding loading indicator service to go with it.

We will learn how to make the loading indicator component detect route transitions and display the indicator while the route is changing.

We will also learn how we can easily turn on or off the loading indicator for every HTTP request.

Also, we will see how to use the loading when loading data from a backend using Observables and async-await.

Besides being useful on its own, building this widget makes up for a nice Angular exercise, were we will explore several features and patterns of the framework.

So without further ado, let's get started! 🚀

Table Of Contents

In this post, we will cover the following topics:

  • Design goals for the loading indicator
  • Building the loading indicator service
  • Building the loading indicator component
  • Using the loading indicator globally
  • Automatically showing the loading indicator when loading data from the backend
  • Don't turn on the loading indicator for certain HTTP requests
  • Integrating the loading indicator with the router
  • Proving an alternative UI for the loading indicator
  • Summary

Design goals for the loading indicator

First, let's define some core features that we want to have in our loading indicator:

  • we should be able to turn it on and off from anywhere in the application. We shouldn't need to have direct access to the indicator component to do that.
  • we should be able to integrate the loading indicator with the router so that it displays on route transitions.
  • we should be able to automatically show the loading indicator when loading data from the backend.
  • For certain HTTP requests, we should be able to not turn on the loading indicator, for example if we want to silently trigger the request in the background without the user noticing it
  • the loading indicator should have a default UI design based on Angular Material, but we should be able to customize it if we want to

The loading indicator should have:

  • a loading component, that's the spinner element itself
  • a loading service, that can be usee to control the indicator from anywhere on the application

With these design goals in mind, let's start building our loading indicator widget!

Building the loading indicator service

Let's start building the service that we are going to use to activate/deactivate the loading indicator.

The main goal of this service is to allow control of the behavior of the loading indicator even from a component that has no direct access to the loading component.

Imagine for example a global loading indicator, that's displayed right in the center of the screen.

We want to be able to turn on and off this loading indicator from anywhere in the application.

In order to achieve this, instead of using the loading component directly to turn on/off the indicator, we are going to do that indirectly via a shared service.

We will be able to inject this service anywhere that we want, and control the loading indicator via the service.

We are going to write this service in a reactive style using RxJs.

Here is the full code of the service:

@Injectable({
  providedIn: "root",
})
export class LoadingService {
  private loadingSubject = 
    new BehaviorSubject<boolean>(false);

  loading$ = this.loadingSubject.asObservable();

  loadingOn() {
    this.loadingSubject.next(true);
  }

  loadingOff() {
    this.loadingSubject.next(false);
  }
}

Let's break this code down, step by step:

  • we are using the Observable Data Service pattern in this service.
  • we are using a BehaviorSubject to store the current state of the loading indicator.
  • we are keeping the subject private, to keep control over who can emit values using it.
  • we are exposing the subject as an observable so that any component can subscribe to it and get notified when the loading indicator is turned on or off.
  • we are exposing two simple public methods to turn the loading indicator on or off.

As you can see, this service is super easy to use!

This service is declared globally as a singleton, so you can inject it anywhere on the application and use it to control the global loading indicator.

You can do so even in components deeply nested in the component tree, that don't have access to the global loading indicator.

Building the loading indicator component

Now let's build the component itself.

For the default UI of the indicator, we are going to use an Angular Material spinner component, but remember that we are going to be able to replace this with something else if we want to.

Let's show here the full code of the component, and then we are going to break it down step by step.

Let's start with the styling of the component:

.spinner-container {
  position: fixed;
  height: 100%;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  top: 0;
  left: 0;
  background: rgba(0, 0, 0, 0.32);
  z-index: 2000;
}

This spinner-container CSS class is going to be used to style the background that will cover the page while the loading is ongoing.

Let's now have a look at the template of the loading component:

@if(loading$ | async) {

<div class="spinner-container">
  @if(customLoadingIndicator) {

  <ng-container 
    *ngTemplateOutlet="customLoadingIndicator" />

  } @else {

  <mat-spinner />

  }
</div>

}

Notice that we are using the @if template syntax. To read more about it, you can check out this guide: Angular @if - The Complete Guide.

Let's break down the template code step by step:

  • the loading indicator is only going to be displayed if the loading$ observable emits the value true, and hidden otherwise
  • the default UI for the loading indicator is the <mat-spinner /> component from Angular Material
  • But there is also the possibility of providing a custom UI for the component, by providing a template called customLoadingIndicator to the component via content projection

We will dive deeper into the custom UI feature and the use of ngTemplateOutlet later on.

Right now, let's have a look at the component class of the loading indicator:

@Component({
  selector: "loading-indicator",
  templateUrl: "./loading-indicator.component.html",
  styleUrls: ["./loading-indicator.component.scss"],
  imports: [MatProgressSpinnerModule, AsyncPipe, NgIf, NgTemplateOutlet],
  standalone: true,
})
export class LoadingIndicatorComponent implements OnInit {

  loading$: Observable<boolean>;

  @Input()
  detectRouteTransitions = false;

  @ContentChild("loading")
  customLoadingIndicator: TemplateRef<any> | null = null;

  constructor(
  private loadingService: LoadingService, 
  private router: Router) {
    this.loading$ = this.loadingService.loading$;
  }

  ngOnInit() {
    if (this.detectRouteTransitions) {
      this.router.events
        .pipe(
          tap((event) => {
            if (event instanceof RouteConfigLoadStart) {
              this.loadingService.loadingOn();
            } else if (event instanceof RouteConfigLoadEnd) {
              this.loadingService.loadingOff();
            }
          })
        )
        .subscribe();
    }
  }
}

This is where it all comes together!

Let's discuss in the following sections the code of this component, and learn how each of these feature works, one at a time.

Using the loading indicator globally

Thanks to the loading service, we can use the loading indicator from anywhere in the application.

The first this that we need to do, is to set up the loading indicator component in the root component of the application.

Imagine that this is your root component app.component.html:

<ul>
  <li><a routerLink="/contact">Contact</a></li>
  <li><a routerLink="/help">Help</a></li>
  <li><a routerLink="/about">About</a></li>
</ul>

<router-outlet />

<loading-indicator />

The loading widget is in place, and it's turned off by default.

Now imagine that we would like to turn the loading indicator on and off from a child component, that doesn't have direct access to the loading component.

How to use the loading indicator with async/await code

All we have to do is to inject the loading service in the component, and call the loadingOn and loadingOff methods:

@Component({
  selector: "child-component",
  standalone: true,
  imports: [CommonModule],
  template: ` 
  <button (click)="onLoadCourses()">
  Load Courses
  </button> `,
})
export class ChildComponentComponent {
  constructor(private loadingService: LoadingService) {}

  onLoadCourses() {
    try {
      this.loadingService.loadingOn();

      // load courses from backend
    } catch (error) {
      // handle error message
    } finally {
      this.loadingService.loadingOff();
    }
  }
}

We can see on the onLoadCourses method a common pattern of how to handle loading indicators while doing backend requests.

We start by turning the loading indicator on, then we do the backend request, and turn we turn the loading indicator off.

Notice that we turn the loading indicator off in the finally block so that it's always turned off, even if there is an error.

It would be a mistake to try to turn the loading indicator inside the try block, right before the catch block.

This is because if there is an error before that, the loading will not be turned off.

Instead, it will stay on forever and block the whole UI.

But notice that writing this type of code everywhere could be repetitive and error-prone, especially if you plan on applying the same logic to almost every single HTTP request in your application.

Automatically showing the loading indicator when loading data from the backend

To avoid writing this repetitive code everywhere, we can use an Http Interceptor to automatically turn the loading indicator on and off for every HTTP request.

Here is how we can do that:

export const SkipLoading = 
  new HttpContextToken<boolean>(() => false);

@Injectable()
export class LoadingInterceptor 
    implements HttpInterceptor {
  constructor(private loadingService: LoadingService) {
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    // Check for a custom attribute 
    // to avoid showing loading spinner
    if (req.context.get(SkipLoading)) {
      // Pass the request directly to the next handler
      return next.handle(req);
    }

    // Turn on the loading spinner
    this.loadingService.loadingOn();

    return next.handle(req).pipe(
      finalize(() => {
        // Turn off the loading spinner
        this.loadingService.loadingOff();
      })
    );
  }
}

Before breaking this code down, let's see how we can register this interceptor in the application:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(
      BrowserModule,
      AppRoutingModule,
      RouterModule,
      LoadingService
    ),
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LoadingInterceptor,
      multi: true,
    },
  ],
});

As you can see, we need to use a multi-provider, meaning that this injectable token will return an array of interceptors,
as opposed to only one.

And now, let's break down the interceptor code.

As you can see, the interceptor is simply creating a clone of the original request.

Then it turns on the loading indicator, and it turns it off using the finalize operator, meaning that the indicator is only turned off when the request Observable is either completed or it errors out.

Because we know that an HTTP Observable will always emit a value and then complete (or error out), we know that the finalize operator will always be called and the loading indicator will be hidden.

Don't turn on the loading indicator for certain HTTP requests

But notice that in the code of the interceptor we are also checking for a HttpContextToken called SkipLoading.

If the value of this context is true, we are not turning on the loading indicator.

This could be useful in certain scenarios.

Imagine a scenario where we are periodically polling the server in the background every 10 seconds, to refresh the data of a chart.

In that case, we don't want to show the loading indicator every time, so we can skip it by setting the SkipLoading Http context to true.

Here is how we can disable the loading indicator for a single HTTP request:

this.http.get("/api/courses", {
  context: new HttpContext().set(SkipLoading, true),
});

Integrating the loading indicator with the router

Another common feature that we usually want to put in place, is to show the loading indicator during route transitions.

That is where the detectRouteTransitions input property comes into play.

If we set this property to true, the loading indicator will automatically turn on and off when the route is changing.

Here is how we can use this feature in our application root component:

<loading-indicator 
    [detectRouteTransitions]="true" />

Proving an alternative UI for the loading indicator

The loading component also supports the possibility of providing an alternative UI for the loading indicator.

If you don't want to use the Angular Material spinner, you can instead provide a custom template to the component via content projection, like so:

<loading-indicator>
  <ng-template #loading>
    <div class="custom-spinner">
      <img src="custom-spinner.gif" />
    </div>
  </ng-template>
</loading-indicator>

In this case, the custom template will be displayed instead of the Angular Material spinner.

Notice that for this to work, the custom template needs to be named "loading", otherwise this will not work.

So how does the loading indicator component know that it should display the custom template instead of the default UI?

The loading indicator component first checks if the user provided a custom template via the @ContentChild decorator:

@ContentChild("loading")
customLoadingIndicator: TemplateRef<any> | null = null

If the user provided a custom template with the name "loading", then that template is displayed, otherwise, the default UI is displayed:

@if(customLoadingIndicator) {

<ng-container *ngTemplateOutlet="customLoadingIndicator" />

} @else {

<mat-spinner />

}

The way that the custom template is applied to the loading component is via the ngTemplateOutlet directive.

You can read more about that directive here in this guide: Angular ng-template, ng-container, and ngTemplateOutlet - The Complete Guide To Angular Templates.

And with this, we have covered all the features of our global loading indicator.

I think that this widget has all the features that you need for most applications.

I hope that you enjoyed this post, to get notified when new similar posts like this are out, I invite you to subscribe to our newsletter:

You will also get up-to-date news on the Angular ecosystem.

And if you want a deep dive into all the features of Angular Core like @Output and others, have a look at the Angular Core Deep Dive Course:

Summary

In this post, we have seen how to build a loading indicator widget using Angular.

The loading indicator has the following features:

  • we can turn it on and off from anywhere in the application, thanks to the loading service
  • we can automatically turn it on and off for every HTTP request, thanks to an Http Interceptor
  • we can automatically turn it on and off during route transitions
  • we can provide a custom UI for the loading indicator if we need to, via content projection

I hope you will enjoy using this widget, let me know if you have applied it in one of your projects and if you have further improvement suggestions.

If you have any questions or comments, let me know as well. I'll be happy to help!