Packages
mob
0.9.21
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.39
0.7.38
0.7.37
0.7.36
0.7.35
0.7.34
0.7.33
0.7.32
0.7.31
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.2
0.6.1
0.6.0
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.11
0.5.10
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
BEAM-on-device mobile framework for Elixir
Current section
Files
Jump to
Current section
Files
guides/event_audit.md
# Event System Audit (Batch 2)
Snapshot of the event-emission surface as of Batch 1 completion. Pairs with
`event_model.md` (the design) — this is "what's there now and how it maps."
## Event props recognised by the renderer
`lib/mob/renderer.ex` translates these node prop keys into native handle
registrations. Each is opt-in per widget; absence means no event delivery.
### Tap-family (existing)
| Prop | Form accepted | Native message | Notes |
|------|---------------|----------------|-------|
| `on_tap` | `pid \| {pid, tag}` | `{:tap, tag}` | `tag` defaults to `:ok` for bare pid; when atom, also emits `accessibility_id` for test tooling |
| `on_change` | `{pid, tag}` | `{:change, tag, value}` | text_field (binary), toggle (boolean atom), slider (float) |
| `on_focus` | `{pid, tag}` | `{:focus, tag}` | text_field |
| `on_blur` | `{pid, tag}` | `{:blur, tag}` | text_field |
| `on_submit` | `{pid, tag}` | `{:submit, tag}` | text_field return key |
| `on_end_reached` | `{pid, tag}` | `{:tap, tag}` | `:lazy_list` only; pagination trigger when the last row appears |
| `on_tab_select` | `{pid, tag}` | `{:tab_select, tag, tab_id}` | tab bars |
### New (Batch 3 / Batch 4 added in this pass)
| Prop | Form accepted | Native message | Status |
|------|---------------|----------------|--------|
| `on_select` | `{pid, tag}` | `{:select, tag}` | Reserved for a future native selection primitive; no core picker/menu currently emits it. `Mob.List` routes indexed selection through row taps instead. |
| `on_long_press` | `{pid, tag}` | `{:long_press, tag}` | Renderer + iOS NIF + native gesture; Android wired |
| `on_double_tap` | `{pid, tag}` | `{:double_tap, tag}` | Same |
| `on_press_in` | `{pid, tag}` | `{:press_in, tag}` | Finger down on the node. Observes without consuming; any node type. iOS + Android (bridge from `mob_new` 0.6.5+) (MOB-380) |
| `on_press_out` | `{pid, tag}` | `{:press_out, tag}` | That finger lifted or its touch was cancelled; on a node declaring both, always follows its `press_in`. Routing is snapshotted at touch-down so a re-render can't drop it (MOB-380) |
| `on_swipe` | `{pid, tag}` | `{:swipe, tag, direction}` | Direction is `:left \| :right \| :up \| :down` |
| `on_swipe_left` | `{pid, tag}` | `{:swipe_left, tag}` | Specific direction only |
| `on_swipe_right` | `{pid, tag}` | `{:swipe_right, tag}` | |
| `on_swipe_up` | `{pid, tag}` | `{:swipe_up, tag}` | |
| `on_swipe_down` | `{pid, tag}` | `{:swipe_down, tag}` | |
## Existing native event paths
### iOS (`mob_nif.m` → `MobRootView.swift`)
- Tap-family: SwiftUI `.onTapGesture { closure() }` calls `mob_send_*` from
the closure stored on `MobNode`. Closure was wired by the JSON deserialiser
using the integer handle from `register_tap`.
- Audio session and lifecycle observers: `[NSNotificationCenter defaultCenter]`
with blocks that `enif_send` to a registered dispatcher pid.
- Gestures (new in this pass): `extension View { func mobGestures(_:) }` adds
`.onLongPressGesture`, `.onTapGesture(count: 2)`, and a `DragGesture`
(only when at least one swipe handler is set, to avoid interfering with
ScrollView).
### Android (`android/jni/mob_nif.c` + `MobBridge.kt`)
- Tap-family: same handle-lookup pattern via `mob_send_*` C functions called
from JNI stubs (`Java_..._MobBridge_nativeSendTap`).
- Gestures (new in this pass): C senders declared and exported via
`mob_beam.h`. JNI stubs in `beam_jni.c` and Compose `Modifier` setup in the
generated app's `MobBridge` are still pending — see "Pending native work."
## Bridge to canonical envelope
Today, screens receive legacy shapes (`{:tap, tag}`, `{:change, tag, value}`).
`Mob.Event.Bridge` converts these to the canonical
`{:mob_event, %Address{}, event, payload}` envelope on demand. Screens can
opt in incrementally:
```elixir
def handle_info(msg, socket) do
case Mob.Event.Bridge.legacy_to_canonical(msg, __MODULE__) do
{:ok, envelope} -> handle_canonical(envelope, socket)
:passthrough -> # not a recognised legacy shape — handle directly
end
end
```
When all native emitters are migrated to the canonical envelope, this bridge
can be removed.
## Pending native work
### iOS
- ✅ NIF entry points (`mob_send_long_press`, `mob_send_double_tap`,
`mob_send_swipe_*`)
- ✅ MobNode property declarations (`onLongPress`, `onDoubleTap`,
`onSwipe`, `onSwipeLeft`, `onSwipeRight`, `onSwipeUp`, `onSwipeDown`)
- ✅ Prop deserialiser wires them up
- ✅ SwiftUI `.mobGestures()` modifier applies them
- ⏳ Verify on physical device — gesture conflicts with scroll need real-world
testing
### Android
- ✅ C sender functions (`mob_send_long_press` etc.)
- ✅ Header export in `mob_beam.h`
- ⏳ JNI stubs in `beam_jni.c` (need entries calling the C functions)
- ⏳ Kotlin `MobBridge` JNI declarations
- ⏳ Compose `Modifier` setup — `pointerInput { detectTapGestures(...) }` for
long-press/double-tap, `detectDragGestures` for swipes, gated per-node by
the corresponding handle being non-null
## Migration path for `Mob.List`
Currently `Mob.List` is a render helper, not a stateful component. Each row
gets `on_tap: {screen_pid, {:list, list_id, :select, index}}`. `Mob.Screen.Server`
has hardcoded knowledge of this shape and re-emits as `{:select, list_id, index}`.
Under the new event model, this becomes a stateful component (planned, not in
this pass):
1. `Mob.List` becomes a `Mob.Event.StatefulComponent` (see future module)
2. Row taps target the list's pid, not the screen's
3. List handles row events internally (selection state, scroll position,
etc.) and emits semantic events upward to its parent
Until that lands, the bridge module handles the existing list-tap shape:
`{:tap, {:list, id, :select, index}}` →
`{:mob_event, addr(:list, id, instance: index), :select, nil}`.
## Tests
| Module | Coverage |
|--------|----------|
| `Mob.Event.Address` | 47 tests + 10 doctests (struct, validation, formatting, pattern matching) |
| `Mob.Event.Target` | 17 tests + 3 doctests (every target form + classification) |
| `Mob.Event` | 20 tests + 4 doctests (dispatch, emit, matchers, test helpers) |
| `Mob.Event.Bridge` | 19 tests + 4 doctests (each legacy shape + passthrough) |
| `Mob.Renderer` | 9 new tests (Batch 3 + Batch 4 prop registration) |
Total Mob.Event-related tests added in this pass: **125 + doctests**.