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:
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.
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:
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:
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 anAbortControllerto 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:
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:
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 memberdom::AbortController) or capture astd::weak_ptrinstead.
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:
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:
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():
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):
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:
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 }) |