Skip to content

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)

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

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

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

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

typescript
          
            
              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 or finalize callbacks.
  • 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

typescript
          
            
              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

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

css
          
            
              /* 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

ngx-request-lock

UI locking bound to HTTP request lifecycle for Angular applications

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