Installation and Setup
Minimal setup for ngx-request-lock: package installation, provider registration, attaching a shared requestId to HTTP requests, and applying the directive to target UI elements
- Angular v22 or newer, declaring peer dependencies
@angular/commonand@angular/coreat^22.0.0. - Standalone APIs, functional HTTP interceptors, and signals enabled (default in v22).
- The default ID generator uses
crypto.randomUUID(), requiring a secure browser context (HTTPS orlocalhost).
Install the package from npm. No runtime dependencies exist other than tslib
npm install ngx-request-lock
provideRequestLock() is an environment provider factory that registers the functional interceptor. Add it to the providers array in ApplicationConfig
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRequestLock } from 'ngx-request-lock';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideRequestLock(),
],
};
If the application configures provideHttpClient with existing interceptors, omit provideRequestLock() and register requestLockInterceptor directly
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { requestLockInterceptor } from 'ngx-request-lock';
import { authInterceptor } from './core/http/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, requestLockInterceptor]),
),
],
};
The interceptor processes only requests carrying a value under REQUEST_LOCK_ID in their HttpContext. Use createRequestLockContext(id) to build this context
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { createRequestLockContext } from 'ngx-request-lock';
const http = inject(HttpClient);
http.post('/api/users', payload, {
context: createRequestLockContext(id),
}).subscribe();
REQUEST_LOCK_IDis anHttpContextToken<string | null>defaulting tonull.createRequestLockContext(id)returns anHttpContextwith the specified token. Ifidis falsy, an empty context is returned and the interceptor skips the request.- Untracked HTTP requests pass through unmodified.
RequestLockDirective is a standalone directive with selector [ngxRequestLock] and exportAs: 'requestLock'. Import it into the component's imports array and apply it to target elements
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
RequestLockDirective,
createRequestLockContext,
} from 'ngx-request-lock';
@Component({
selector: 'ngx-save-button',
imports: [RequestLockDirective],
template: `
<button ngxRequestLock #lock="requestLock" (click)="save(lock.requestId())">
Save
</button>
`,
})
export class SaveButton {
private readonly http = inject(HttpClient);
protected save(id: string): void {
this.http
.post('/api/users', {}, { context: createRequestLockContext(id) })
.subscribe();
}
}
- Each directive instance generates a unique
requestIdviacrypto.randomUUID(). Bind[requestId]explicitly to share an ID across multiple elements and coordinate a flow. - Export the instance via
#lock="requestLock"to readlock.requestId()for single-element locks. For shared flows, manage the ID in the component (e.g.,signal(crypto.randomUUID())) and bind it to directives andcreateRequestLockContext(...)calls. - The directive locates the button via
elementRef.closest('button')or the first child<button>, toggling itsdisabledattribute. Non-button host elements receive no DOM modifications, but their lock state still updates the shared signal for wrapper components.
UI unlocking occurs automatically as HTTP request lifecycles complete
- On click, the target directive sets
isBlocked = trueand disables its resolved button. requestLockInterceptorreads the ID fromHttpContextand callsRequestLockService.start(id). Each response pipes throughfinalize(() => service.end(id)), decrementing the counter on success, error, or unsubscription.RequestLockServicemaintains a reference counter signalsignal<Record<string, number>>and exposesisPending(id).- Directives and components reading
isPending(id)observe the signal inside aneffect, re-enabling controls once the counter reaches zero.
Safety timeouts
Two timeouts prevent elements from staying disabled indefinitely:
- 500 ms: unblocks the directive if no pending HTTP requests are detected (handling actions that fire no network calls).
- 10 s: unconditional ceiling that unlocks elements if requests hang.
Complete provider configuration in application bootstrap and directive usage in a component
app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRequestLock } from 'ngx-request-lock';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideRequestLock(),
],
};
ping.component.ts
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
RequestLockDirective,
createRequestLockContext,
} from 'ngx-request-lock';
@Component({
selector: 'ngx-ping',
imports: [RequestLockDirective],
template: `
<button
ngxRequestLock
#lock="requestLock"
type="button"
(click)="ping(lock.requestId())"
>
Ping
</button>
`,
})
export class Ping {
private readonly http = inject(HttpClient);
protected ping(id: string): void {
this.http
.get('/api/ping', { context: createRequestLockContext(id) })
.subscribe();
}
}