Event handler helpers for interactive elements.
Event helpers attach process messages to elements. When the event fires, Emerge sends the stored message to the target process.
All helpers accept either:
- a bare
message - a
{pid, message}tuple
Passing only a message is shorthand for {self(), message}. Inside a
viewport render/0 or render/1 callback, that means the viewport process, so the
message arrives in handle_info/2.
Payload Routing
Some element events include a payload. Today the main public example is
on_change/1 for text-input value changes.
When a payload-bearing event is delivered through a viewport, the payload is
wrapped into the stored message using the viewport's wrap_payload/3
callback. The default behavior is:
- non-tuple message:
{message, payload} - tuple message: append the payload to the tuple
For example:
This field sends the new value back to the viewport whenever the text changes.
def render(state) do
Input.text(
[key(:search), Event.on_change(:search_changed)],
state.query
)
end
def handle_info({:search_changed, value}, state) do
{:noreply, %{state | query: value} |> Viewport.rerender()}
endIf you want more structure in the delivered message, store a tuple message:
This is useful when several fields share the same handle_info/2 path and you
want the message to identify which field changed.
Input.text([Event.on_change({self(), {:search_changed, :field}})], state.query)
def handle_info({:search_changed, :field, value}, state) do
{:noreply, %{state | query: value} |> Viewport.rerender()}
endPointer Events
Prefer on_press/1 for normal button-like activation. It is the default for
actions such as save, submit, open, and delete.
Use on_click/1 when you specifically want pointer click behavior.
Swipe helpers emit once a pointer gesture resolves to that direction.
on_mouse_down/1 and on_mouse_up/1 are left-button only.
on_mouse_enter/1, on_mouse_leave/1, and on_mouse_move/1 do not include
cursor coordinates in the delivered message.
Events do not bubble to parent elements. Attach the handler to the element that should react.
If you only want visual hover, focus, or pressed styling, use
Emerge.UI.Interactive instead of event handlers.
Input And Focus
on_change/1 is intended for text inputs.
Text editing still works without on_change/1; the handler only controls
whether a change message is emitted.
on_focus/1 and on_blur/1 work on focusable elements, not only text inputs.
Keyboard Events
Focused elements can listen for keyboard input with:
Keyboard matchers can be written as either:
- a key atom such as
:enter - a keyword matcher such as
[key: :digit_1, mods: [:ctrl], match: :all]
Supported modifier atoms are :shift, :ctrl, :alt, and :meta.
Match modes are:
:exact- the modifier set must match exactly:all- the listed modifiers must be present, but extra modifiers are ok
on_key_press/2 is for completed key gestures. It fires on release after a
matching press.
You can attach multiple keyboard listeners to one element by listing
on_key_down/2, on_key_up/2, or on_key_press/2 more than once.
Virtual Keys
virtual_key/1 is for on-screen keyboard keys and similar soft-key controls.
A virtual key spec must include :tap, which can be one of:
{:text, binary}{:key, key, mods}{:text_and_key, text, key, mods}
Optional hold behavior is:
nil- no hold behavior:repeat- repeat the tap action while held{:event, payload}- emit a separate hold event
Defaults:
hold_ms: 350repeat_ms: 40
virtual_key/1 cannot be combined with on_click/1 or on_press/1 on the
same element.
Examples
In this example, the first button sends :save, the second reacts to focused
Enter, and the third acts like a soft keyboard key.
def render(_state) do
column([spacing(16)], [
Input.button(
[
Event.on_press(:save),
Background.color(color(:sky, 500)),
Border.rounded(8),
Font.color(color(:white)),
padding(12)
],
text("Save")
),
Input.button(
[Event.on_key_down(:enter, :submit)],
text("Submit")
),
Input.button(
[Event.virtual_key(tap: {:text_and_key, "A", :a, [:shift]})],
text("A")
)
])
end
Summary
Types
Normalized keyboard binding stored on an element.
Public descriptor form of a normalized keyboard binding.
How modifier matching is interpreted for keyboard listeners.
Keyboard matcher accepted by on_key_down/2, on_key_up/2, and
on_key_press/2.
Modifier keys accepted by keyboard matchers.
Canonical key atoms accepted by keyboard event helpers.
Destination process and message sent when the event fires.
Normalized descriptor form of a virtual key spec.
Optional hold behavior for virtual_key/1.
User-facing virtual key spec.
Tap behavior for virtual_key/1.
Functions
Register a blur payload for a focusable element.
Register a value-change payload for a text input.
Register a pointer click payload for this element.
Register a focus payload for a focusable element.
Register a focused key-down payload for this element.
Register a focused completed key-press payload for this element.
Register a focused key-up payload for this element.
Register a mouse-down payload for this element. Left mouse button only.
Register a mouse-enter payload for this element. Delivered without cursor coordinates.
Register a mouse-leave payload for this element. Delivered without cursor coordinates.
Register a mouse-move payload for this element. Delivered without cursor coordinates.
Register a mouse-up payload for this element. Left mouse button only.
Register a press payload for this element.
Register a swipe-down payload for this element. Fires when the gesture resolves downward.
Register a swipe-left payload for this element. Fires when the gesture resolves leftward.
Register a swipe-right payload for this element. Fires when the gesture resolves rightward.
Register a swipe-up payload for this element. Fires when the gesture resolves upward.
Register virtual-key behavior for an element.
Types
@type blur_attr() :: {:on_blur, payload()}
@type change_attr() :: {:on_change, payload()}
@type click_attr() :: {:on_click, payload()}
@type focus_attr() :: {:on_focus, payload()}
@type key_binding() :: %{ key: key_name(), mods: [key_modifier()], match: key_match_mode(), payload: payload(), route: binary() }
Normalized keyboard binding stored on an element.
route is derived automatically from the key, modifiers, and match mode.
@type key_binding_descriptor() :: %{ key: key_name(), mods: [key_modifier()], match: key_match_mode(), route: binary() }
Public descriptor form of a normalized keyboard binding.
@type key_down_attr() :: {:on_key_down, key_binding()}
@type key_match_mode() :: :exact | :all
How modifier matching is interpreted for keyboard listeners.
:exactrequires the active modifiers to match exactly.:allrequires the listed modifiers to be present, but allows extras.
@type key_matcher() :: key_name() | [key: key_name(), mods: [key_modifier()], match: key_match_mode()]
Keyboard matcher accepted by on_key_down/2, on_key_up/2, and
on_key_press/2.
Use either a key atom like :enter or a keyword matcher like
[key: :digit_1, mods: [:ctrl], match: :all].
@type key_modifier() :: :shift | :ctrl | :alt | :meta
Modifier keys accepted by keyboard matchers.
@type key_name() ::
:a
| :b
| :c
| :d
| :e
| :f
| :g
| :h
| :i
| :j
| :k
| :l
| :m
| :n
| :o
| :p
| :q
| :r
| :s
| :t
| :u
| :v
| :w
| :x
| :y
| :z
| :digit_0
| :digit_1
| :digit_2
| :digit_3
| :digit_4
| :digit_5
| :digit_6
| :digit_7
| :digit_8
| :digit_9
| :minus
| :equal
| :plus
| :asterisk
| :left_bracket
| :right_bracket
| :backslash
| :semicolon
| :apostrophe
| :grave
| :comma
| :period
| :slash
| :space
| :enter
| :tab
| :escape
| :backspace
| :insert
| :delete
| :home
| :end
| :page_up
| :page_down
| :arrow_left
| :arrow_right
| :arrow_up
| :arrow_down
| :shift
| :control
| :alt
| :alt_graph
| :super
| :caps_lock
| :num_lock
| :scroll_lock
| :print_screen
| :pause
| :context_menu
| :f1
| :f2
| :f3
| :f4
| :f5
| :f6
| :f7
| :f8
| :f9
| :f10
| :f11
| :f12
| :f13
| :f14
| :f15
| :f16
| :f17
| :f18
| :f19
| :f20
| :f21
| :f22
| :f23
| :f24
| :unknown
Canonical key atoms accepted by keyboard event helpers.
@type key_press_attr() :: {:on_key_press, key_binding()}
@type key_up_attr() :: {:on_key_up, key_binding()}
@type mouse_down_attr() :: {:on_mouse_down, payload()}
@type mouse_enter_attr() :: {:on_mouse_enter, payload()}
@type mouse_leave_attr() :: {:on_mouse_leave, payload()}
@type mouse_move_attr() :: {:on_mouse_move, payload()}
@type mouse_up_attr() :: {:on_mouse_up, payload()}
Destination process and message sent when the event fires.
Most helpers also accept a bare message, which is shorthand for
{self(), message}.
@type press_attr() :: {:on_press, payload()}
@type swipe_down_attr() :: {:on_swipe_down, payload()}
@type swipe_left_attr() :: {:on_swipe_left, payload()}
@type swipe_right_attr() :: {:on_swipe_right, payload()}
@type swipe_up_attr() :: {:on_swipe_up, payload()}
@type t() :: click_attr() | press_attr() | swipe_up_attr() | swipe_down_attr() | swipe_left_attr() | swipe_right_attr() | mouse_down_attr() | mouse_up_attr() | mouse_enter_attr() | mouse_leave_attr() | mouse_move_attr() | change_attr() | focus_attr() | blur_attr() | key_down_attr() | key_up_attr() | key_press_attr() | virtual_key_attr()
@type virtual_key_attr() :: {:virtual_key, virtual_key_spec()}
@type virtual_key_descriptor() :: %{ tap: virtual_key_tap(), hold: :none | :repeat | :event, hold_ms: non_neg_integer(), repeat_ms: pos_integer() }
Normalized descriptor form of a virtual key spec.
@type virtual_key_hold() :: nil | :repeat | {:event, payload()}
Optional hold behavior for virtual_key/1.
@type virtual_key_spec() :: %{ :tap => virtual_key_tap(), optional(:hold) => virtual_key_hold(), optional(:hold_ms) => non_neg_integer(), optional(:repeat_ms) => pos_integer() }
User-facing virtual key spec.
Required key:
:tap
Optional keys:
:hold:hold_ms:repeat_ms
@type virtual_key_tap() :: {:text, binary()} | {:key, key_name(), [key_modifier()]} | {:text_and_key, binary(), key_name(), [key_modifier()]}
Tap behavior for virtual_key/1.
Functions
Register a blur payload for a focusable element.
@spec on_change(payload() | term()) :: change_attr()
Register a value-change payload for a text input.
Use on_change/1 with Emerge.UI.Input.text/2 or
Emerge.UI.Input.multiline/2.
Text editing still works without this handler; on_change/1 only controls
whether a message is emitted when the value changes.
When delivered through a viewport, the changed value is wrapped into the
stored message using wrap_payload/3. Multiline values use the same payload
shape; newline characters remain part of the emitted binary.
Examples
This example keeps the input value in viewport state and updates that state whenever the field changes.
def render(state) do
Input.text(
[
key(:search),
Event.on_change(:search_changed)
],
state.query
)
end
def handle_info({:search_changed, value}, state) do
{:noreply, %{state | query: value} |> Viewport.rerender()}
end
@spec on_click(payload() | term()) :: click_attr()
Register a pointer click payload for this element.
Use this when you specifically want click behavior from the pointer.
For normal button-like activation, prefer on_press/1.
@spec on_focus(payload() | term()) :: focus_attr()
Register a focus payload for a focusable element.
@spec on_key_down(key_matcher(), payload() | term()) :: key_down_attr()
Register a focused key-down payload for this element.
matcher can be a key atom like :enter or a keyword matcher such as
[key: :digit_1, mods: [:ctrl], match: :all].
:match defaults to :exact.
Example
This lets one focused button respond both to Ctrl+1 and to plain Enter.
Input.button(
[
Event.on_key_down([key: :digit_1, mods: [:ctrl], match: :all], :select_tab_1),
Event.on_key_down(:enter, :submit)
],
text("Save")
)On focused Emerge.UI.Input.text/2 and Emerge.UI.Input.multiline/2
elements, a matching on_key_down/2 suppresses the input's default keydown
behavior for that keydown. This lets apps override built-in editing such as
character insertion or multiline Enter handling.
For example, on_key_down(:enter, :submit) on a focused
Emerge.UI.Input.multiline/2 intercepts Enter before the default newline is
inserted.
@spec on_key_press(key_matcher(), payload() | term()) :: key_press_attr()
Register a focused completed key-press payload for this element.
on_key_press/2 is for completed key gestures and fires on release after a
matching press.
@spec on_key_up(key_matcher(), payload() | term()) :: key_up_attr()
Register a focused key-up payload for this element.
Key-up handlers use the same matcher forms as on_key_down/2.
@spec on_mouse_down(payload() | term()) :: mouse_down_attr()
Register a mouse-down payload for this element. Left mouse button only.
@spec on_mouse_enter(payload() | term()) :: mouse_enter_attr()
Register a mouse-enter payload for this element. Delivered without cursor coordinates.
@spec on_mouse_leave(payload() | term()) :: mouse_leave_attr()
Register a mouse-leave payload for this element. Delivered without cursor coordinates.
@spec on_mouse_move(payload() | term()) :: mouse_move_attr()
Register a mouse-move payload for this element. Delivered without cursor coordinates.
@spec on_mouse_up(payload() | term()) :: mouse_up_attr()
Register a mouse-up payload for this element. Left mouse button only.
@spec on_press(payload() | term()) :: press_attr()
Register a press payload for this element.
This is the recommended default for buttons and other action controls.
For standard activation, prefer on_press/1 over on_click/1 because it
also works with focused Enter.
Examples
This is a conventional action button: pressing it sends :save to the target
process while the surrounding attrs provide the visual styling.
Input.button(
[
Event.on_press(:save),
Background.color(color(:sky, 500)),
Border.rounded(8),
Font.color(color(:white)),
padding(12)
],
text("Save")
)
@spec on_swipe_down(payload() | term()) :: swipe_down_attr()
Register a swipe-down payload for this element. Fires when the gesture resolves downward.
@spec on_swipe_left(payload() | term()) :: swipe_left_attr()
Register a swipe-left payload for this element. Fires when the gesture resolves leftward.
@spec on_swipe_right(payload() | term()) :: swipe_right_attr()
Register a swipe-right payload for this element. Fires when the gesture resolves rightward.
@spec on_swipe_up(payload() | term()) :: swipe_up_attr()
Register a swipe-up payload for this element. Fires when the gesture resolves upward.
@spec virtual_key(virtual_key_spec() | keyword()) :: virtual_key_attr()
Register virtual-key behavior for an element.
This is useful for on-screen keyboards and similar soft-key controls.
The spec must include :tap. Optional keys are :hold, :hold_ms, and
:repeat_ms.
virtual_key/1 cannot be combined with on_click/1 or on_press/1 on the
same element.
Example
This virtual key inserts an uppercase A on tap and sends a separate
:show_alternates event when the key is held.
Input.button(
[
width(px(56)),
height(px(56)),
Event.virtual_key(
tap: {:text_and_key, "A", :a, [:shift]},
hold: {:event, {self(), :show_alternates}}
)
],
text("A")
){:text_and_key, text, key, mods} participates in the same text-input
keydown suppression rules as a physical key press. {:text, text} does not,
because it inserts text without a preceding keydown. See on_key_down/2 for
the suppression behavior on focused Input.text/2 and Input.multiline/2.