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 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 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 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 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.