Emerge.UI.Event (Emerge v0.1.0)

Copy Markdown View Source

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()}
end

If 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()}
end

Pointer 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: 350
  • repeat_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.

t()

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

blur_attr()

@type blur_attr() :: {:on_blur, payload()}

change_attr()

@type change_attr() :: {:on_change, payload()}

click_attr()

@type click_attr() :: {:on_click, payload()}

focus_attr()

@type focus_attr() :: {:on_focus, payload()}

key_binding()

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

key_binding_descriptor()

@type key_binding_descriptor() :: %{
  key: key_name(),
  mods: [key_modifier()],
  match: key_match_mode(),
  route: binary()
}

Public descriptor form of a normalized keyboard binding.

key_down_attr()

@type key_down_attr() :: {:on_key_down, key_binding()}

key_match_mode()

@type key_match_mode() :: :exact | :all

How modifier matching is interpreted for keyboard listeners.

  • :exact requires the active modifiers to match exactly.
  • :all requires the listed modifiers to be present, but allows extras.

key_matcher()

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

key_modifier()

@type key_modifier() :: :shift | :ctrl | :alt | :meta

Modifier keys accepted by keyboard matchers.

key_name()

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

key_press_attr()

@type key_press_attr() :: {:on_key_press, key_binding()}

key_up_attr()

@type key_up_attr() :: {:on_key_up, key_binding()}

mouse_down_attr()

@type mouse_down_attr() :: {:on_mouse_down, payload()}

mouse_enter_attr()

@type mouse_enter_attr() :: {:on_mouse_enter, payload()}

mouse_leave_attr()

@type mouse_leave_attr() :: {:on_mouse_leave, payload()}

mouse_move_attr()

@type mouse_move_attr() :: {:on_mouse_move, payload()}

mouse_up_attr()

@type mouse_up_attr() :: {:on_mouse_up, payload()}

payload()

@type payload() :: {pid(), term()}

Destination process and message sent when the event fires.

Most helpers also accept a bare message, which is shorthand for {self(), message}.

press_attr()

@type press_attr() :: {:on_press, payload()}

swipe_down_attr()

@type swipe_down_attr() :: {:on_swipe_down, payload()}

swipe_left_attr()

@type swipe_left_attr() :: {:on_swipe_left, payload()}

swipe_right_attr()

@type swipe_right_attr() :: {:on_swipe_right, payload()}

swipe_up_attr()

@type swipe_up_attr() :: {:on_swipe_up, payload()}

t()

virtual_key_attr()

@type virtual_key_attr() :: {:virtual_key, virtual_key_spec()}

virtual_key_descriptor()

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

virtual_key_hold()

@type virtual_key_hold() :: nil | :repeat | {:event, payload()}

Optional hold behavior for virtual_key/1.

virtual_key_spec()

@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

virtual_key_tap()

@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

on_blur(payload)

@spec on_blur(payload() | term()) :: blur_attr()

Register a blur payload for a focusable element.

on_change(payload)

@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

on_click(payload)

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

on_focus(payload)

@spec on_focus(payload() | term()) :: focus_attr()

Register a focus payload for a focusable element.

on_key_down(matcher, payload)

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

on_key_press(matcher, payload)

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

on_key_up(matcher, payload)

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

on_mouse_down(payload)

@spec on_mouse_down(payload() | term()) :: mouse_down_attr()

Register a mouse-down payload for this element. Left mouse button only.

on_mouse_enter(payload)

@spec on_mouse_enter(payload() | term()) :: mouse_enter_attr()

Register a mouse-enter payload for this element. Delivered without cursor coordinates.

on_mouse_leave(payload)

@spec on_mouse_leave(payload() | term()) :: mouse_leave_attr()

Register a mouse-leave payload for this element. Delivered without cursor coordinates.

on_mouse_move(payload)

@spec on_mouse_move(payload() | term()) :: mouse_move_attr()

Register a mouse-move payload for this element. Delivered without cursor coordinates.

on_mouse_up(payload)

@spec on_mouse_up(payload() | term()) :: mouse_up_attr()

Register a mouse-up payload for this element. Left mouse button only.

on_press(payload)

@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")
)

on_swipe_down(payload)

@spec on_swipe_down(payload() | term()) :: swipe_down_attr()

Register a swipe-down payload for this element. Fires when the gesture resolves downward.

on_swipe_left(payload)

@spec on_swipe_left(payload() | term()) :: swipe_left_attr()

Register a swipe-left payload for this element. Fires when the gesture resolves leftward.

on_swipe_right(payload)

@spec on_swipe_right(payload() | term()) :: swipe_right_attr()

Register a swipe-right payload for this element. Fires when the gesture resolves rightward.

on_swipe_up(payload)

@spec on_swipe_up(payload() | term()) :: swipe_up_attr()

Register a swipe-up payload for this element. Fires when the gesture resolves upward.

virtual_key(spec)

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