You can bind form controls on the page to native fields using `ul-value`. The control shows the field's value, and user changes come back to native code as requests that native code accepts or declines.

## Example Data

The examples on this page use a settings panel where `dd::Editable` marks the fields the page may change— see [Describing Types with Schemas](/docs/2.0/describing-types-with-schemas):

```cpp
enum class Difficulty { Easy, Normal, Hard };

struct Settings {
  double volume = 0.8;
  bool subtitles = true;
  std::string playerName = "Ava";
  Difficulty difficulty = Difficulty::Normal;

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

Native code binds the settings instance:

```cpp
Settings settings;

// This instance will be exposed to markup as "settings.<field>..."
dd::Binding binding = ctx.Bind("settings", settings);
```

## Binding a Control

Set `ul-value` on a form control with the path to the field:

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

The library never writes directly to the field— it delivers the change request to an `OnChange` handler:

```cpp
binding.OnChange<"volume">([&](double volume) {
  settings.volume = volume;  // storing the value accepts the edit
});
```

### How an Edit Travels

When a user edits a control, the change passes through four stages:

1. The user moves the slider, and the control shows the new position right away.
2. The next `Sync()` runs the handler on the home thread (before any actions).
3. The handler stores the value to accept the edit, or leaves the field as it was to decline it.
4. The same `Sync()` sends the field's value back to the page, so a declined edit snaps back.

> 📘 Only Fields of the Bound Type
>
> Edits come back only for fields of the bound type itself. Using `ul-value` in a list row (see [Binding Lists](/docs/2.0/binding-lists)) or on a nested object shows the value but sends no changes. A field without `dd::Editable` doesn't bind the control at all.

## Controls for Each Field Type

Each field type pairs with a matching form control on the page:

```html
<input type="checkbox" ul-value="settings.subtitles">
<input type="text" ul-value="settings.playerName">
<select ul-value="settings.difficulty">
  <option>Easy</option>
  <option>Normal</option>
  <option>Hard</option>
</select>
```

Each `OnChange` handler takes the field's type:

```cpp
binding.OnChange<"subtitles">([&](bool on) { settings.subtitles = on; });
binding.OnChange<"playerName">(
    [&](const std::string& name) { settings.playerName = name; });
binding.OnChange<"difficulty">([&](Difficulty d) { settings.difficulty = d; });
```

| Field Type | Control | Notes |
| :--- | :--- | :--- |
| `bool` | Checkbox | Sends its checked state. |
| Number | Range, number, or text input | |
| String | Text input or `<textarea>` | |
| Enum | `<select>` | Each option's value (or text) is an enumerator name. |
| `Color` | Color input (`type="color"`) | The handler takes a `Color`. |

### Text and Number Fields

A text field sends a change on every keystroke— the handler runs once per `Sync()` with the latest text.

A number field ignores text that isn't a number yet (such as an empty field or a lone minus sign).

An integer field rounds the number.

## Validating Changes

A validator in the schema checks or corrects a value before the change request is queued— it returns the value to accept:

```cpp
struct Audio {
  double volume = 0.8;

  static constexpr auto schema = dd::Schema(
      dd::Field("volume", &Audio::volume, dd::Editable,
                dd::Validate([](double v) {
                  return std::clamp(v, 0.0, 1.0);
                })));
};
```

### Rejecting a Value

Return a `std::optional` and return `std::nullopt` to reject a value— the control snaps back to the last accepted value right away:

```cpp
struct Profile {
  std::string playerName = "Ava";

  static constexpr auto schema = dd::Schema(
      dd::Field("playerName", &Profile::playerName, dd::Editable,
                dd::Validate(
                    [](std::string name) -> std::optional<std::string> {
                      if (name.empty())
                        return std::nullopt;  // the field snaps back
                      return name;
                    })));
};
```

### Checking Other Fields

A second parameter receives the values the page shows— declare it `auto` inside the schema and read fields with `Get<"name">()`:

```cpp
struct Shop {
  int64_t gold = 500;
  int64_t bid = 0;

  static constexpr auto schema = dd::Schema(
      dd::Field("gold", &Shop::gold),
      dd::Field("bid", &Shop::bid, dd::Editable,
                dd::Validate([](int64_t bid, auto s) -> std::optional<int64_t> {
                  if (bid > s.template Get<"gold">())
                    return std::nullopt;  // can't bid more than you have
                  return bid;
                })));
};
```

> 🚧 Validators Run on the Renderer's Thread
>
> Validators must not read or change native objects (read other fields through the second parameter). A validator must not capture anything and must not throw.
