Joy DOM

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 onEvent listener. 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:

  • onEvent fires 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.

On this page