Watchstop
00:00.00
Frameworks

Angular

@watchstop/angular adapter.

Adapter bridging Store into an Angular signal, cleaned up with DestroyRef. @watchstop/core is a peer dependency — install both.

Exact public names

injectStopwatch is the entire public API.

type InjectStopwatchOptions =
  | { clock?: Clock; precisionMs?: number }
  | { stopwatch: Stopwatch }

type StopwatchBinding = {
  elapsed: Signal<number>
  running: Signal<boolean>
  start: () => void
  stop: () => void
  reset: () => void
  stopwatch: Stopwatch
}

declare function injectStopwatch(options?: InjectStopwatchOptions): StopwatchBinding

injectStopwatch

injectStopwatch owns a Stopwatch and its teardown, so a component that needs its own timer imports one thing and holds no instance itself. It must run in an injection context (constructor, field initializer, factory, or runInInjectionContext).

import { Component } from '@angular/core'
import { injectStopwatch } from '@watchstop/angular'

@Component({
  selector: 'app-timer',
  template: `
    <p>{{ elapsed() }} ms</p>
    <button (click)="running() ? stop() : start()">{{ running() ? 'Stop' : 'Start' }}</button>
    <button (click)="reset()">Reset</button>
  `,
})
export class Timer {
  readonly binding = injectStopwatch()
  readonly elapsed = this.binding.elapsed
  readonly running = this.binding.running
  readonly start = this.binding.start
  readonly stop = this.binding.stop
  readonly reset = this.binding.reset
}
  • Construction is inert. Nothing is scheduled until start().
  • start, stop, and reset keep the same identity for the life of the owner.
  • DestroyRef.onDestroy calls destroy() when the injecting component, directive, or injector is destroyed, after the unsubscribe registered for the signal.
  • elapsed is a readonly Signal<number>. stopwatch is the owned instance, exposed for passing elsewhere; do not call destroy() on it yourself.

Options

OptionTypePurpose
clockClockOwned mode: use this clock instead of detectClock(). Pass createMockClock() in tests.
precisionMsnumberOwned mode: coarsen notify cadence — see Options.
stopwatchStopwatchBorrowed mode: bind this instance; do not pass clock / precisionMs.

Sharing one stopwatch across components

Pass the same core instance into each inject call:

import { Component } from '@angular/core'
import { Stopwatch } from '@watchstop/core'
import { injectStopwatch } from '@watchstop/angular'

const session = new Stopwatch()

@Component({
  selector: 'app-session-chip',
  template: `<!-- ... -->`,
})
export class SessionChip {
  readonly binding = injectStopwatch({ stopwatch: session })
}

The adapter never calls destroy() on a borrowed instance. Own teardown yourself when the session ends, or leave a module-level instance alive for the page lifetime.

Contract

  • The signal starts at store.get() and is written only from subscribe.
  • DestroyRef unsubscribes and destroys when the owning injection context is destroyed.
  • The returned signal is read-only; controls stay on the Stopwatch.

Re-render cost

elapsed is raw milliseconds delivered at the clock's tick cadence, so anything reading the signal re-renders roughly 60 times a second under createBrowserClock. Pass precisionMs to coarsen notifies — see Options. Keep the elapsed read in a small component when you still want finer UI.

See Store and Spec.

On this page