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
| Name | Type | Default / behavior |
|---|---|---|
mode | Mode | Partial<Record<PolicyId, Mode>> | 'enforce'. Unspecified policies in a mode map remain enforced. |
policies | PolicyId[] | All four policies by default. |
cooldownMs | number | 500. Age policy threshold in milliseconds. |
rectThresholdPx | number | 4. Continuity movement tolerance. |
refractoryMs | number | 300. DoubleFire recency window. |
semanticsTextCheck | boolean | false. Also compare target text. |
clickSuppressMs | number | 500. Matching native click window. |
onBlocked | (element: Element) => void | Called after an enforced activation is canceled; replaces the default shake. |
blockAnimation | boolean | true. Enables the default shake when no custom callback is set. |
blockAnimationMs | number | 300. Default shake duration. |
(decision) | Decision | Output. Emits for allowed and suspicious decisions. |
(suspect) | Decision | Output. 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: () => elementto guard a specific element instead ofdocument. 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 fromprovideActivationGuard(). 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
}
}