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:

```cpp
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.

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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()`:

```cpp
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):

```cpp
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`:

```cpp
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 }`) |
