Flow Lock Examples
A requestId can attach to multiple UI elements and sequential HTTP calls to lock and unlock them together. Reference counting in RequestLockService ensures the lock persists until every request in the flow settles. For single-element use cases, see Directive Usage
Bind the same ID to primary actions, secondary actions, and follow-up refreshes to synchronize their disabled states
Shared requestId across a flow
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
RequestLockDirective,
createRequestLockContext,
} from 'ngx-request-lock';
/**
* One shared `requestId` coordinates the whole flow, not just a single click.
* Every element bound to the same id observes the same lock signal, so:
*
* - both buttons stay disabled during the mutation,
* - they stay disabled while the automatic refresh (GET) is still in flight,
* - any other related action that reuses the id joins the same lock.
*
* The lock releases only when the entire flow settles.
*/
@Component({
selector: 'ngx-post-flow',
imports: [RequestLockDirective],
template: `
<button
ngxRequestLock
[requestId]="flowId()"
type="button"
(click)="save()"
>
Save
</button>
<button
ngxRequestLock
[requestId]="flowId()"
type="button"
(click)="reset()"
>
Reset
</button>
`,
})
export class PostFlow {
private readonly http = inject(HttpClient);
// The flow owns the id, not the button. Anything tagged with it joins
// the same reference-counted lock.
protected readonly flowId = signal(crypto.randomUUID());
protected save(): void {
const id = this.flowId();
this.http
.post('/api/posts', { title: 'hello' }, {
context: createRequestLockContext(id),
})
.subscribe({
// Follow-up GET reuses the same id. The lock stays held until
// both the POST and the GET have settled.
next: () => this.refresh(id),
});
}
protected reset(): void {
this.refresh(this.flowId());
}
private refresh(id: string): void {
this.http
.get('/api/posts/1', { context: createRequestLockContext(id) })
.subscribe();
}
}
Live demo
Last loaded title: -
- Multiple
ngxRequestLockinstances with the same ID share one reference-counted lock. - Follow-up HTTP calls reusing the ID keep the lock active until they complete.
- Errors on any request release the lock via the interceptor's
finalizecallback. - Component containers or forms can bind the shared ID to disable entire page sections.
Form: POST request followed by automatic GET refresh
A shared requestId across form submit and data reload keeps both the submit and refresh buttons locked until both requests complete
Form: POST + automatic GET refresh
@Component({
selector: 'ngx-user-form',
imports: [ReactiveFormsModule, RequestLockDirective],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="name" />
<input formControlName="email" type="email" />
<button
ngxRequestLock
[requestId]="flowId()"
type="submit"
[disabled]="form.invalid"
>
Save
</button>
<button
ngxRequestLock
[requestId]="flowId()"
type="button"
(click)="refresh()"
>
Refresh
</button>
</form>
`,
})
export class UserForm {
private readonly http = inject(HttpClient);
protected readonly form = inject(FormBuilder).nonNullable.group({
name: [''],
email: [''],
});
// The form, the submit, and the refresh all share one lock.
protected readonly flowId = signal(crypto.randomUUID());
protected submit(): void {
if (this.form.invalid) return;
const id = this.flowId();
this.http
.post<{ id: number }>('/api/users', this.form.getRawValue(), {
context: createRequestLockContext(id),
})
.subscribe({
// Automatic GET refresh reuses the flow id: the whole form stays
// locked until the refresh completes.
next: (user) => this.load(id, user.id),
});
}
protected refresh(): void {
this.load(this.flowId(), /* userId */ 1);
}
private load(id: string, userId: number): void {
this.http
.get(`/api/users/${userId}`, {
context: createRequestLockContext(id),
})
.subscribe();
}
}
A shared requestId can also drive container-level visual states (dimmed panels, overlays, or aria-busy regions) by reading RequestLockService.isPending(flowId)
Panel-level visual in-flight state
import { Component, computed, inject, signal } from '@angular/core';
import {
RequestLockDirective,
RequestLockService,
createRequestLockContext,
} from 'ngx-request-lock';
/**
* The shared `requestId` also drives a visual in-flight state at the panel
* level. The buttons still lock through the directive, but the wrapper reads
* `RequestLockService.isPending(flowId)` and dims the whole card, sets
* `aria-busy`, and renders an overlay spinner while any request in the flow
* is still pending.
*/
@Component({
selector: 'ngx-post-panel',
imports: [RequestLockDirective],
template: `
<section
class="panel"
[class.is-busy]="isPending()"
[attr.aria-busy]="isPending() ? 'true' : null"
>
<button
ngxRequestLock
[requestId]="flowId()"
type="button"
(click)="load()"
>
Load
</button>
<button
ngxRequestLock
[requestId]="flowId()"
type="button"
(click)="destroy()"
>
Delete
</button>
@if (isPending()) {
<div class="overlay" aria-hidden="true">
<span class="spinner"></span>
</div>
}
</section>
`,
})
export class PostPanel {
private readonly http = inject(HttpClient);
private readonly lockService = inject(RequestLockService);
protected readonly flowId = signal(crypto.randomUUID());
// Panel-level pending state. Anything tagged with `flowId` counts,
// regardless of which button (or follow-up call) started it.
protected readonly isPending = computed(() =>
this.lockService.isPending(this.flowId())(),
);
protected load(): void {
this.http
.get('/api/posts/1', {
context: createRequestLockContext(this.flowId()),
})
.subscribe();
}
protected destroy(): void {
this.http
.delete('/api/posts/1', {
context: createRequestLockContext(this.flowId()),
})
.subscribe();
}
}
Live demo
Last loaded post id: -
- Containers do not require
ngxRequestLock; they consume the pending signal directly. - Class bindings (
[class.opacity-60]) manage visuals, while[attr.aria-busy]notifies assistive tech. - Container styling and button disabling stay synchronized through the shared signal.
- This pattern complements per-button locking for comprehensive interface management.
The three core primitives (ngxRequestLock, a shared requestId, and createRequestLockContext) handle both isolated interactions and complex multi-request flows:
- Flexible granularity: the same
requestIdcan lock a single button, a control group, or an entire panel. - No per-component boilerplate: eliminates manual
loadingflags andfinalizecallbacks in components. - Safe error handling: the interceptor re-enables UI via
finalizeon exceptions, 5xx responses, or unsubscriptions. - Automatic flow coordination: reference counting keeps the UI locked until the final request completes.
- Unstyled by design: the library includes no default CSS, allowing full styling freedom in the application.
- Angular-native integration: built on
HttpContext, functional interceptors, and signals without external state managers.