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. Bindings live on the Component
they relate to and are registered once at
component.mount().
Selectors are standard CSS, matched globally via document-level event
delegation. Locality is organisational, the framework does not scope
matches to the component the binding is attached to. Patterns like
on(event.click, selector: "#app") paired with data-msg attributes
keep working as you patch the DOM. Bindings declared inside
each and
each_live item bodies are not collected,
place them on the each/each_live wrapper or any static ancestor.
import lily/client
import lily/component
import lily/event
import lily/store
fn app(_model: Model) {
component.fragment([
component.simple(
slice: fn(m: Model) { m.search },
render: fn(value, _) { html.input([attribute.value(value)]) },
)
|> event.on(event: event.input, selector: "#search", handler: Search),
])
|> event.on_decoded(
event: event.click,
selector: "#app",
decoder: parse_click,
)
|> event.on(
event: event.key_down,
selector: "document",
handler: fn(ke) { KeyPressed(ke.key) },
)
}
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,
)
}
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().
component.simple(slice: ..., render: ...)
|> 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(
component: component.Component(model, message, html),
event event: Event(payload),
selector selector: String,
handler handler: fn(payload) -> message,
) -> component.Component(model, message, html)
Bind an event handler to a component. The handler always dispatches a
message; the event argument fixes the payload type so the handler
signature is checked at compile time. The binding is registered during
component.mount(), and a single registration
covers every DOM element matching selector via document-level
delegation.
Selectors are global CSS selectors, not scoped to the component the
binding is attached to. Locality is organisational, the framework does
not constrain which elements the listener matches. Bindings declared
inside each and
each_live item bodies are ignored,
attach them to the each/each_live wrapper or any static ancestor.
component.simple(slice: ..., render: ...)
|> 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(
component: component.Component(model, message, html),
event event: Event(payload),
selector selector: String,
decoder decoder: fn(payload) -> Result(message, Nil),
) -> component.Component(model, message, html)
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).
component.fragment([...])
|> 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(
component: component.Component(model, message, html),
event event: Event(payload),
selector selector: String,
options options: EventOptions,
decoder decoder: fn(payload) -> Result(message, Nil),
) -> component.Component(model, message, html)
Like on_decoded with an extra
EventOptions parameter. See options().
pub fn on_with_options(
component: component.Component(model, message, html),
event event: Event(payload),
selector selector: String,
options options: EventOptions,
handler handler: fn(payload) -> message,
) -> component.Component(model, message, html)
Like on with an extra EventOptions
parameter. See options().
component.simple(slice: ..., render: ...)
|> 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.