DEVELOPER DOCUMENTATION

Angular API reference

Guard directive, app-wide provider, per-element target directive, callbacks, and shared guard options for Angular 22 and later.

DIRECTIVE · GUARD ROOT

[sparshGuard]

Add sparshGuard to any element to guard everything inside it. The element itself becomes the guard root, so no wrapper element is rendered. The guard attaches after the first render and removes its listeners when the element is destroyed. It never runs during server-side rendering.

Changing a guard configuration input recreates the guard and resets in-flight interaction observations. Keep configuration stable during a press. Changes to onBlocked and the output handlers are picked up without recreating the guard.

import { Component } from '@angular/core'
import { ActivationGuardDirective, type Decision } from '@sparshlabs/angular'

@Component({
  selector: 'app-root',
  imports: [ActivationGuardDirective],
  template: `
    <main
      sparshGuard
      mode="enforce"
      [cooldownMs]="500"
      (decision)="handleDecision($event)"
    >
      <app-shell />
    </main>
  `,
})
export class AppComponent {
  handleDecision(decision: Decision) {
    if (!decision.allowed) {
      console.info('Suspicious activation:', decision.reason)
    }
  }
}

Numeric and boolean inputs also accept static attribute strings, such as cooldownMs="800". Import ActivationGuardDirective into each standalone component (or NgModule) whose template uses it.

Inputs and outputs

NameTypeDefault / behavior
modeMode | Partial<Record<PolicyId, Mode>>'enforce'. Unspecified policies in a mode map remain enforced.
policiesPolicyId[]All four policies by default.
cooldownMsnumber500. Age policy threshold in milliseconds.
rectThresholdPxnumber4. Continuity movement tolerance.
refractoryMsnumber300. DoubleFire recency window.
semanticsTextCheckbooleanfalse. Also compare target text.
clickSuppressMsnumber500. Matching native click window.
onBlocked(element: Element) => voidCalled after an enforced activation is canceled; replaces the default shake.
blockAnimationbooleantrue. Enables the default shake when no custom callback is set.
blockAnimationMsnumber300. Default shake duration.
(decision)DecisionOutput. Emits for allowed and suspicious decisions.
(suspect)DecisionOutput. Emits when a policy verdict is suspicious, including report mode.

PROVIDER · APP-WIDE

provideActivationGuard()

Guards the whole document from your application config, or from any environment injector such as a lazy route’s providers. It takes the same options as the directive, with onDecision and onSuspect callbacks in place of outputs.

// app.config.ts
import type { ApplicationConfig } from '@angular/core'
import { provideActivationGuard } from '@sparshlabs/angular'

export const appConfig: ApplicationConfig = {
  providers: [
    provideActivationGuard({
      mode: 'enforce',
      onSuspect: (decision) => console.info('Suspicious activation:', decision.reason),
    }),
  ],
}
  • Pass root: () => element to guard a specific element instead of document. It’s a factory so it runs only in the browser, after the first render.
  • Options are fixed for the injector’s lifetime. Use the directive for options that change at runtime.
  • The guard is removed when the injector is destroyed.

DIRECTIVE · ELEMENT FEEDBACK

[sparshGuardTarget]

Put sparshGuardTarget on an element to observe it, and read its isGuarded() signal through a template reference. The signal becomes true when that element is actually blocked.

import { Component } from '@angular/core'
import { ActivationGuardTarget } from '@sparshlabs/angular'

@Component({
  selector: 'app-save-button',
  imports: [ActivationGuardTarget],
  template: `
    <button type="button" sparshGuardTarget #guard="sparshGuardTarget">
      Save draft
    </button>
    @if (guard.isGuarded()) {
      <p role="status">The screen changed. Try again.</p>
    }
  `,
})
export class SaveButtonComponent {}
  • It uses the nearest [sparshGuard] ancestor, otherwise the app-wide guard from provideActivationGuard().
  • isGuarded() resets after an allowed activation.
  • Without any guard above it, the directive is a no-op and does not throw.

DIRECTIVE · SCOPED GUARD

Nested [sparshGuard]

A sparshGuard inside another one installs a separate guard on its own subtree. Options are not merged with an ancestor’s, and descendants’ [sparshGuardTarget] directives use the nearest guard.

<section sparshGuard [mode]="{ age: 'report' }">
  <app-frequently-updated-panel />
</section>

Nesting guards: each guard installs its own capture-phase listeners and policy pipeline. Nothing is merged or inherited from an ancestor. When guards overlap the same DOM, the outermost one sees pointer, click, and key events first. If it blocks, the inner guard never sees that interaction. That’s the intended behavior for a scoped override. Guarding the same region twice by accident, for example with both provideActivationGuard() and a top-level sparshGuard, is usually a mistake. sparsh logs a console.warn whenever a new guard’s root overlaps one that’s already active, so you can tell the two cases apart.

BLOCK FEEDBACK

Customize the blocked animation

onBlocked runs after cancellation and receives the interactive element. A callback exception is ignored so it cannot affect the guard’s decision. Define it as an arrow function property if it needs this.

import { Component } from '@angular/core'
import { ActivationGuardDirective } from '@sparshlabs/angular'

@Component({
  selector: 'app-root',
  imports: [ActivationGuardDirective],
  template: `
    <main sparshGuard [onBlocked]="animateBlocked">
      <app-shell />
    </main>
  `,
})
export class AppComponent {
  animateBlocked = (element: Element) => {
    element.animate(
      [
        { transform: 'scale(1)' },
        { transform: 'scale(0.96)' },
        { transform: 'scale(1)' },
      ],
      { duration: 220, easing: 'ease-out' },
    )
  }
}

RELATED TYPES

Decision and mode

Mode, PolicyId, and Decision are re-exported from @sparshlabs/angular. A decision’s allowed field records the policy verdict; enforced says whether it actually stopped the activation. See Core concepts for policy behavior and accessibility details.

import type { InputKind, Phase, PolicyId } from '@sparshlabs/core'
import type { Signal } from '@angular/core'

// [sparshGuardTarget], via exportAs: 'sparshGuardTarget'
export declare class ActivationGuardTarget {
  readonly isGuarded: Signal<boolean>
}

export interface Decision {
  allowed: boolean
  enforced: boolean
  policy?: PolicyId
  reason?: string
  event: { kind: InputKind; phase: Phase }
  target: {
    identity: unknown
    ageMs: number
    guardKey?: string
  }
}