docs

Describing Types with Schemas

Define schemas for native types to bind data and actions to the page.

On this page

A schema declares which members of a native type the page can access and how each field is exposed to markup.

Mapping Types to Schemas

Choose how to describe a type based on how it is defined in native code:

Native Type What to Use
Existing class dd::TypeTraits specialization
Type written for the UI schema member
Plain struct Automatic binding (no schema)
Plain struct with actions or editable fields dd::Reflect in a dd::TypeTraits specialization

A TypeTraits Specialization

You can define schemas for existing classes by specializing dd::TypeTraits:

C++
// An existing class that we want to expose to markup:
class Player {
 public:
  const std::string& name() const;
  int level() const;
  const Stats& stats() const;       // simple structs marshal via reflection
};

// Define a schema for "Player" by specializing dd::TypeTraits<Player>
template <> struct dd::TypeTraits<Player> {
  static constexpr auto schema = dd::Schema(
      dd::Field("name", &Player::name),     // player.name
      dd::Field("level", &Player::level),   // player.level
      dd::Field("stats", &Player::stats));  // player.stats
};

A Schema Member

You can also define schemas inside structs or classes by defining a schema member:

C++
struct Settings {
  double volume = 0.8;
  bool subtitles = true;

  static constexpr auto schema = dd::Schema(
      dd::Field("volume", &Settings::volume),
      dd::Field("subtitles", &Settings::subtitles));
};

This keeps field declarations beside the data members they expose.

Plain Structs

Simple structs bind automatically via reflection, with each field exposed under its C++ member name:

C++
// No schema needed (marshalled automatically):
struct Stats {
  int health = 100;  // stats.health
  int armor = 0;     // stats.armor
};

📘 Compiler Requirements

Automatic reflection requires Clang, GCC, or MSVC 19.40+. The struct must be a plain aggregate with at most 24 members, and without custom constructors, virtual functions, or C array members.

Reflect, Then Annotate

For the sake of convenience (and to add extra annotations where needed), you can combine reflection with explicit schemas by using dd::Reflect inside a dd::TypeTraits specialization:

C++
struct Toolbar {
  std::string url;
  double progress = 0;
};

template <> struct dd::TypeTraits<Toolbar> {
  static constexpr auto schema = dd::Schema(
      dd::Reflect<Toolbar>(),  // url and progress
      dd::Action("reload"));
};

Because dd::Reflect requires a complete type, define it in a dd::TypeTraits specialization rather than inside the struct itself.

Declaring Custom Fields

Pass dd::Field the property name (for exposing to markup) and a custom accessor to obtain the value:

C++
struct Enemy {
  std::string name;
  int hp = 0;
  int max_hp = 100;
  bool IsBoss() const;
};

template <> struct dd::TypeTraits<Enemy> {
  static constexpr auto schema = dd::Schema(
      dd::Field("name", &Enemy::name),    // a data member
      dd::Field("boss", &Enemy::IsBoss),  // a const member function
      dd::Field("hpPct", [](const Enemy& e) {
        return 100.0 * e.hp / e.max_hp;   // a custom, computed value
      }));
};

An accessor can be a member pointer, a const member function, or a stateless lambda. Lambda accessors compute values on the fly— define them in a dd::TypeTraits specialization because the class must be complete.

The return type of the accessor determines how the property behaves on the page:

Field Type What the Page Receives
Value type (bool, number, std::string, enum, Color) Bound value shown on the page
Described type Nested object reached with dot notation
std::optional or pointer to a described type Nested object that can be absent (null)
Container of described rows List bound to repeating elements

Nested Objects

Describing a schema with an existing, described type (or simple struct) exposes the type as a nested object in page markup:

C++
struct Profile {
  std::string name;  // profile.name
  Stats stats;       // { profile.stats.armor, profile.stats.health }

  static constexpr auto schema = dd::Schema(
      dd::Field("name", &Profile::name),
      dd::Field("stats", &Profile::stats));
};

In markup, access nested fields with dot notation:

HTML
<p>Armor: {{profile.stats.armor}}</p>

Optional Objects

An std::optional or pointer to a described type is null on the page while empty:

C++
struct MatchResult {
  int kills = 0;
};

struct Profile {
  std::optional<MatchResult> lastMatch;  // absent until the first match

  static constexpr auto schema = dd::Schema(
      dd::Field("lastMatch", &Profile::lastMatch));
};

Use ul-if to show an element only when the object is present:

HTML
<p ul-if="profile.lastMatch">Kills: {{profile.lastMatch.kills}}</p>

Enums

Enums bind automatically as strings using their enumerator names:

C++
enum class Rank { Bronze, Silver, Gold };

struct Badge {
  Rank rank = Rank::Gold;  // the page sees "Gold"
};

The page receives the name as text, which works directly in attributes and CSS selectors:

HTML
<div class="badge" ul-attr:data-rank="badge.rank"></div>

The library reflects values from 0 to 63 by default. Specialize dd::EnumRange if an enum uses values outside that range:

C++
template <> struct dd::EnumRange<ErrorCode> {
  static constexpr long long min = 0, max = 255;
};

Custom Value Types

To expose custom engine types as primitive values on the page, specialize dd::ValueTraits:

C++
template <> struct dd::ValueTraits<Fixed> {
  static constexpr dd::ValueKind kind = dd::ValueKind::Double;
  static double ToSlot(const Fixed& value) { return value.ToDouble(); }
};

ToSlot() converts the custom type into a supported primitive before the value reaches the page.

A change handler registered for a dd::ValueTraits field accepts either the underlying primitive or a dd::Value— it never receives the custom type.

When an action uses a dd::ValueTraits type as its payload, handlers receive the value only as a dd::Value.

Declaring Lists

Declare a list using dd::List, passing the collection accessor and a member pointer that identifies each row:

C++
struct Item {
  int64_t id = 0;
  std::string name;
  int count = 0;
};

struct Inventory {
  std::vector<Item> items;

  static constexpr auto schema = dd::Schema(
      dd::List("items", &Inventory::items, &Item::id));  // keyed by id
};

In markup, place ul-for on the container element and put row markup inside a <template> tag (a standard HTML tag whose contents the browser parses but never shows; the page copies it once per row):

HTML
<ul ul-for="inventory.items">
  <template>
    <li>{{name}} x{{count}}</li>
  </template>
</ul>

Paths inside the template resolve against the row's fields directly.

Keyed and Positional Lists

Omitting the key identifier matches rows by position rather than identity:

C++
struct Line {
  std::string text;
};

struct Chat {
  std::vector<Line> log;

  static constexpr auto schema = dd::Schema(
      dd::List("log", &Chat::log));  // no key: rows match by position
};

A keyed list preserves existing DOM elements, input focus, and CSS transitions when items reorder. A positional list suits collections that only append, such as chat or event logs.

For more on lists, see Binding Lists.

Declaring Actions

Actions let the page request operations in native code without calling functions directly:

C++
struct Door {
  bool open = false;

  static constexpr auto schema = dd::Schema(
      dd::Field("open", &Door::open),
      dd::Action("toggle"));  // the page fires door.toggle
};

Trigger the action in markup using the ul-on:click attribute:

HTML
<button ul-on:click="door.toggle">Open or Close</button>

Register a native handler to process the queued request during the next sync:

C++
binding.OnAction<"toggle">([&] { door.open = !door.open; });

Actions with a Payload

To pass data with an action, specify the payload type as a template parameter:

C++
struct Move {
  int from = 0;
  int to = 0;
};

struct Bag {
  static constexpr auto schema = dd::Schema(
      dd::Action<Move>("moveItem"),  // a struct payload
      dd::Action<double>("zoom"));   // a single value
};

Actions triggered from markup carry no payload. Payload actions are dispatched programmatically from native code.

For details on subscribing to and emitting actions, see Handling Actions.

Editable Fields

Marking a field with dd::Editable allows form controls on the page to request changes through ul-value:

C++
struct Settings {
  double volume = 0.8;

  static constexpr auto schema = dd::Schema(
      dd::Field("volume", &Settings::volume, dd::Editable));
};

Bind an input element in HTML to the editable field:

HTML
<input type="range" min="0" max="1" step="0.01" ul-value="settings.volume">

Applying Changes

Register an OnChange() handler to store the requested value:

C++
binding.OnChange<"volume">([&](double v) { settings.volume = v; });

The library never modifies native data directly— change requests are queued until Sync() runs the handler. Editable fields apply only to the bound root type, not to nested objects or list rows.

To learn how change requests work with form controls, see Binding Form Controls.

CSS Variables

Use dd::Var to publish numeric or color values directly to CSS custom properties without extra markup:

C++
struct Vitals {
  double stamina = 1.0;  // 0 to 1

  static constexpr auto schema = dd::Schema(
      dd::Var("--stamina", &Vitals::stamina));
};

Reference the property in CSS stylesheets using the standard var() function:

CSS
.stamina-bar { width: calc(var(--stamina) * 100%); }

On a bound root type, the property is set on the document's root element. On a row type, it is set on each row's root element. For more on styling with bindings, see Showing Data in Markup.

👍 Compile-Time Validation

Schema mistakes fail at compile time rather than runtime. Invalid names, duplicate entries, or annotations for missing fields produce compiler errors that name the broken rule, such as SchemaErrorDuplicateEntryName.

Entry names for fields, lists, and actions must start with a letter or an underscore, followed by letters, digits, and underscores (eg, maxHealth or slot_2). A dd::Var name must start with two hyphens followed by lowercase letters, digits, and hyphens (eg, --max-hp).