Directive Usage
Using RequestLockDirective for single interactive elements. Each directive instance generates a requestId, tags the HTTP request via createRequestLockContext(id), and unlocks automatically when the request settles. For multi-element or multi-request scenarios, see Flow Lock Examples
Minimal setup for a single interaction: element, directive, template reference requestLock, and an HTTP request using createRequestLockContext(id)
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
RequestLockDirective,
createRequestLockContext,
} from 'ngx-request-lock';
@Component({
selector: 'ngx-basic-example',
imports: [RequestLockDirective],
template: `
<button ngxRequestLock #lock="requestLock" (click)="ping(lock.requestId())">
Ping
</button>
`,
})
export class BasicExample {
private readonly http = inject(HttpClient);
protected ping(id: string): void {
this.http
.get('/api/ping', { context: createRequestLockContext(id) })
.subscribe();
}
}
Live demo
#lock="requestLock"exposes the directive instance to the template.lock.requestId()provides a stable UUID for the instance.- The element remains disabled until the HTTP request settles.
The pattern applies to any HTTP action by changing the verb and payload while keeping the directive, exported reference, and context helper identical
Save (POST)
@Component({
selector: 'ngx-save-user',
imports: [RequestLockDirective],
template: `
<button ngxRequestLock #lock="requestLock" (click)="save(lock.requestId())">
Save
</button>
`,
})
export class SaveUser {
private readonly http = inject(HttpClient);
protected readonly form = inject(FormBuilder).nonNullable.group({
name: [''],
email: [''],
});
protected save(id: string): void {
this.http
.post('/api/users', this.form.getRawValue(), {
context: createRequestLockContext(id),
})
.subscribe();
}
}
Live demo
Delete (DELETE)
@Component({
selector: 'ngx-delete-user',
imports: [RequestLockDirective],
template: `
<button
ngxRequestLock
#lock="requestLock"
class="text-red-600"
(click)="remove(lock.requestId())"
>
Delete
</button>
`,
})
export class DeleteUser {
private readonly http = inject(HttpClient);
protected readonly userId = input.required<string>();
protected remove(id: string): void {
this.http
.delete(`/api/users/${this.userId()}`, {
context: createRequestLockContext(id),
})
.subscribe();
}
}
Live demo
Reactive form submit
@Component({
selector: 'ngx-signup-form',
imports: [ReactiveFormsModule, RequestLockDirective],
template: `
<form [formGroup]="form">
<input formControlName="email" type="email" />
<input formControlName="password" type="password" />
<button
ngxRequestLock
#lock="requestLock"
[disabled]="form.invalid"
(click)="submit(lock.requestId())"
>
Sign up
</button>
</form>
`,
})
export class SignupForm {
private readonly http = inject(HttpClient);
protected readonly form = inject(FormBuilder).nonNullable.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
protected submit(id: string): void {
if (this.form.invalid) return;
this.http
.post('/api/signup', this.form.getRawValue(), {
context: createRequestLockContext(id),
})
.subscribe();
}
}
Live demo
To update labels or display spinners during request execution (e.g., swapping Save to Saving...), use the reactive signal provided by the service. Read RequestLockService.isPending(lock.requestId()) inside a computed() signal to update template state
Live demo
Signal-driven pending state
import { Component, computed, inject, signal, viewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
RequestLockDirective,
RequestLockService,
createRequestLockContext,
} from 'ngx-request-lock';
@Component({
selector: 'ngx-pending-save',
imports: [RequestLockDirective],
template: `
<button
ngxRequestLock
#lock="requestLock"
type="button"
[attr.aria-busy]="isPending() ? 'true' : null"
(click)="save(lock.requestId())"
>
@if (isPending()) {
<span class="spinner" aria-hidden="true"></span>
<span>Saving...</span>
} @else {
<span>Save</span>
}
</button>
`,
})
export class PendingSave {
private readonly http = inject(HttpClient);
private readonly lockService = inject(RequestLockService);
private readonly lock = viewChild.required(RequestLockDirective);
// Reactive pending flag derived from the shared service.
protected readonly isPending = computed(() =>
this.lockService.isPending(this.lock().requestId)(),
);
protected save(id: string): void {
this.http
.post('/api/users', {}, { context: createRequestLockContext(id) })
.subscribe();
}
}
viewChild.required(RequestLockDirective)accesses the directive instance from the component class.computed(() => service.isPending(lock().requestId)())converts service state into a boolean signal.- Use
@if (isPending())in the template to swap labels or icons without managing manual loading flags orfinalizecallbacks. - Set
[attr.aria-busy]during active requests to inform assistive technologies.
RequestLockDirective only toggles the native disabled attribute on target buttons. To add custom spinners or animations, extend the directive and override setBlockStatus
LoadingRequestLockDirective
import { Directive, Renderer2, inject } from '@angular/core';
import { RequestLockDirective } from 'ngx-request-lock';
/**
* Docs-app-only example.
* Extends RequestLockDirective and overrides setBlockStatus to render
* a custom loading animation instead of toggling [disabled].
*/
@Directive({
selector: '[ngxLoadingRequestLock]',
exportAs: 'loadingRequestLock',
})
export class LoadingRequestLockDirective extends RequestLockDirective {
private readonly localRenderer = inject(Renderer2);
private spinner: HTMLElement | null = null;
protected override setBlockStatus(): void {
if (!this.button) return;
if (this.isBlocked) {
this.localRenderer.setAttribute(this.button, 'aria-disabled', 'true');
this.localRenderer.setAttribute(this.button, 'aria-busy', 'true');
this.localRenderer.addClass(this.button, 'ngx-lock-loading');
this.attachSpinner();
return;
}
this.localRenderer.removeAttribute(this.button, 'aria-disabled');
this.localRenderer.removeAttribute(this.button, 'aria-busy');
this.localRenderer.removeClass(this.button, 'ngx-lock-loading');
this.detachSpinner();
}
private attachSpinner(): void {
if (this.spinner || !this.button) return;
const el = this.localRenderer.createElement('span') as HTMLElement;
this.localRenderer.addClass(el, 'ngx-lock-spinner');
this.localRenderer.setAttribute(el, 'aria-hidden', 'true');
this.localRenderer.appendChild(this.button, el);
this.spinner = el;
}
private detachSpinner(): void {
if (!this.spinner || !this.button) return;
this.localRenderer.removeChild(this.button, this.spinner);
this.spinner = null;
}
}
Usage matches the base directive; only the selector and exported name change
Usage
@Component({
selector: 'ngx-fancy-save',
imports: [LoadingRequestLockDirective],
template: `
<button
ngxLoadingRequestLock
#lock="loadingRequestLock"
(click)="save(lock.requestId())"
>
<span>Save</span>
</button>
`,
})
export class FancySave {
private readonly http = inject(HttpClient);
protected save(id: string): void {
this.http
.post('/api/users', {}, { context: createRequestLockContext(id) })
.subscribe();
}
}
Consumer styles
/* consumer styles */
.ngx-lock-loading {
position: relative;
cursor: not-allowed;
opacity: 0.75;
pointer-events: none;
}
.ngx-lock-spinner {
display: inline-block;
width: 0.9em;
height: 0.9em;
margin-left: 0.5em;
border-radius: 9999px;
border: 2px solid currentColor;
border-top-color: transparent;
animation: ngx-lock-spin 0.6s linear infinite;
}
@keyframes ngx-lock-spin {
to { transform: rotate(360deg); }
}
Live demo