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): StopwatchBindingcreateStopwatch
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(). initsubscribes and writeselapsedthroughthisso Alpine's reactive proxy sees updates.destroyunsubscribes and callsstopwatch.destroy(). When Alpine removes the component it invokesdestroyfor you. Outside Alpine, calldestroy()yourself (same ownership story as Svelte outside component init).start,stop, andresetkeep the same identity for the life of the binding.stopwatchis the owned instance, exposed for passing elsewhere; do not calldestroy()on it yourself when Alpine (or you) already owns teardown viabinding.destroy().
Options
| Option | Type | Purpose |
|---|---|---|
clock | Clock | Owned mode: use this clock instead of detectClock(). Pass createMockClock() in tests. |
precisionMs | number | Owned mode: coarsen notify cadence — see Options. |
stopwatch | Stopwatch | Borrowed 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.