You can define DOM event bindings in C++ that persist across page loads (and re-use them across multiple Views).

## Wiring Every Page

A `dom::Triggers` set holds your DOM event wiring in one place. Attach the set to a View before loading content so every page the View loads gets it:

```cpp
dom::Triggers ui;

ui.On("#save", "click", [] { SaveGame(); });
ui.OnDOMReady([](dom::Document document) {
  document.getElementById("status").textContent = "Ready";
});

if (ui.AttachTo(view.get()))
  view->LoadURL("file:///app.html");
```

### Kinds of Wiring

The set supports event listeners for matched elements along with lifecycle hooks for page loading:

| Registration | Runs |
| :--- | :--- |
| `On(selector, type, callback)` | When events fire on elements matching the selector, including elements added later (the callback takes the matched element, both the event and element, or nothing) |
| `OnDOMReady(callback)` | Once per page after parsing finishes (the place to find elements and keep handles, though the set never sees the page's own `DOMContentLoaded`) |
| `OnRestore(callback)` | Each time a page comes back from the back-forward cache |

### Events That Don't Bubble

Events that don't bubble (eg, `focus`, `blur`, and `invalid`) reach `On()` only with `dom::Capture`. Pass the flag as the last parameter to handle them:

```cpp
ui.On("input", "focus", [](dom::Element field) {
  field.classList.add("editing");
}, dom::Capture);
```

## Choosing Which Pages Get the Wiring

By default only your own content gets the set (`file://` pages and `View::LoadHTML()`). Passing origin rules replaces that default, so add `"file://*"` if you want to keep local pages (see [Extending JavaScript with Native API](/docs/2.0/extending-javascript-with-native-api) for rule syntax).

Pass `dom::AllFrames` to include subframes (which runs `OnDOMReady` for each frame's document):

```cpp
bool attached = ui.AttachTo(view.get(), {
    .flags = dom::AllFrames,
    .origin_rules = { "https://*.mygame.com", "file://*" } });
```

`AttachTo()` returns false (and logs why) when a rule fails to parse.

### Filtering Pages in Code

For decisions origin rules can't express (such as checking a user setting or admitting an opaque origin), set an injection filter on the View:

```cpp
dom::SetInjectionFilter(view.get(), [](const dom::InjectionRequest& request) {
  return request.rules_allow && request.is_main_frame;
});
```

Origin rules run first and pass their verdict in `rules_allow`. Return `true` to add the listeners or `false` to skip the page— the return value overrides the origin rules either way.

> 🚧 Call on the Renderer's Thread
>
> Call `dom::SetInjectionFilter()` on the Renderer's thread. The filter callback runs on that thread too.

To remove the filter and let origin rules decide on their own again, call `dom::ClearInjectionFilter(view.get())`.

## Navigation and the Back-Forward Cache

Same-document navigations like a hash change or `pushState` aren't new pages (handles stay valid and nothing is re-added). A page restored from the back-forward cache (`Config::page_cache_size` above 0) gets its listeners back before `pageshow`, but DOM-ready hooks don't run again— use `OnRestore()` for work needed on every restore:

```cpp
ui.OnRestore([](dom::Document document) {
  document.getElementById("status").textContent = "Welcome back";
});
```

> 📘 Direct Listeners Don't Survive Back and Forward
>
> Listeners added with `addEventListener()` are gone when a page is restored. Use a `dom::Triggers` set instead, or add them again in `OnRestore()`.

## Changing the Wiring

Registrations can't be removed one at a time, and any handler you add after attaching only reaches the next page the View loads. Call `DetachFrom()` to remove the whole set from a View right away:

```cpp
ui.DetachFrom(view.get());
```

### Storing a Set in an Object

Store a `dom::Triggers` set as a member of any class whose callbacks capture `this`:

```cpp
class Hud {
 public:
  explicit Hud(View* view) {
    ui_.On("#close", "click", [this] { Close(); });
    if (!ui_.AttachTo(view))
      Log("an origin rule failed to parse");
  }

 private:
  void Close() {}
  dom::Triggers ui_;
};
```

An attached View keeps the set only while the `dom::Triggers` object exists. Destroying the object detaches it from every View— its listeners stop right away, so callbacks can safely call the owner's methods.

The library destroys registered callables (and anything they capture) on the Renderer's thread. This cleanup runs during destruction when nothing else references the set, or during a later `Renderer::Update()`.

## Triggers or addEventListener?

Use `dom::Triggers` for listeners that need to work across multiple page navigations (or need to be used across multiple Views), and use `addEventListener()` for listeners tied to a single page's state:

| | `dom::Triggers` | `addEventListener()` |
| :--- | :--- | :--- |
| Pages | Every page of every attached View | The current page only |
| Targets | Elements matching a selector, now and later | One element, document, or window |
| Back and forward | Re-added on restore | Gone after a restore |
| Removing | The whole set, with `DetachFrom()` | One listener, with `Remove()` |
| Use it for | UI wiring that must always work | Listeners tied to one page's state |
