import { Injectable, computed, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoadingService {
private readonly activeRequests = signal(0);
readonly loading = computed(() => this.activeRequests() > 0);
show(): void {
this.activeRequests.update((count) => count + 1);
}
hide(): void {
this.activeRequests.update((count) => Math.max(0, count - 1));
}
}
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs';
import { LoadingService } from './loading.service';
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loading = inject(LoadingService);
// Requests can opt out of the global spinner (e.g. background polling).
if (req.headers.has('X-Skip-Loading')) {
const cleaned = req.clone({ headers: req.headers.delete('X-Skip-Loading') });
return next(cleaned);
}
loading.show();
return next(req).pipe(finalize(() => loading.hide()));
};
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { loadingInterceptor } from './loading.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([loadingInterceptor])),
],
};
import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LoadingService } from './loading.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `
@if (loadingService.loading()) {
<div class="loading-overlay" role="status" aria-live="polite">
<div class="spinner"></div>
<span class="sr-only">Loading…</span>
</div>
}
<router-outlet></router-outlet>
`,
styleUrl: './app.component.css',
})
export class AppComponent {
protected readonly loadingService = inject(LoadingService);
}
This snippet wires up a global loading indicator that reflects whether any HTTP request is currently in flight, without every component having to manage its own isLoading flag. The pattern centralizes request counting in a service and lets an HttpInterceptor increment on request start and decrement on completion, so the UI reacts automatically.
In loading.service.ts, the count of active requests is held in a private signal, and a public computed loading signal exposes whether that count is greater than zero. Using a counter rather than a boolean is deliberate: multiple concurrent requests may overlap, so a naive boolean would be flipped off by the first response even while others are still pending. show() bumps the counter and hide() decrements it, guarded with Math.max(0, ...) so an unexpected double-completion can never drive the count negative and leave the spinner stuck or falsely hidden.
In loading.interceptor.ts, a functional interceptor (HttpInterceptorFn) calls loading.show() before delegating to next(req), then uses RxJS finalize to call loading.hide() exactly once when the stream terminates — whether it completes, errors, or is unsubscribed. finalize is the key operator here because it fires on every terminal path, which is what makes the counter reliably balanced. The interceptor also inspects a custom header via req.headers.has('X-Skip-Loading') so that background polling or silent requests can opt out of the global spinner, and it strips that header before forwarding the request so it never leaks to the server.
In app.config.ts, the interceptor is registered with provideHttpClient(withInterceptors([loadingInterceptor])), the modern standalone way to compose interceptors.
Finally, app.component.ts consumes loadingService.loading directly in the template with the @if control-flow block, rendering an overlay only while requests are active. Because everything flows through signals, change detection stays cheap and no manual subscription management is needed. The main trade-off is that this couples all HTTP traffic to one spinner; the opt-out header keeps that manageable, and per-feature indicators can still be layered on top when finer control is required.
Related snips
class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2)
margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)
Django model signals vs overriding save
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
import Combine
import Foundation
class SearchViewModel: ObservableObject {
@Published var searchQuery = ""
@Published var results: [SearchResult] = []
Combine operators for data transformation
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
Share this code
Here's the card — post it anywhere.