docs

DOM Triggers and Navigation

Register DOM event callbacks once in C++ and keep them active across page navigations.

On this page

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:

C++
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:

C++
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 for rule syntax).

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

C++
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:

C++
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:

C++
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:

C++
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:

C++
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