Events and actions
Wire onclick/onfocus/onblur/onchange to named handlers. Mutations change document state; events reach your onEvent listener.
A node's event property names a handler in the document's root handlers region (see §8 Actions, events, and state). A handler is one of three kinds:
- Mutation changes the document's own state.
- Event reaches your
onEventlistener. A reference whose name matches no handler counts as an event too. - Sequence runs several actions in order.
Mutations: the document handles it
A counter needs no host code. The document owns its state, and the mutation writes it:
import { } from "@joy-dom/react";
import type { } from "@joy-dom/core";
const : = {
: { : 0 },
: {
: { : "increment", : "/count" },
},
: { ".counter": { : "flex" } },
: {
: "div",
: {
: ["counter"],
: { : "event", : "bump" },
},
: ["Count: ", { : "/count" }],
},
};
export function () {
return < ={} />;
}Each click runs one interaction. The mutation applies, directives re-resolve, and the view updates. Templates and state covers where the state comes from.
Events: reaching the host
A reference whose name matches no handler signals your app directly. onEvent receives the event (name, resolved params, and the node that raised it) plus a handle to respond through:
import { , type } from "@joy-dom/react";
import type { } from "@joy-dom/core";
const : = {
: 1,
: { ".buy": { : "flex" } },
: [],
: {
: "div",
: {
: ["buy"],
: { : "event", : "checkout", : { : "joy-01" } },
},
: ["Buy now"],
},
};
const : = (, ) => {
if (. === "checkout") {
.("checkout", .., .());
}
};
export function () {
return < ={} ={} />;
}Three rules define the contract:
onEventfires after the interaction's state changes apply.handle.getState()always returns settled state.handle.dispatch(action)is how the host writes back. Any action works: a mutation, an event, a sequence.- Everything the host dispatches runs as a new interaction, queued after the current one. Interactions run one at a time, in order.
Sequences and the event payload
$event reads the payload the firing event carried. For onchange, that is the normalized { value } of the input. A reference's params merge into the payload before the handler runs:
"handlers": {
"saveDraft": {
"type": "sequence",
"actions": [
{ "type": "set", "path": "/draft/email", "to": { "$event": "/value" } },
{ "type": "event", "name": "draftChanged" }
]
}
}"onchange": { "type": "event", "name": "saveDraft" }The mutation applies first, then the event reaches the host, in the order the list declares.
Errors
A mutation that hits the wrong shape (incrementing a string, toggling an object) stops its interaction with a typed ActionError. Applied actions stay applied, and the render never crashes. Pass onError to observe them. Without it, they log to the console:
<Joy spec={template} onError={(error) => console.warn(error.identity, error.path)} />Custom components receive the same on* handlers when their node binds an action, and decide which element fires them: an inner <input>'s onChange, a button's onClick. The interactive widgets use case walks through a card that mixes both.