docs

Handling DOM Events

Respond to DOM events in C++ and dispatch custom events.

On this page

You can bind C++ callbacks directly to DOM events like clicks and key presses using similar syntax to JavaScript.

Listening for Events

Calling addEventListener() on an element works like JavaScript— the listener keeps firing until its page goes away. Your callback can accept a dom::Event or take no arguments at all:

C++
auto save = document.getElementById("save");
save.addEventListener("click", [] { SaveGame(); });

Reading the Event

You can inspect an event by taking a dom::Event by value in the callback.

A dom::Event is valid only during the callback that received it— you can't copy, move, or store it. Helper functions you pass it to should take const dom::Event&.

Handles retrieved from the event (eg, target()) are ordinary DOM handles, so you can keep them after the callback returns.

C++
document.addEventListener("keydown", [](dom::Event event) {
  if (event.AsKeyboard().key() == "Escape")
    CloseMenu();
});

Standard methods like type(), target(), preventDefault(), and stopPropagation() work like JavaScript (with timeStamp() reporting milliseconds using the page's clock).

Event Types

You can cast a generic dom::Event to a specific view to read type-specific fields. Calling a view on the wrong event type returns fallback values (so check type() first if one callback handles multiple events):

View Adds
AsMouse() Position, buttons, and modifier keys
AsWheel() Scroll deltas
AsKeyboard() key(), code(), and modifier keys
AsFocus() The element losing or gaining focus
AsInput() The inserted text
AsSubmit() The button that submitted the form
AsCustom() The detail string of a custom event

Listener Options

You can configure a listener by passing an options struct as the third argument (or flags like dom::Once | dom::Capture as shorthand).

Option Effect
capture Receive the event on its way down (capture phase)
once Remove the listener after its first event
passive Ignore preventDefault() (wheel listeners on the window, document, <html>, and <body> are passive by default)
signal Remove the listener when an AbortController aborts

Designated initializers let you specify only the options you need:

C++
save.addEventListener("click", [] { ShowTutorial(); }, { .once = true });

Removing Listeners

Because C++ can't compare lambdas, there's no removeEventListener() function taking a callback. Instead, you should store the dom::EventListener handle that addEventListener() returns and call Remove() on it later:

C++
dom::EventListener on_hover =
    card.addEventListener("mouseenter", [] { ShowPreview(); });

// Later:
on_hover.Remove();

📘 Destroying the Handle Doesn't Remove the Listener

A listener keeps firing after its handle is destroyed— call Remove() or use an AbortController to stop it.

Removing a Group

To manage multiple listeners together, pass the signal from a single dom::AbortController to each one. Calling abort() on the controller removes all listeners registered with that signal at once:

C++
dom::AbortController drag;
document.addEventListener("mousemove", OnDrag, { .signal = drag.signal() });
document.addEventListener("mouseup", OnDrop, { .signal = drag.signal() });

// Later:
drag.abort();

Tying Listeners to an Object

Store a dom::AbortController as a member of any class whose listeners capture this. Unlike on the web, destroying the controller aborts it automatically, so all listeners stop when the owner is destroyed:

C++
class Hud {
 public:
  explicit Hud(dom::Document document) {
    document.getElementById("close").addEventListener(
        "click", [this] { Close(); }, { .signal = listeners_.signal() });
  }

 private:
  void Close() {}
  dom::AbortController listeners_;
};

🚧 Callbacks Outlive Your Object

A callback that captures this, a pointer, or a reference lives as long as the page. You must stop the listener before the object is destroyed (using a member dom::AbortController) or capture a std::weak_ptr instead.

Member Functions

You can pass an object and a member function pointer instead of a lambda.

Passing a smart pointer manages the object's lifetime— an owning pointer (RefPtr or std::shared_ptr) keeps the object alive for as long as the listener exists, while a weak pointer (WeakPtr or std::weak_ptr) skips events once the object is destroyed:

C++
save.addEventListener("click", menu, &Menu::OnSave);

Delegated Listeners

Calling On() on an element or the document handles events for matching descendants, including elements added after page load (eg, dynamic rows). The callback receives the matched dom::Element directly whenever the event fires:

C++
document.On("#inventory .slot", "click", [](dom::Element slot) {
  slot.classList.toggle("selected");
});

You can use pseudo-classes like :has() and :focus-visible in the selector.

The library checks the selector when you call On()— a malformed selector adds nothing and logs a warning at every diagnostics level.

Only custom pseudo-elements (like ::placeholder and ::-webkit-*) are refused, behaving like a malformed selector.

Window Events

Events like load and resize fire on the window only, so document listeners never see them (while viewport scroll fires on the document and bubbles to the window). To handle window-level events, attach your listener to document.defaultView():

C++
document.defaultView().addEventListener("resize", [] { LayoutHud(); });

Dispatching Events

Calling dispatchEvent() runs every native and page listener synchronously before returning. It returns false if a listener cancels the event (which requires setting .cancelable = true on the options struct):

C++
if (!panel.dispatchEvent("closerequest",
                         { .bubbles = true, .cancelable = true }))
  return;

Sending Data to the Page

To send data to the page, call dispatchCustomEvent() with a string payload (use JSON when passing structured data). Page scripts can then read the string directly from event.detail:

C++
document.dispatchCustomEvent("lootdrop", R"({ "item": "Sword" })");

Differences from JavaScript

While DOM event handling in Ultralight mirrors JavaScript, several behaviors and types differ in C++.

JavaScript C++
removeEventListener(type, fn) Keep the handle and call Remove()
The event object can be kept Borrowed for the callback only (it can't be copied or stored)
CustomEvent.detail is any value A string (use JSON)
Options object { once: true } A struct with designated initializers ({ .once = true })