events API

events

package

API reference for the events package.

T
type

Event

Event is a type-safe application event identifier.

events/events.go:13-13
type Event string
S
struct

busHandler

busHandler pairs a handler with a stable ID so unsubscribe can remove it:
comparing function values is not valid in Go, so removal goes by ID.

events/events.go:27-30
type busHandler struct

Fields

Name Type Description
id int
fn func(any)
S
struct

eventBus

events/events.go:32-36
type eventBus struct

Methods

On
Method

On registers a handler for an application event and returns an unsubscribe function.

Parameters

event Event
handler func(any)

Returns

func()
func (*eventBus) On(event Event, handler func(any)) func()
{
	b.mu.Lock()
	b.nextID++
	id := b.nextID
	b.handlers[event] = append(b.handlers[event], busHandler{id: id, fn: handler})
	b.mu.Unlock()
	return func() {
		b.mu.Lock()
		defer b.mu.Unlock()
		handlers := b.handlers[event]
		for i, h := range handlers {
			if h.id == id {
				b.handlers[event] = append(handlers[:i], handlers[i+1:]...)
				return
			}
		}
	}
}
Emit
Method

Emit dispatches an application event to all registered handlers.

Parameters

event Event
data any
func (*eventBus) Emit(event Event, data any)
{
	b.mu.RLock()
	handlers := make([]busHandler, len(b.handlers[event]))
	copy(handlers, b.handlers[event])
	b.mu.RUnlock()
	for _, h := range handlers {
		h.fn(data)
	}
}

Fields

Name Type Description
mu sync.RWMutex
handlers map[Event][]busHandler
nextID int
F
function

OnApp

OnApp registers a handler for an application event. Returns unsubscribe function.

Parameters

event
handler
func(any)

Returns

func()
events/events.go:70-72
func OnApp(event Event, handler func(any)) func()

{
	return bus.On(event, handler)
}
F
function

EmitApp

EmitApp dispatches an application event.

Parameters

event
data
any
events/events.go:75-77
func EmitApp(event Event, data any)

{
	bus.Emit(event, data)
}
F
function

On

On attaches a handler function for the given DOM event to target.
Optional opts are forwarded to addEventListener as-is.
It returns a function that removes the listener and releases resources.

Parameters

event
string
target
handler
func(js.Value)
opts
...any

Returns

func()
events/events.go:82-100
func On(event string, target js.Value, handler func(js.Value), opts ...any) func()

{
	fn := js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		if len(args) > 0 {
			handler(args[0])
		} else {
			handler(js.Null())
		}
		return nil
	})
	callArgs := []any{event, fn}
	if len(opts) > 0 {
		callArgs = append(callArgs, opts...)
	}
	target.Call("addEventListener", callArgs...)
	return func() {
		target.Call("removeEventListener", event, fn)
		fn.Release()
	}
}
F
function

Once

Once attaches a handler that fires at most one time: the listener is removed
and its callback released as soon as the event lands. One-shot browser
callbacks (FileReader load, Image load, transitionend) otherwise leak a
js.Func per call, since there is no natural moment to call the stop function.
The returned function cancels a listener that has not fired yet and is a
no-op afterwards.

Parameters

event
string
target
handler
func(js.Value)

Returns

func()
events/events.go:108-130
func Once(event string, target js.Value, handler func(js.Value)) func()

{
	var fn js.Func
	fired := false
	release := func() {
		if fired {
			return
		}
		fired = true
		target.Call("removeEventListener", event, fn)
		fn.Release()
	}
	fn = js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		evt := js.Null()
		if len(args) > 0 {
			evt = args[0]
		}
		release()
		handler(evt)
		return nil
	})
	target.Call("addEventListener", event, fn)
	return release
}
F
function

OnClick

OnClick attaches a click handler to target.

Parameters

target
handler
func(js.Value)

Returns

func()
events/events.go:133-135
func OnClick(target js.Value, handler func(js.Value)) func()

{
	return On("click", target, handler)
}
F
function

OnScroll

OnScroll attaches a scroll handler to target.

Parameters

target
handler
func(js.Value)

Returns

func()
events/events.go:138-140
func OnScroll(target js.Value, handler func(js.Value)) func()

{
	return On("scroll", target, handler)
}
F
function

OnInput

OnInput attaches an input handler to target.

Parameters

target
handler
func(js.Value)

Returns

func()
events/events.go:143-145
func OnInput(target js.Value, handler func(js.Value)) func()

{
	return On("input", target, handler)
}
F
function

OnTimeUpdate

OnTimeUpdate attaches a timeupdate handler to target.

Parameters

target
handler
func(js.Value)

Returns

func()
events/events.go:148-150
func OnTimeUpdate(target js.Value, handler func(js.Value)) func()

{
	return On("timeupdate", target, handler)
}
F
function

OnKeyDown

OnKeyDown attaches a keydown handler to the window object.

Parameters

handler
func(js.Value)

Returns

func()
events/events.go:153-155
func OnKeyDown(handler func(js.Value)) func()

{
	return On("keydown", js.Window(), handler)
}
F
function

OnKeyUp

OnKeyUp attaches a keyup handler to the window object.

Parameters

handler
func(js.Value)

Returns

func()
events/events.go:158-160
func OnKeyUp(handler func(js.Value)) func()

{
	return On("keyup", js.Window(), handler)
}
F
function

Listen

Listen attaches an event listener to target and returns a channel that
receives the first argument of the event callback, plus a stop function.
The stop function removes the listener, releases the underlying js.Func and
closes the channel so range loops over it terminate; without calling it
every Listen leaks a listener and its goroutine across bind/unmount cycles.

Parameters

event
string
target

Returns

<-chan
func()
events/events.go:167-184
func Listen(event string, target js.Value) (<-chan js.Value, func())

{
	ch := make(chan js.Value)
	fn := js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		if len(args) > 0 {
			ch <- args[0]
		} else {
			ch <- js.Null()
		}
		return nil
	})
	target.Call("addEventListener", event, fn)
	stop := func() {
		target.Call("removeEventListener", event, fn)
		fn.Release()
		close(ch)
	}
	return ch, stop
}
F
function

ObserveMutations

ObserveMutations observes DOM mutations on the first node matching sel.
It returns a channel receiving MutationRecord objects and a stop function
that disconnects the observer and releases resources.

Parameters

sel
string

Returns

<-chan
func()
events/events.go:189-216
func ObserveMutations(sel string) (<-chan js.Value, func())

{
	ch := make(chan js.Value)
	node := js.Document().Call("querySelector", sel)
	fn := js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		mutations := args[0]
		for i := 0; i < mutations.Length(); i++ {
			m := mutations.Index(i)
			t := m.Get("target")
			if t.Truthy() && t.Get("closest").Type() != js.TypeUndefined {
				if t.Call("closest", "[data-rfw-ignore]").Truthy() {
					continue
				}
			}
			ch <- m
		}
		return nil
	})
	observer := js.MutationObserver().New(fn)
	opts := js.NewDict()
	opts.Set("childList", true)
	opts.Set("subtree", true)
	observer.Call("observe", node, opts.Value)
	stop := func() {
		observer.Call("disconnect")
		fn.Release()
	}
	return ch, stop
}
F
function

ObserveIntersections

ObserveIntersections observes intersections for elements matching sel.
opts is passed directly to the IntersectionObserver constructor.
It returns a channel receiving IntersectionObserverEntry objects and a
stop function to disconnect the observer and release resources.

Parameters

sel
string
opts

Returns

<-chan
func()
events/events.go:222-241
func ObserveIntersections(sel string, opts js.Value) (<-chan js.Value, func())

{
	ch := make(chan js.Value)
	fn := js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		entries := args[0]
		for i := 0; i < entries.Length(); i++ {
			ch <- entries.Index(i)
		}
		return nil
	})
	observer := js.IntersectionObserver().New(fn, opts)
	nodes := js.Document().Call("querySelectorAll", sel)
	for i := 0; i < nodes.Length(); i++ {
		observer.Call("observe", nodes.Index(i))
	}
	stop := func() {
		observer.Call("disconnect")
		fn.Release()
	}
	return ch, stop
}
F
function

TestPlaceholder

Parameters

events/events_test.go:5-5
func TestPlaceholder(_ *testing.T)

{}
F
function

TestOnAppUnsubscribe

Unsubscribe must remove exactly the handler it was returned for; the old
implementation compared local variable addresses and never removed anything.

Parameters

events/events_wasm_test.go:14-32
func TestOnAppUnsubscribe(t *testing.T)

{
	const ev Event = "test:unsub"
	var first, second int
	off1 := OnApp(ev, func(any) { first++ })
	off2 := OnApp(ev, func(any) { second++ })

	EmitApp(ev, nil)
	off1()
	EmitApp(ev, nil)
	off2()
	EmitApp(ev, nil)

	if first != 1 {
		t.Fatalf("first handler expected 1 call, got %d", first)
	}
	if second != 2 {
		t.Fatalf("second handler expected 2 calls, got %d", second)
	}
}
F
function

TestListenStop

Listen’s stop function removes the listener, releases the js.Func and closes
the channel so consumer goroutines terminate.

Parameters

events/events_wasm_test.go:36-69
func TestListenStop(t *testing.T)

{
	doc := js.Document()
	el := doc.Call("createElement", "div")
	doc.Get("body").Call("appendChild", el)
	defer el.Call("remove")

	ch, stop := Listen("click", el)
	received := 0
	done := make(chan struct{})
	go func() {
		for range ch {
			received++
		}
		close(done)
	}()

	el.Call("click")
	// The dispatch is synchronous but delivery goes through the channel; give
	// the consumer goroutine a beat before stopping.
	time.Sleep(50 * time.Millisecond)
	stop()

	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatalf("consumer goroutine did not terminate after stop")
	}
	if received != 1 {
		t.Fatalf("expected 1 event before stop, got %d", received)
	}
	// Events after stop must not fire the released handler (would panic on a
	// released js.Func if the listener were still attached).
	el.Call("click")
}
F
function

dispatch

Parameters

target
name
string
events/once_test.go:11-14
func dispatch(target js.Value, name string)

{
	evt := js.CustomEvent().New(name)
	target.Call("dispatchEvent", evt)
}
F
function

TestOnceFiresOnlyOnce

Parameters

events/once_test.go:16-27
func TestOnceFiresOnlyOnce(t *testing.T)

{
	target := js.Document().Call("createElement", "div")
	calls := 0
	Once("ping", target, func(js.Value) { calls++ })

	dispatch(target, "ping")
	dispatch(target, "ping")

	if calls != 1 {
		t.Fatalf("handler ran %d times, want 1", calls)
	}
}
F
function

TestOnceCancelBeforeFiring

Parameters

events/once_test.go:29-40
func TestOnceCancelBeforeFiring(t *testing.T)

{
	target := js.Document().Call("createElement", "div")
	calls := 0
	cancel := Once("ping", target, func(js.Value) { calls++ })
	cancel()

	dispatch(target, "ping")

	if calls != 0 {
		t.Fatalf("handler ran %d times after cancel, want 0", calls)
	}
}
F
function

TestOnceCancelAfterFiringIsNoop

Parameters

events/once_test.go:42-48
func TestOnceCancelAfterFiringIsNoop(_ *testing.T)

{
	target := js.Document().Call("createElement", "div")
	cancel := Once("ping", target, func(js.Value) {})
	dispatch(target, "ping")
	cancel()
	cancel()
}
F
function

TestOnRecoversHandlerPanic

A panicking handler must not take the wasm instance down with it: the
framework’s own listeners carry the recover guard, so an application that
uses events.On never has to reach for SafeFuncOf itself.

Parameters

events/safe_test.go:14-33
func TestOnRecoversHandlerPanic(t *testing.T)

{
	prev := js.OnFuncPanic
	defer func() { js.OnFuncPanic = prev }()

	var got any
	js.OnFuncPanic = func(r any, _ []byte) { got = r }

	target := js.Document().Call("createElement", "div")
	stop := On("boom", target, func(js.Value) { panic("handler exploded") })
	defer stop()

	target.Call("dispatchEvent", js.CustomEvent().New("boom"))

	if got == nil {
		t.Fatal("panic escaped the listener")
	}
	if s, ok := got.(string); !ok || s != "handler exploded" {
		t.Fatalf("unexpected recovered value: %v", got)
	}
}
F
function

TestOnceRecoversHandlerPanic

Parameters

events/safe_test.go:35-49
func TestOnceRecoversHandlerPanic(t *testing.T)

{
	prev := js.OnFuncPanic
	defer func() { js.OnFuncPanic = prev }()

	var got any
	js.OnFuncPanic = func(r any, _ []byte) { got = r }

	target := js.Document().Call("createElement", "div")
	Once("boom", target, func(js.Value) { panic("once exploded") })
	target.Call("dispatchEvent", js.CustomEvent().New("boom"))

	if got == nil {
		t.Fatal("panic escaped the one-shot listener")
	}
}