hypereffect

Actions

Event attributes such as onPress take a handler, which can change the signals locally, send an action to the server, or both.

Handlers

A handler is an object with at least one of:

When the handler runs, the renderer:

  1. stops if the element, or one of its ancestors, is disabled or hidden;
  2. applies the patch, if any;
  3. stops if there is no action, or the connection is closed;
  4. sets the indicator to true, if any;
  5. sends an action message with the name, the payload if there is one, and the signals, without local ones.

So when both are present, the patch is applied first and the action carries the patched signals. Handlers are structural: they cannot be expressions. The exact rules are in the specification.

Events

Attribute Components Runs when
onPress button The button is pressed.
onChange input An edit is finished: on losing focus, or after a short idle period. Not on every keystroke.
checkbox, select, tabs After each change.
slider The slider is released.
onSubmit input The user confirms, with Enter in a single-line field.
onDismiss dialog The user asks to close the dialog.

For controls with bind, the new value is already in the signals when the handler runs.

Examples

In these examples, lines from the renderer are marked in comments; everything else is from the server.

An action

A button that sends an action with a payload. The signals hold a filter and a local tab selection.

signal {"filter":"all","_tab":"inbox"}
element {"id":"archive-7","type":"button","label":"Archive",
         "onPress":{"action":"archive","payload":{"contact":7}}}

When the user presses it, the renderer sends the action with the signals. _tab is local, so it is left out:

# renderer → server
action {"name":"archive","payload":{"contact":7},"signals":{"filter":"all"}}

The server answers however it likes, here by removing the row and updating a count:

remove {"id":"row-7"}
signal {"inbox":12}

An action with bound input

An input writes to the signals through bind, so the server reads what was typed from the action's signals, with no payload needed.

element {"id":"q","type":"input","bind":"search.q","placeholder":"Search",
         "onSubmit":{"action":"search"}}

The user types lamp and presses Enter:

# renderer → server
action {"name":"search","signals":{"search":{"q":"lamp"}}}

A local patch

A handler with only a patch changes the signals on the renderer and sends nothing. Here a button toggles a local signal that shows a section:

signal {"_more":false}
element {"id":"toggle","type":"button","variant":"plain",
         "label":{"cel":"s._more ? 'Less' : 'More'"},
         "onPress":{"patch":{"_more":{"cel":"!s._more"}}}}
element {"id":"details","type":"text","text":"Lorem ipsum","visible":{"cel":"s._more"}}

Pressing the button sets _more to true: the label becomes Less and the text appears. No message is sent.

Patch and action

The patch is applied before the action is sent, so the action carries the patched signals. Here the buttons of a filter set the filter and ask the server for the matching items:

signal {"filter":"all"}
element {"id":"done","type":"button","label":"Done",
         "variant":{"cel":"s.filter == 'done' ? 'primary' : 'secondary'"},
         "onPress":{"patch":{"filter":"done"},"action":"filter"}}

The button is highlighted at once, from the patch, and the server receives the new filter:

# renderer → server
action {"name":"filter","signals":{"filter":"done"}}

An indicator

A confirmation dialog whose Delete button closes the dialog locally, marks the deletion as in progress, and sends the action.

signal {"_confirm":true,"_deleting":false}
element {"id":"confirm","type":"dialog","title":"Delete project?",
         "open":{"cel":"s._confirm"},"onDismiss":{"patch":{"_confirm":false}}}
element {"id":"delete","type":"button","parent":"confirm","variant":"failure","label":"Delete",
         "onPress":{"patch":{"_confirm":false},"action":"delete","payload":{"project":3},
                    "indicator":"_deleting"}}
element {"id":"status","type":"progress","label":"Deleting…","visible":{"cel":"s._deleting"}}

When the user presses Delete, the renderer applies the patch (the dialog closes), sets _deleting to true (the progress appears), and sends:

# renderer → server
action {"name":"delete","payload":{"project":3},"signals":{}}

The renderer never clears an indicator. The server clears it when it is done, usually together with the result, and should do so on every path, including errors:

signal {"_deleting":false}
remove {"id":"project-3"}