Watchstop
00:00.00
Frameworks

Alpine

@watchstop/alpine adapter.

Alpine has no hook or inject context, so the entry point is a factory — the same rationale as Svelte's createStopwatch. A plugin / Alpine.data registration helper was considered and rejected so the package stays a single entry point. @watchstop/core is a peer dependency — install both.

Exact public names

createStopwatch is the entire public API.

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

type StopwatchBinding = {
  elapsed: number
  running: boolean
  start: () => void
  stop: () => void
  reset: () => void
  stopwatch: Stopwatch
  init: (this: StopwatchBinding) => void
  destroy: () => void
}

declare function createStopwatch(options?: CreateStopwatchOptions): StopwatchBinding

createStopwatch

createStopwatch owns a Stopwatch and returns a binding you can hand to Alpine as x-data (or return from your own Alpine.data factory). Alpine calls init / destroy on component data automatically; that is how subscribe and teardown attach.

<div x-data="createStopwatch()">
  <p x-text="elapsed + ' ms'"></p>
  <button @click="running ? stop() : start()" x-text="running ? 'Stop' : 'Start'"></button>
  <button @click="reset()">Reset</button>
</div>
import Alpine from 'alpinejs'
import { createStopwatch } from '@watchstop/alpine'

window.createStopwatch = createStopwatch
Alpine.start()

Or register a named data factory yourself (still the same package entry point):

Alpine.data('timer', () => createStopwatch())
  • Construction is inert. Nothing is scheduled until start().
  • init subscribes and writes elapsed through this so Alpine's reactive proxy sees updates.
  • destroy unsubscribes and calls stopwatch.destroy(). When Alpine removes the component it invokes destroy for you. Outside Alpine, call destroy() yourself (same ownership story as Svelte outside component init).
  • start, stop, and reset keep the same identity for the life of the binding.
  • stopwatch is the owned instance, exposed for passing elsewhere; do not call destroy() on it yourself when Alpine (or you) already owns teardown via binding.destroy().

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 factory:

import { Stopwatch } from '@watchstop/core'
import { createStopwatch } from '@watchstop/alpine'

const session = new Stopwatch()

Alpine.data('sessionChip', () => createStopwatch({ stopwatch: session }))

The adapter never calls destroy() on a borrowed instance — Alpine's data destroy still unsubscribes, but leaves the shared stopwatch intact. Own teardown yourself when the session ends, or leave a module-level instance alive for the page lifetime.

Why a factory (not a hook)

React, Vue, Solid, and Qwik have hook or composable context. Angular has inject. Alpine does not — there is nowhere for a library to hang ownership without the caller participating. The factory returns { elapsed, start, stop, reset, stopwatch } plus Alpine's usual init / destroy hooks so one object is both the binding and the lifecycle owner.

Re-render cost

elapsed is raw milliseconds delivered at the clock's tick cadence. Pass precisionMs to coarsen notifies — see Options. Keep the x-text / binding read in the smallest possible element when you still want finer UI.

See Store, Architecture, and Spec.

On this page