Skip to content

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

typescript
          
            
              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 ngxRequestLock instances 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 finalize callback.
  • 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

typescript
          
            
              @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

typescript
          
            
              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 requestId can lock a single button, a control group, or an entire panel.
  • No per-component boilerplate: eliminates manual loading flags and finalize callbacks in components.
  • Safe error handling: the interceptor re-enables UI via finalize on 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.

ngx-request-lock

UI locking bound to HTTP request lifecycle for Angular applications

© 2026 Salvatore Di Genua - Built with Angular and Tailwind CSS