Expert guidance for writing frontend code using NuiCpp -- a C++ WebAssembly (WASM) frontend library compiled via Emscripten. Conceptually similar to JSX but expressed entirely in C++. Use this skill whenever the user asks to build UI components, pages, or applications using NuiCpp, Nui, or "C++ frontend/WASM UI". Also trigger when the user mentions Nui::Observed, Nui::range, ElementRenderer, Nui::Elements, Nui::Attributes, StateTransformer, ChangePolicy, ValWrapper, convertToVal, convertFromVal, or any other Nui:: namespace identifiers. Trigger even for partial tasks like "add a button to my Nui component", "how do I handle events in Nui", or "write a function component". This skill covers element syntax, reactive state, range rendering, class components (functor + pimpl), function components, StateTransformer, nil/fragment, WebAPI event wrappers, val conversion, JS interop via emscripten::val, and reactive side-effects via listen/smartListen/ListenRemover.
NuiCpp is a C++ WASM frontend library (Emscripten). Think of it as JSX-in-C++: you build DOM trees using a C++ DSEL (domain-specific embedded language). Always apply the patterns below precisely -- the syntax is strict and deviations cause compile errors.
div{
class_ = "my-class",
id = "my-id",
onClick = [](Nui::val event) { std::cout << "clicked\n"; },
"change"_event = [](Nui::val event) { /* custom event */ },
"data-foo"_attr = "bar", // custom data attributes
"value"_prop = "something", // DOM properties
}(
/* children - parentheses are NEVER omitted */
span{}("child text"),
text{"plain text content"}() // use text{} for mixed text+children
)
Key rules:
() after the {} attribute block. Always include () even if empty.text{"..."}(), not raw string literals alongside elements.Nui::WebApi::MouseEventNui::WebApi::EventNui::WebApi::KeyboardEventNui::WebApi::DragEventNui::Observed#include <nui/event_system/observed_value.hpp>
Nui::Observed<std::string> label{"Hello"};
Nui::Observed<std::vector<std::string>> items;
| Intent | Syntax | Effect |
|---|---|---|
| Mutate + track change | items.push_back("x") | Queues UI update |
| Mutate silently | items.value().push_back("x") | No UI update |
| Full invalidation | items.modify() | Full range redraw |
Use .modify() when changing 50% or more of a vector's elements.
Changes are queued, not applied immediately.
Nui::globalEventContext.executeActiveEventsImmediately();
Do not copy Nui::Observed into temporaries when mutating -- always mutate the original.
listen / smartListen#include <nui/event_system/listen.hpp>
Use listen / smartListen to run imperative side-effects whenever an Observed value
changes (logging, fetching, cascading state updates, etc.). This is separate from the
declarative UI bindings in the section below.
listen -- raw event registrationReturns an EventRegistry::EventIdType. You are responsible for removing the event manually.
// Basic: callback receives the new value by const-ref
Nui::listen(myObserved, [](std::string const& newValue) {
Nui::WebApi::Console::log("changed to: " + newValue);
});
// Early-out: return false to automatically deregister after first fire
Nui::listen(myObserved, [](int val) -> bool {
if (val == 42) return false; // unregistered after this call
return true; // keep listening
});
// shared_ptr overload: event is auto-removed when the Observed is destroyed
auto sharedObs = std::make_shared<Nui::Observed<int>>(0);
Nui::listen(sharedObs, [](int val) { /* ... */ });
Warning: Do not mutate any Observed from inside a listen callback -- this causes
infinite recursion. Use smartListen instead.
smartListen -- RAII + safe side-effectsReturns a ListenRemover that automatically deregisters the listener on destruction.
The callback is delayed until after all currently-active events have finished, so it is safe
to mutate other Observed values inside the callback.
// Must store the remover -- discarding it immediately removes the listener!
auto remover = Nui::smartListen(myObserved, [&otherObs](std::string const& newValue) {
// Safe to modify other Observed values here:
otherObs = "derived: " + newValue;
Nui::globalEventContext.executeActiveEventsImmediately(); // flush if outside event handler
});
ListenRemover is move-only (no copy). Store it as a member in the component that owns
the side-effect:
struct MyWidget::Implementation {
Nui::Observed<std::string> query{};
Nui::Observed<std::vector<std::string>> results{};
Nui::ListenRemover<Nui::Observed<std::string>> queryListener;
Implementation()
: queryListener{Nui::smartListen(query, [this](std::string const& q) {
// Kick off a fetch, update results, etc.
results.assign({});
Nui::globalEventContext.executeActiveEventsImmediately();
})}
{}
};
smartListen with shared_ptr<Observed>When the observed value is heap-allocated and may be destroyed independently:
auto sharedObs = std::make_shared<Nui::Observed<int>>(0);
auto remover = Nui::smartListen(sharedObs, [](int val) {
// callback fires only while sharedObs is alive
});
ListenRemoverremover.removeEvent(); // detach now (also called by destructor)
remover.disarm(); // prevent destructor from removing (ownership transfer)
| API | Returns | Safe to mutate Observed? | Lifetime managed by |
|---|---|---|---|
Nui::listen(obs, fn) | EventIdType | ❌ causes recursion | caller |
Nui::smartListen(obs, fn) | ListenRemover (nodiscard) | ✅ delayed execution | ListenRemover RAII |
div{
class_ = Nui::observe(label).generate([](std::string const& txt) {
return fmt::format("active {}", txt);
})
}()
div{
style = Nui::observe(visible).generate([](bool v) {
return fmt::format("display: {};", v ? "flex" : "none");
})
}()
div{}(label) // renders label's value as text, auto-updates
Nui::observe + renderer)Must be the only logical child of its parent:
div{}(
Nui::observe(label),
[](std::string const& txt) -> Nui::ElementRenderer {
return span{}(txt);
}
)
Nui::range must be the first (and only) logical child of its parent element,
paired immediately with its renderer lambda.
div{}(
Nui::range(items),
[](long long /*index - must be long long, not auto*/, auto const& item) -> Nui::ElementRenderer {
return span{}(item);
}
)
Use .before() / .after() instead of placing elements outside the range:
div{}(
Nui::range(items)
.before(
div{}("Header"),
div{}("Subheader")
)
.after(
div{}("Footer")
),
[](long long i, auto const& item) -> Nui::ElementRenderer {
return div{}(std::to_string(item));
}
)
Nui::nil() produces an ElementRenderer that inserts nothing into the DOM. Use it to
conditionally render nothing in places that syntactically require a child expression.
#include <nui/frontend/elements/nil.hpp>
div{}(
Nui::observe(showBanner),
[](bool show) -> Nui::ElementRenderer {
if (!show)
return Nui::nil();
return div{class_ = "banner"}("Hello!");
}
)
Nui::Elements::fragment(...) renders multiple children into a parent without adding an
enclosing DOM node, similar to React fragments.
#include <nui/frontend/elements/fragment.hpp>
div{}(
fragment(
span{}("First"),
span{}("Second"),
span{}("Third")
)
)
Important limitation: Do not use fragments with observed/reactive logic. Because fragments are removed and reinserted as a unit on change, and the rerender is deferred, their contents end up appended to the back of the parent rather than staying in place. Keep fragments to simple, static structural groupings only.
All typed event classes (MouseEvent, KeyboardEvent, etc.) extend Nui::ValWrapper, which
is a lightweight, non-owning view over a Nui::val. Construction is cheap -- no data is
copied; all property accessors read directly from the underlying JS object.
#include <nui/frontend/val_wrapper.hpp> // Nui::ValWrapper base
#include <nui/frontend/api/mouse_event.hpp>
div{
onClick = [](Nui::WebApi::MouseEvent event) {
double x = event.clientX();
double y = event.clientY();
bool shifted = event.shiftKey();
bool ctrl = event.ctrlKey();
int btn = event.button(); // 0=left, 1=middle, 2=right
// event.val() gives the raw Nui::val back if needed
}
}()
clientX/Y, x/y, screenX/Y, pageX/Y, offsetX/Y, movementX/Y,
button, buttons, altKey, ctrlKey, shiftKey, metaKey, relatedTarget.
| Header | Type | Typical use |
|---|---|---|
nui/frontend/api/event.hpp | Nui::WebApi::Event | onChange, onInput, generic |
nui/frontend/api/keyboard_event.hpp | Nui::WebApi::KeyboardEvent | onKeyDown/Up/Press |
nui/frontend/api/drag_event.hpp | Nui::WebApi::DragEvent | onDrag, onDrop, etc. |
nui/frontend/api/ui_event.hpp | Nui::WebApi::UiEvent | base of mouse/keyboard events |
All wrappers have a .val() accessor returning the raw Nui::val for any property not
directly exposed.
Using Nui::val directly is always valid too, for cases where the typed wrapper isn't
available or you need a property not yet wrapped:
onClick = [](Nui::val event) {
auto target = event["target"];
std::string value = target["value"].as<std::string>();
}
convertToVal / convertFromVal -- C++ to JS Conversion#include <nui/frontend/utility/val_conversion.hpp>
These are rarely needed directly -- prefer Nui::val member access for JS interop -- but
they are useful when passing structured C++ data to a JS function or when receiving
structured data back.
convertToVal -- C++ to JSHandles: fundamental types, std::string, std::string_view, std::filesystem::path,
std::vector, std::map/unordered_map<std::string, T>, std::pair, std::optional,
std::variant, std::unique_ptr, std::shared_ptr, Nui::Observed<T>, Nui::val,
and any boost-described struct/class.
// Fundamental and string types
Nui::val n = Nui::convertToVal(42);
Nui::val s = Nui::convertToVal(std::string{"hello"});
// Vector becomes a JS array