Skip to main content
Version: 1.0.4

Event Actions

An event action is what happens when an operator interacts with a component. Configure them in the Event Manager: pick an event, add one or more actions, and they run in order.

Events

Available on every component

EventFires on
clickSingle click
dblclickDouble click
contextmenuRight click — see the note below
mouseenterPointer enters
mouseleavePointer leaves
contextmenu does not fire on the Designer canvas

Right-click is the Designer's own gesture: on the canvas it opens the Event Manager for the component under the cursor, and the event is consumed there. Your configured contextmenu actions run at runtime only — in preview and in LiveView.

Test right-click in preview. Configuring it, right-clicking the canvas, seeing the Event Manager and concluding it is broken is the usual first encounter with this.

Two further things worth knowing:

  • The browser's own menu still appears. The runtime fires your actions but does not suppress the native context menu. If you are building a custom right-click menu, suppress it yourself in a script action.
  • It applies to every component, in every layout mode — the behaviour lives in the shared widget wrapper, not in individual widgets.

Input components

EventFires on
changeThe value changed
submitSubmitted
focusReceived focus
blurLost focus

Table

EventPayload
rowClickThe clicked row
rowDoubleClickThe row
selectionChangeThe current selection
rowEditThe edited row
rowDeleteThe row to delete

Form

EventPayload
submitSuccessThe write result, under submit
submitErrorThe error

Upload

EventPayload
fileReceivedFile metadata, under file / files

Events on a part of a widget New in 1.0.0

Some widgets have addressable parts, and a part can carry its own events. Wiring "click this 3D viewer" is rarely what you meant; "click the robot's eye" is.

WidgetIts partsEvents they carry
3D ViewerObjects in the modelobjectClick · objectDoubleClick
TableColumnscellClick · cellDoubleClick

A wired part appears as an extra section in the Event Manager's list, below the widget's own events — not as a mode the dialog switches into, so opening it shows at a glance what the widget does and what each of its parts does. Everything else works unchanged: the action cards, chaining, and Only when… all behave on a part exactly as they do on the widget.

Adding one

Open the Event Manager and use the Add object… / Add column… field at the foot of the list.

A part is addressed by the same string a binding row targets — a 3D node name or index path, a Table column id — so a part never acquires a second name that could drift from the first.

The 3D Viewer publishes its node list, so the field offers them as suggestions. The Table does not publish a column list at runtime, so you type the column id — the same id used everywhere else in the Table's configuration. Typing is allowed either way: in the editor the model may not be loaded yet, and you may already know the name.

Removing a part removes every event on it.

Rows are deliberately not addressable

Only address spaces you can enumerate at design time get sections. A query can produce thousands of rows, so branching on a row stays a guard — rowClick with an Only when… on the row payload.

What a click resolves to

Both resolutions are worth knowing, because both fail silently when the address is wrong — the event simply never fires.

  • 3D Viewer. A click reports the mesh the ray actually hit, which for a multi-primitive node is a leaf like Eye_L_2 rather than the Eye_L you wired. The hit is walked up to the deepest ancestor that has events on it, so wiring the name you see in the model tree works.
  • Table. The column comes from the cell's own column marker, not from a handler threaded through every cell — which is what keeps rows from re-rendering on every sort.

Actions

Go to another view, optionally with parameters.

navigate → view: "equipment-detail"
params: { tagPath: row.path }

The foundation of drill-down: an overview passes the selected equipment's path to a single parameterised detail view.

setTag

Write a value to a tag.

setTag → tag: "Line1/Filler/Motor1/SpeedSetpoint"
value: Slider.value

Subject to the tag's writable flag and write level, enforced server-side. A modified client cannot bypass it.

Confirm consequential writes

A button that starts equipment should ask first. Pair setTag with a confirmation, and set a write level so only sufficiently privileged operators can reach it.

setClientVar

Set a client-side variable — screen-local state that no other session sees.

setClientVar → name: "selectedLine"
value: Dropdown.value

Use it for selections, filters and UI state. For anything shared across sessions, use an internal tag; for anything shared across pipelines, use a variable.

script

Run a client script — JavaScript in the browser, with the full system API.

const r = await system.tag.read("Line1/Filler/Motor1/Speed");
if (r.quality !== "Good") {
context.ui.notify({ message: "Speed unavailable", severity: "warning" });
return;
}
await system.tag.write("Line1/Filler/Motor1/SpeedSetpoint", r.value * 0.9);

For anything transactional or long-running, call a Gateway script instead — it runs server-side, so a browser disconnect cannot strand an open transaction. → Gateway scripts

notify

Show the operator a message.

notify → message: "Setpoint applied"
severity: info | success | warning | error

sendMessage

Send a message to other components. Any component with a matching "on message" handler receives it.

sendMessage → type: "refreshDashboard"
payload: { line: "L1" }

This is local, in-browser messaging. To push from the gateway down to browsers — from a pipeline or a Gateway script — use system.message.send, which reaches all sessions, one project, or one user.

refreshBinding

Re-evaluate a query binding on demand — a refresh button, or after a write that changes what a table should show.

panel / dock

Open, close or toggle a panel or dock region — detail drawers, side panels, popovers.

login / logout

Trigger the LiveView runtime login or logout flow, for projects bound to a User Database.

Chaining actions

Actions on one event run in order:

Button "Apply recipe" → click
1. script validate the entered values
2. setTag write the setpoint
3. notify "Recipe applied"
4. refreshBinding refresh the recipe table

Keep chains short. Once the logic outgrows a few steps, move it into a single script action — or a Gateway script — where it can be read as one piece.

Running an action conditionally

Every action carries an optional guard, edited from the Only when… chip in its header. It is a comparison, not an expression: a source on the left, one of is / is not / is set / is empty, and a value on the right.

Vertical Menu → Menu item click
1. Popover only when {{item.key}} is help
2. Sign out only when {{item.key}} is logout

This is what lets ONE handler serve several cases. A menu's item click is the clearest example — the event belongs to the whole widget, so without a guard every action would fire for every item — but the same applies to a table row, a chart point, or any chain that has to behave differently depending on a tag or a prior result.

The left side accepts anything a binding does: the event's own payload ({{item.key}}, {{row.status}}, {{submit.rows}}), a widget property (self.props.mode), a tag, a view param, a variable, or a chain result (ok, error.message, results.0.id). The picker offers exactly what applies to the event you are editing.

The right side is compared as typed unless you wrap it in {{ }}. That matters: error, ok, last, results and outcomes are also the names of chain results, and a plain error here means the word, not the previous action's error object.

Values are compared as text, so a tag holding 42 matches a typed 42. is set / is empty treat an empty string, false, 0, null and undefined as empty — so a bound boolean tag behaves the way you would expect rather than the way JavaScript would.

What a skipped action does to the chain

A guarded-out action is skipped completely: it records no result and raises no error, exactly as if it were not in the list for that particular firing.

Two consequences worth knowing:

  • A later action's On success / On error condition refers to the last action that actually ran, not necessarily the one directly above it.
  • A guarded-out notify does not count as "the author reported this", so the built-in failure message can still appear if an earlier setTag or script failed.

Patterns

Drill-down from a table

Table.rowClick → navigate("equipment-detail", { tagPath: row.path })

Guarded start

Button.click → setTag("Line1/Conv1/StartCmd", true)
with Confirm ticked: "Start conveyor 1?"

Confirmation is an option on the action, not something a script asks for. A confirmation a script raises could be skipped by a script, and the point of one is that it cannot be.

Filter that drives everything on the screen

Dropdown.change → setClientVar("line", value)
→ sendMessage("filterChanged", { line: value })

Server-side work from a button

Button.click → script:
const res = await system.script.runOnGateway("reports.generateShiftReport",
{ shift: "A" },
{ idempotencyKey: "shift-A-" + today });
context.ui.notify({ message: `Report ${res.id} generated`, severity: "success" });

The idempotency key means a double-click produces one report, not two.

Security

  • Every action a screen can perform is still subject to server-side authorization. Client-side logic decides what to offer; the server decides what is allowed.
  • Tag writes are gated by writable and the write level.
  • Runtime operators are authorised by their runtime roles, independently of platform permissions.
  • Consequential actions are recorded in the audit journal.

Design screens so operators do not see actions they cannot perform — but never rely on hiding a button as the control.

Next

Drawing & symbols