astwerk

Stopwatch

A running timer ticks an elapsed signal and a text binding renders it — the display doesn't know or care that it's a timer. Compiled markup has no combinator for "every 100ms"; this is what wasmwrap.SetInterval is for, so this demo is WebAssembly-only.

0:00.0

How it works

State

go
elapsed := reactive.NewSignal(0.0)

One signal, holding seconds as a float. It starts at zero and nothing about it says "timer" — it's a value like any other.

The tick

go
cancel := wasmwrap.SetInterval(func() {
		elapsed.Update(func(v float64) float64 { return v + 0.1 })
	}, 100)

	// start:   cancel = wasmwrap.SetInterval(...)
	// pause:   cancel()

SetInterval runs the function every 100ms; the returned cancel stops it. The tick is the only place that knows this demo has anything to do with time.

The display

go
reactive.BindText(root.Find("[data-time]"), func() string {
		v := elapsed.Get()
		ms := int(v*1000) % 1000 / 100
		sec := int(v) % 60
		min := int(v) / 60
		return fmt.Sprintf("%d:%02d.%d", min, sec, ms)
	})

BindText re-runs on every tick because it reads elapsed.Get() — the formatting is just a function of the value, so it can be as simple or as fancy as you like.

Start, pause, reset

go
reactive.On(start, "click", func(wasmwrap.Event) {
		if running {
			cancel()
			start.SetText("Resume")
		} else {
			cancel = wasmwrap.SetInterval(func() {
				elapsed.Update(func(v float64) float64 { return v + 0.1 })
			}, 100)
			start.SetText("Pause")
		}
		running = !running
	})
	reactive.On(reset, "click", func(wasmwrap.Event) { elapsed.Set(0) })

The start button flips between starting and cancelling the interval and relabels itself. Reset just writes zero — the display updates on its own.

Full source
go
elapsed := reactive.NewSignal(0.0)
	var cancel func()
	running := false

	reactive.Hydrate("#stopwatch-demo", func(root wasmwrap.Element) {
		start := root.Find("[data-start]")
		reactive.BindText(root.Find("[data-time]"), func() string {
			v := elapsed.Get()
			ms := int(v*1000) % 1000 / 100
			sec := int(v) % 60
			min := int(v) / 60
			return fmt.Sprintf("%d:%02d.%d", min, sec, ms)
		})

		reactive.On(start, "click", func(wasmwrap.Event) {
			if running {
				cancel()
				start.SetText("Resume")
			} else {
				cancel = wasmwrap.SetInterval(func() {
					elapsed.Update(func(v float64) float64 { return v + 0.1 })
				}, 100)
				start.SetText("Pause")
			}
			running = !running
		})
		reactive.On(root.Find("[data-reset]"), "click", func(wasmwrap.Event) {
			elapsed.Set(0)
		})
	})
How this demo runs

scripts/demo.go is compiled to demo.wasm by CompileFrom and loaded by WasmScript — see Scripts & WebAssembly. Without WebAssembly the page still renders; only the time stays put.