This guide walks through the process of creating a loading indicator widget for Angular applications. We will go over the design step by step, constructing both a global loading indicator component and its companion service. You will learn how to make the component react to route changes automatically and how to toggle the indicator for individual HTTP requests without extra effort. We will also cover using the indicator when fetching backend data with Observables or async-await. Beyond practical utility, building this widget is a valuable Angular exercise that touches on several framework features and common patterns. Let's dive in.
Table Of Contents
Here is what we will explore:
- Defining the loading indicator's core requirements
- Implementing the loading indicator service
- Implementing the loading indicator component
- Setting up the indicator for global use
- Triggering the indicator automatically during backend data loads
- Excluding specific HTTP requests from showing the indicator
- Wiring the indicator to the router
- Offering a custom indicator UI
- Wrap-up and key takeaways
Defining the loading indicator's core requirements
Before we start coding, let's outline the essential capabilities our loading indicator needs to offer:
- It should be possible to enable or disable the indicator from any location in the app. Direct access to the indicator component should not be necessary for this control.
- It should integrate smoothly with the router, displaying automatically during route transitions.
- It should be able to auto-activate whenever we are fetching data from a backend service.
- There should be a way to keep the indicator hidden for certain HTTP requests, for example, during silent background syncs that the user doesn't need to see.
- The indicator should have a sensible default design using Angular Material, but it should also allow for a fully custom appearance.
To achieve this, our implementation will consist of two main parts:
- A loading component, which renders the visual spinner element.
- A loading service, which acts as the central point of control.
With these goals established, we're ready to build.
Implementing the loading indicator service
We'll begin by creating the service that will be used to activate and deactivate the loading indicator. The primary purpose of this service is to decouple the control of the indicator from any specific component that displays it. This means any deeply nested component, even without a reference to the loading component itself, can control the global spinner.
To achieve this, instead of directly manipulating the component, we will control it indirectly through a shared, singleton service. This service can be injected anywhere, providing a clean API to toggle the loading state.
This service will be built using a reactive style with RxJs.
Here is the complete implementation 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 down this code closely:
- The service uses the Observable Data Service pattern.
- A
BehaviorSubjectis used internally to hold the current boolean state of the loading indicator. - The subject is marked as private to ensure that only the service's public methods can modify its value, keeping the state changes controlled.
- The subject is exposed as an
Observable(viaasObservable()) allowing any component to subscribe and react to changes in the loading state. - Two straightforward public methods,
loadingOnandloadingOff, are exposed as the only way to modify the indicator's state.
Using the service is remarkably simple. Since it's registered as a global singleton provider, you can inject and manage the loading indicator from any part of your application tree.
Implementing the loading indicator component
Next, we'll construct the visual component. Our default UI will use an Angular Material spinner, but as per our requirements, we'll also include the ability to override this default with a custom template.
Let's examine the full source code of this component, starting with its styles:
.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;
}
The spinner-container class defines the styling for the overlay background that covers the page while loading is in progress.
Now, let's look at the component template:
@if(loading$ | async) {
<div class="spinner-container">
@if(customLoadingIndicator) {
<ng-container
*ngTemplateOutlet="customLoadingIndicator" />
} @else {
<mat-spinner />
}
</div>
}
By the way, this template uses the @if syntax. If you need a refresher, you can consult this guide: Angular @if - The Complete Guide.
Let's analyze the template step by step:
- The indicator is only rendered when the
loading$observable emitstrue; otherwise, it is entirely hidden. - In its default state, the component uses the Angular Material
<mat-spinner />. - However, it also supports a custom UI. This is achieved by projecting a template named
customLoadingIndicatorinto the component.
We'll discuss the custom UI and ngTemplateOutlet in more detail later. For now, let's focus on the component's TypeScript class:
@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 the core logic resides! We'll go over the distinct parts of this component code in detail in the following sections.
Setting up the indicator for global use
With the service in place, we can control the indicator from anywhere. The first setup step is to add the component to the root of your application. Consider the following example for your 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 now in your layout, and it's inactive by default. Now, let's say we want to turn it on or off from a child component that has no direct reference to it.
How to use the loading indicator with async/await code
To control the indicator from any component, simply inject the LoadingService and use its public 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();
}
}
}
Look at the onLoadCourses method to see a standard pattern for loading data with a loading indicator.
We first call loadingOn(), then perform the backend operation, and finally call loadingOff(). Be careful to turn the indicator off inside a finally block. This guarantees the indicator is always switched off, even if the request fails with an error.
It would be a mistake to turn the loading off inside the try block just before the catch. If an error occurs before that line, the loading off call would never be reached, leaving the UI blocked by the spinner forever. However, this manual pattern can become tedious and error-prone when applied to every HTTP call across your entire codebase.
Triggering the indicator automatically during backend data loads
To avoid repetitive code, we can leverage an HttpInterceptor. This lets us automatically turn the indicator on at the start of every HTTP request and off when it finishes. Here’s how to set it up:
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 we dissect this, let's see how to register this interceptor. You'll need to provide it in your application's module or bootstrap configuration:
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(
BrowserModule,
AppRoutingModule,
RouterModule,
LoadingService
),
{
provide: HTTP_INTERCEPTORS,
useClass: LoadingInterceptor,
multi: true,
},
],
});
As shown, this must be a multi-provider. This setup tells Angular to treat the token as an array, allowing for multiple interceptors to be registered.
Now, let's examine the interceptor's logic itself. It does the following:
- First, it creates a
cloneof the outgoing request. This is a good practice to avoid side effects on the original request. - It calls
loadingOn()before passing the request along. - It uses the
finalizeoperator to callloadingOff(). Thefinalizeoperator will execute its callback regardless of whether the request completes, errors, or is cancelled.
Since an HTTP Observable from Angular's HttpClient will always eventually either emit a response and complete, or emit an error, we can be sure that finalize will always run, and our loading indicator will be hidden.
Excluding specific HTTP requests from showing the indicator
Notice that the interceptor also checks a value from the request context: an HttpContextToken called SkipLoading. If this context token is set to true, the interceptor bypasses turning on the loading indicator.
This is perfect for scenarios like periodic background polling. For instance, if you're refreshing chart data every 10 seconds, you typically don't want to flash a spinner each time. You can silence the indicator for those specific requests by setting the SkipLoading token.
Here is an example of how to use this for an individual HTTP request:
this.http.get("/api/courses", {
context: new HttpContext().set(SkipLoading, true),
});
Wiring the indicator to the router
Another common requirement is to show the loading indicator while navigating between routes. This is where the detectRouteTransitions input comes in. By setting this boolean to true, the loading indicator will automatically toggle on and off during router navigation events.
Here's how to activate this feature in the root component:
<loading-indicator
[detectRouteTransitions]="true" />
Offering a custom indicator UI
The loading component allows for a completely replaceable visual presentation. If you don't want the standard Angular Material spinner, you can provide your own design via content projection.
Here’s an example of how to pass a custom template:
<loading-indicator>
<ng-template #loading>
<div class="custom-spinner">
<img src="custom-spinner.gif" />
</div>
</ng-template>
</loading-indicator>
When this custom template is provided, it will be rendered in place of the default spinner. Crucially, this projected template must be given the reference name "loading" for the mechanism to work. Without this name binding, the component won't pick it up.
How does the component detect this custom projectable content? It uses the @ContentChild decorator to query for a template with that specific name:
@ContentChild("loading")
customLoadingIndicator: TemplateRef<any> | null = null
If this query returns a template, the indicator component renders that; if not, it falls back to the standard spinner. Here's the conditional logic that makes that decision:
@if(customLoadingIndicator) {
<ng-container *ngTemplateOutlet="customLoadingIndicator" />
} @else {
<mat-spinner />
}
The power behind this is the ngTemplateOutlet directive, which is used to dynamically render the selected template. For a complete guide on this directive, check out: Angular ng-template, ng-container, and ngTemplateOutlet - The Complete Guide To Angular Templates.
With that, we've covered all the primary features of this loading indicator widget. This toolset is robust enough for the needs of most production applications.
If you found this walkthrough helpful and want to stay updated on future posts like this, consider subscribing to our newsletter. You'll also receive timely updates on the broader Angular ecosystem.
For a more in-depth exploration of Angular's core features, like @Output, take a look at this in-depth course: Angular Core Deep Dive Course.
Wrap-up and key takeaways
In this guide, we went through the full process of building a loading indicator widget for Angular. Here's a recap of its notable features:
- Global control via an injectable service.
- Seamless integration with the router for automatic display during navigation.
- Automatic activation for all HTTP requests using an interceptor.
- Flexibility to hide the indicator for individual requests via an Http context flag.
- Support for a fully customized UI using content projection.
I hope this widget proves useful in your projects. Feel free to share your experiences or any improvement ideas you might have. If any part of this is unclear, or if you have questions, just ask. I'm here to help.
