My resolution to this problem involved the development of a versatile LoadingStatusManager
utility, which appears as follows:
import { computed, Signal, signal, WritableSignal } from '@angular/core';
import { catchError, Observable, tap } from 'rxjs';
export class LoadingStatusManager<KeyType extends string> {
private readonly statusMap = {} as { [key in KeyType]: WritableSignal<boolean> };
constructor(private readonly keys: KeyType[]) {
for (const key of keys) {
this.statusMap[key] = signal(false);
}
}
executeAndUpdateStatus<T>(key: KeyType, observable: Observable<T>): Observable<T> {
this.statusMap[key].set(true);
return observable.pipe(
tap(() => this.statusMap[key].set(false)),
catchError((error) => {
this.statusMap[key].set(false);
throw error;
}),
);
}
getSignalWatchingOnProps(keys?: KeyType[]): Signal<boolean> {
const keySet = keys ?? this.keys;
return computed(() => keySet.some((key) => this.statusMap[key]()));
}
}
To implement it, start by defining a type that lists one key (string) for each operation you want to monitor:
type AnimalLoadingStatusKeyType = 'fetchCats' | 'fetchDogs' | 'fetchChickens';
Then instantiate a new LoadingStatusManager
object using this type:
private readonly loadingStatusManager = new LoadingStatusManager<AnimalLoadingStatusKeyType>([
'fetchCats',
'fetchDogs',
'fetchChickens',
]);
Now you can employ the loadingStatusManager
object to monitor your asynchronous tasks:
fetchCats() {
return this.loadingStatusManager.executeAndUpdateStatus(
'fetchCats',
this.httpClient.get('animals.org/cats'),
);
}
fetchDogs() {
return this.loadingStatusManager.executeAndUpdateStatus(
'fetchDogs',
this.httpClient.get('animals.org/dogs'),
);
}
fetchChickens() {
return this.loadingStatusManager.executeAndUpdateStatus(
'fetchChickens',
this.httpClient.get('animals.org/chickens'),
);
}
You can easily obtain a Signal
that monitors if any of the async operations are ongoing:
areAnimalsBeingLoaded: Signal<boolean> = this.loadingStatusManager.getSignalWatchingOnProps();
By not specifying any key to getSignalWatchingOnProps
, it automatically watches all keys.
If you desire a Signal dedicated to specific keys, use:
areMammalsBeingLoaded: Signal<boolean> = this.loadingStatusManager.getSignalWatchingOnProps([
'fetchCats',
'fetchDogs',
]);
The entirety of the code for AnimalService
is provided below:
import { HttpClient } from '@angular/common/http';
import { inject, Injectable, Signal } from '@angular/core';
import { LoadingStatusManager } from './loading-status-manager';
type AnimalLoadingStatusKeyType = 'fetchCats' | 'fetchDogs' | 'fetchChickens';
@Injectable({
providedIn: 'root',
})
export class AnimalService {
private readonly httpClient = inject(HttpClient);
private readonly loadingStatusManager = new LoadingStatusManager<AnimalLoadingStatusKeyType>([
'fetchCats',
'fetchDogs',
'fetchChickens',
]);
readonly areAnimalsBeingLoaded: Signal<boolean> = this.loadingStatusManager.getSignalWatchingOnProps();
readonly areMammalsBeingLoaded: Signal<boolean> = this.loadingStatusManager.getSignalWatchingOnProps([
'fetchCats',
'fetchDogs',
]);
fetchCats() {
return this.loadingStatusManager.executeAndUpdateStatus(
'fetchCats',
this.httpClient.get('animals.org/cats'),
);
}
fetchDogs() {
return this.loadingStatusManager.executeAndUpdateStatus(
'fetchDogs',
this.httpClient.get('animals.org/dogs'),
);
}
fetchChickens() {
return this.loadingStatusManager.executeAndUpdateStatus(
'fetchChickens',
this.httpClient.get('animals.org/chickens'),
);
}
}
Any Component or Service can inject the AnimalService
and access the signals from outside:
readonly animalService: inject(AnimalService);
// ...
this.animalService.areAnimalsBeingLoaded();
this.animalService.areMammalsBeingLoaded();
This functionality can also be utilized in a Component's template employing the OnPush
change detection strategy. With the assistance of Angular Signals, the template will refresh automatically whenever there are changes in the signal values.