lily/event

Event handlers attach browser events to DOM elements via CSS selectors. Each handler produces a message that flows into the Store, so anything the user does on the page becomes a tidy little Message value somewhere downstream.

The public API is two type-safe binders, on() and on_decoded(), paired with one constant per DOM event (event.click, event.mouse_down, event.key_down, etc.). The constant fixes the payload type, so the compiler enforces that the handler matches.

Selectors are standard CSS. Lily uses event delegation under the hood, so patterns like on(event.click, selector: "#app") paired with data-msg attributes keep working as you patch the DOM. Handlers are set up once and persist until page unload.

import lily/client
import lily/component
import lily/event
import lily/store

pub fn main() {
  let runtime =
    store.new(Model(count: 0), with: update)
    |> client.start(store.wiring())

  runtime
  |> component.mount(
    selector: "#app",
    to_html: element.to_string,
    to_slot: fn() { element.element("lily-slot", [], []) },
    view: app,
  )
  |> event.on_decoded(
    event: event.click,
    selector: "#app",
    decoder: parse_click,
  )
  |> event.on(event: event.input, selector: "#search", handler: Search)
  |> event.on(
    event: event.key_down,
    selector: "document",
    handler: fn(ke) { KeyPressed(ke.key) },
  )
}

fn parse_click(message_name: String) -> Result(Message, Nil) {
  case message_name {
    "increment" -> Ok(Increment)
    "decrement" -> Ok(Decrement)
    _ -> Error(Nil)
  }
}

For events that need debouncing, throttling, or preventDefault, build an EventOptions with options() and the builder functions, then use on_with_options() or on_decoded_with_options().

runtime
|> event.on_with_options(
  event: event.input,
  selector: "#search",
  options: event.options() |> event.debounce_milliseconds(200),
  handler: Search,
)

All event handlers are JavaScript-only (@target(javascript)).

Types

Data extracted from the DOM element that matched the event handler’s selector. dataset contains all data-* attributes as name/value pairs using their original kebab-case names (e.g., data-card-id"card-id").

pub type ElementData {
  ElementData(dataset: List(#(String, String)))
}

Constructors

  • ElementData(dataset: List(#(String, String)))

A typed handle for a DOM event. The payload parameter is fixed by each constant (e.g. mouse_down is Event(#(Int, Int, ElementData))), so the handler signature is checked at compile time. Pass these constants to on() and friends.

pub opaque type Event(payload)

Optional modifiers for an event handler: debounce, throttle, fire-once, stop-propagation, prevent-default. Build with options() and the dedicated builder functions.

event.options()
|> event.debounce_milliseconds(200)
|> event.stop_propagation
pub opaque type EventOptions

Data extracted from a keyboard event. key is the key name (e.g., "Enter", "ArrowUp", "a"). The modifier flags match the corresponding browser event properties.

pub type KeyEvent {
  KeyEvent(
    key: String,
    ctrl: Bool,
    shift: Bool,
    alt: Bool,
    meta: Bool,
  )
}

Constructors

  • KeyEvent(
      key: String,
      ctrl: Bool,
      shift: Bool,
      alt: Bool,
      meta: Bool,
    )

Values

pub const blur: Event(ElementData)

blur event, fires when an element loses focus.

pub const change: Event(String)

change event, fires when an input value is committed (after blur). For real-time updates, use input.

pub const click: Event(String)

click event, uses delegation against the data-msg attribute. The payload is the matched element’s data-msg value. Pair with on_decoded() so unknown messages can be skipped via Error(Nil).

pub const context_menu: Event(#(Int, Int, ElementData))

contextmenu event, fires on right-click. Payload is (x, y, element) relative to the viewport.

pub const copy: Event(Nil)

copy event, fires when text is copied to the clipboard.

pub const cut: Event(Nil)

cut event, fires when text is cut to the clipboard.

pub fn debounce_milliseconds(
  options: EventOptions,
  value: Int,
) -> EventOptions

Set the debounce delay in milliseconds. Multiple events within the window collapse to a single dispatch fired after the gap.

pub const double_click: Event(ElementData)

dblclick event, fires on a double click.

pub const drag: Event(#(Int, Int))

drag event, fires repeatedly while an element is being dragged. Consider pairing with throttle_milliseconds.

pub const drag_end: Event(ElementData)

dragend event, fires once when a drag operation ends.

pub const drag_over: Event(#(Int, Int, ElementData))

dragover event, fires repeatedly while a dragged element is over a valid drop target. Pair with prevent_default(options) to enable dropping.

pub const drag_start: Event(#(Int, Int, ElementData))

dragstart event, fires once when a drag operation starts.

pub const drop: Event(#(Int, Int, ElementData))

drop event, fires when a dragged element is dropped on a valid target.

pub fn focus(
  runtime: client.Runtime(model, message),
  selector: String,
) -> Nil

Programmatically move focus to the first element matching selector. Runs after the next paint so the call is safe from a client.on_message hook whose dispatch may have just rendered the target element. No-op if the selector matches nothing.

client.on_message(runtime, fn(message, _model) {
  case message {
    OpenDialog -> event.focus(runtime, "#dialog-cancel")
    CloseDialog -> event.focus(runtime, "#dialog-trigger")
    _ -> Nil
  }
})
pub const focus_event: Event(ElementData)

focus event, fires when an element receives focus. Named focus_event to avoid collision with the focus() function.

pub fn focus_trap(
  runtime: client.Runtime(model, message),
  within within: String,
  release_on release_on: fn(String) -> Bool,
  on_exit on_exit: fn() -> message,
) -> Nil

Confine Tab and Shift+Tab cycling to focusable descendants of the element matching within. Focusable elements are re-enumerated on every Tab press so dynamic content inside the container is handled. The release_on predicate runs on every keydown inside the document while the trap is active; returning True releases the trap and dispatches the message produced by on_exit. Only one trap can be active at a time, activating a new one silently replaces the previous trap (no on_exit).

Pair with focus to seed initial focus inside the trapped region, and release_focus_trap for imperative release (e.g. clicking Cancel rather than pressing the exit key).

pub const form_change: Event(List(#(String, String)))

input on a form, payload is the current FormData as a list of name/value pairs. The uncontrolled-form counterpart of input. Pair with on_decoded() to skip dispatch on validation failure.

pub const form_submit: Event(List(#(String, String)))

submit on a form, payload is the submitted FormData as a list of name/value pairs. Prevents the browser’s default form submission and resets the form after the handler runs. Pair with on_decoded() to skip dispatch on validation failure.

pub const input: Event(String)

input event, fires immediately when an input value changes. Payload is the current value. For delayed updates use change.

pub const key_down: Event(KeyEvent)

keydown event, fires when a key is pressed.

pub const key_up: Event(KeyEvent)

keyup event, fires when a key is released.

pub const mouse_down: Event(#(Int, Int, ElementData))

mousedown event, fires when a mouse button is pressed. Payload is (x, y, element).

pub const mouse_enter: Event(ElementData)

mouseenter event, fires when the mouse enters an element’s boundary. Mapped to mouseover with a relatedTarget guard so delegation works.

pub const mouse_leave: Event(ElementData)

mouseleave event, fires when the mouse leaves an element’s boundary. Mapped to mouseout with a relatedTarget guard so delegation works.

pub const mouse_move: Event(#(Int, Int))

mousemove event, fires repeatedly while the mouse moves. Consider pairing with throttle_milliseconds.

pub const mouse_up: Event(#(Int, Int, ElementData))

mouseup event, fires when a mouse button is released.

pub fn on(
  runtime: client.Runtime(model, message),
  event event: Event(payload),
  selector selector: String,
  handler handler: fn(payload) -> message,
) -> client.Runtime(model, message)

Bind an event handler that always dispatches a message. The event argument fixes the payload type, so the handler’s signature is checked at compile time.

runtime
|> event.on(event: event.input, selector: "#search", handler: Search)
|> event.on(
  event: event.mouse_down,
  selector: ".card",
  handler: fn(payload) {
    let #(x, y, element) = payload
    Pressed(x, y, element)
  },
)
pub fn on_decoded(
  runtime: client.Runtime(model, message),
  event event: Event(payload),
  selector selector: String,
  decoder decoder: fn(payload) -> Result(message, Nil),
) -> client.Runtime(model, message)

Bind an event handler whose decoder may decline to dispatch by returning Error(Nil). Useful for click (the data-msg attribute may not match a known message) and form events (validation failure should skip dispatching).

runtime
|> event.on_decoded(
  event: event.click,
  selector: "#app",
  decoder: parse_click,
)
|> event.on_decoded(
  event: event.form_submit,
  selector: "#todo-form",
  decoder: submit_todo,
)
pub fn on_decoded_with_options(
  runtime: client.Runtime(model, message),
  event event: Event(payload),
  selector selector: String,
  options options: EventOptions,
  decoder decoder: fn(payload) -> Result(message, Nil),
) -> client.Runtime(model, message)

Like on_decoded with an extra EventOptions parameter. See options().

pub fn on_with_options(
  runtime: client.Runtime(model, message),
  event event: Event(payload),
  selector selector: String,
  options options: EventOptions,
  handler handler: fn(payload) -> message,
) -> client.Runtime(model, message)

Like on with an extra EventOptions parameter. See options().

runtime
|> event.on_with_options(
  event: event.input,
  selector: "#search",
  options: event.options() |> event.debounce_milliseconds(200),
  handler: Search,
)
pub fn once(options: EventOptions) -> EventOptions

Set the handler to fire only the first time. After that, all matching events are ignored.

pub fn options() -> EventOptions

Build an EventOptions with all modifiers off: no debounce, no throttle, fires every time, does not stop propagation or prevent default. Compose with the builder functions to enable modifiers.

pub const paste: Event(Nil)

paste event, fires when text is pasted from the clipboard.

pub const pointer_down: Event(#(Int, Int))

pointerdown event, unifies mouse, touch, and pen.

pub const pointer_move: Event(#(Int, Int))

pointermove event, unifies mouse, touch, and pen.

pub const pointer_up: Event(#(Int, Int))

pointerup event, unifies mouse, touch, and pen.

pub fn prevent_default(options: EventOptions) -> EventOptions

Set event.preventDefault() to fire on every matching event, regardless of debounce or throttle. Use to suppress browser defaults (e.g. drop-target behaviour, native form submission).

pub fn release_focus_trap(
  runtime: client.Runtime(model, message),
) -> Nil

Release the active focus trap, if any. No-op when no trap is active. Does not dispatch the trap’s on_exit message, call this when the caller is already running its own close logic and just needs the trap unhooked (e.g. a click on a Cancel button that dispatches CloseDialog and restores focus separately).

pub const resize: Event(Nil)

resize event, fires when an element (or window) resizes. Typically used with selector: "window".

pub const scroll: Event(#(Int, Int))

scroll event, payload is (scroll_top, scroll_left). Consider pairing with throttle_milliseconds.

pub fn stop_propagation(options: EventOptions) -> EventOptions

Set event.stopPropagation() to fire before the inner handler. Useful for delegated events that should not bubble further up.

pub const submit: Event(Nil)

submit event, fires when a form is submitted, with no payload. Prevents the browser’s default form submission. Use for controlled forms (input state already in the model). For uncontrolled forms, use form_submit.

pub fn throttle_milliseconds(
  options: EventOptions,
  value: Int,
) -> EventOptions

Set the throttle interval in milliseconds. Events fire at most once per interval; subsequent events within the window are dropped.

pub const touch_end: Event(ElementData)

touchend event, fires when all touches are removed.

pub const touch_move: Event(#(Int, Int))

touchmove event, fires repeatedly while a touch point moves. Consider pairing with throttle_milliseconds.

pub const touch_start: Event(#(Int, Int))

touchstart event, fires when a touch point is placed.

pub const wheel: Event(#(Float, Float))

wheel event, payload is (delta_x, delta_y). Useful for scroll hijacking and zoom controls.

Search Document