Events
Events map named triggers to action arrays. They are declared in the events object of any component or module.
"events": {"onTap": [ { "state": { ... } } ],"onAppear": [ { "repo": { ... } }, { "state": { ... } } ]}9.1 Supported Events
Section titled “9.1 Supported Events”| Event | Schema | Trigger |
|---|---|---|
| onTap | array | User taps the component. |
| onDoubleTap | array | User double-taps the component. Coexists with onTap; the runtime adds a short disambiguation delay so the single-tap chain only fires when the tap is not part of a double-tap. |
| onAppear | array | Component transitions into the visible UI tree — see §9.1.1 for precise lifecycle semantics. |
| onDisappear | array | Component transitions out of the visible UI tree. |
| onRefresh | array | Pull-to-refresh gesture on a scroll or list container. |
| onSubmit | array | User submits a form or presses the keyboard submit key. |
| onLongPress | array | User performs a long-press gesture. |
| onRotate | array | Device orientation changes. |
| onFocus | array | Input component (textfield, texteditor) gains keyboard focus. |
| onBlur | array | Input component (textfield, texteditor) loses keyboard focus. |
| onChange | object | Observed state key changes value. Special schema — see §9.2. |
| onCustom | object | Bridge event triggered by the host app. Special schema — see §9.3. |
9.1.1 Lifecycle events: onAppear / onDisappear
Section titled “9.1.1 Lifecycle events: onAppear / onDisappear”The runtime fires these handlers only if the component explicitly declares them in its JSON. There is no implicit lifecycle behavior — if a tab module needs to refresh data from the repo whenever the user activates that tab, it MUST declare an onAppear action chain doing the refresh. Otherwise the tab simply shows whatever its state contains at that moment, regardless of what other parts of the app have written.
When onAppear fires
Section titled “When onAppear fires”onAppear fires every time the component transitions from not-rendered to rendered in the visible UI tree. Specifically:
- the component’s first mount (initial render);
- the user switching back to a tab whose content was previously visible (the tab’s child module’s
onAppearre-fires); - returning from a pushed module via the navigation back stack (the destination’s
onAppearre-fires on each return); - any conditional re-mount when an enclosing
_conditionflips from falsy to truthy.
onAppear does NOT fire merely because the component scrolls off-screen and back on — the component remains in the tree continuously. Use onAppear for view-lifecycle transitions, not for scroll visibility.
Canonical usage for tab- and navigation-driven flows
Section titled “Canonical usage for tab- and navigation-driven flows”For any view that displays data that can change while the view is hidden (the repo grows, root state updates from a sibling, etc.), put a refresh handler in that view’s module-level onAppear. Typical shape:
"events": { "onAppear": [ { "repo": { "id": "act_load", "input": { "operation": "read", "params": {...}, "repositoryId": "..." }, "output": { "success": [ { "state": { "id": "act_set_entries", "input": { "entries": "@{act_load.entries}" } } } ] } } } ]}This guarantees that every time the tab becomes visible — including switches and pop-backs — the view reads the latest data from the source of truth.
onDisappear fires correspondingly when the component is removed from the visible UI tree (tab switched away, navigated forward past this screen, condition flips to falsy). Use it sparingly — most refresh patterns live in onAppear.
9.2 onChange Schema
Section titled “9.2 onChange Schema”"events": {"onChange": {"observed": "query","actions": [{ "repo": { "id": "search_call", ... } }]}}| Field | Type | Required | Description |
|---|---|---|---|
| observed | string | REQUIRED | State key name to watch. The actions array fires each time this key’s value changes. |
| actions | array | REQUIRED | Action array in the same format as any other event. |
⚠ The plain array form
"onChange": [...]is not supported. Always use the object form{ "observed": "...", "actions": [...] }. Omittingobservedproduces a validation warning and the event will never fire.
ℹ Note: Debounce can be applied at the input component level (_debounce context key) or at the repo action level (debounce field). Combining both is the recommended pattern for search-as-you-type.
9.3 onCustom Schema
Section titled “9.3 onCustom Schema”"events": { "onCustom": { "name": "custom_event", "actions": [ { "state": { "id": "handle_custom", "input": { "value": "@{handle_custom.payloadKey}" } } } ] }}| Field | Type | Required | Description |
|---|---|---|---|
| name | string | REQUIRED | The event name the host app uses to trigger this handler. |
| actions | array | REQUIRED | Action array fired when the host triggers this event. |
⚠ The plain array form
"onCustom": [...]is not supported. Always use the object form{ "name": "...", "actions": [...] }. Omittingnameproduces a validation warning and the event will never be matched.
Accessing the trigger payload. When the host dispatches the event with a data payload (for example {"id": "d1", "on": true}), that payload is bound into each action’s context under the action’s id. Inside input, reference payload fields as @{<action.id>.<field>}:
"events": { "onCustom": { "name": "deviceToggled", "actions": [ { "state": { "id": "apply_toggle", "input": { "lastToggledId": "@{apply_toggle.id}", "lastToggledName": "@{apply_toggle.name}", "lastToggledOn": "@{apply_toggle.on}" } } } ] }}If the payload is a scalar (e.g. a string) rather than a dict, @{<action.id>} alone holds that value.
Tip for complex lookups. Prefer sending the full data you need in the trigger payload (e.g. include the display name alongside the id) rather than trying to look up the record inside the handler. Filter-predicate paths with @{…} refs do NOT work (see §6.2.1) — looking up by id requires a separate state-capture step first, which is tolerable but harder to read.