You can read and update form controls and handle submissions directly from C++.

The examples use a signup form on the `dom::Document` named `document` (with name and email fields, an agree checkbox, a difficulty dropdown, a mode radio group, and a submit button).

## Reading and Writing Values

Standard form properties like `value`, `checked`, and `selectedIndex` work on any `dom::Element` just like in JavaScript:

```cpp
std::string typed = document.getElementById("name").value;
document.getElementById("agree").checked = true;
```

### Typed Views

You can narrow a generic element to its specific control type using helper methods like `AsInput()`, `AsTextArea()`, `AsSelect()`, or `AsForm()` (which return an empty handle if the tag doesn't match). A typed view gives you access to control-specific methods like `form()`:

```cpp
dom::HTMLInputElement name = document.getElementById("name").AsInput();
dom::HTMLFormElement form = name.form();
```

### Silent Writes and User Edits

Assigning to `value` or `checked` updates the control silently without firing events (useful when syncing native state to the page). To simulate user interaction, call `SetValue()` to update the value and fire `input` and `change` events, or call `click()` to toggle a checkbox:

```cpp
name.value = player.name;
name.SetValue(player.name);
agree.click();
```

### Form Attributes

Common HTML attributes are available as direct properties on typed controls (including `placeholder`, `required`, `readOnly`, `type`, `name`, and `defaultValue`):

```cpp
name.placeholder = "Your name";
name.required = true;
email.type = "email";
```

## Selects, Checkboxes, and Radios

A dropdown's `value` property returns the value of the active option, while `selectedIndex` holds its zero-based position as a `std::optional<size_t>` (`std::nullopt` if no option is selected):

```cpp
std::string level = difficulty.value;
difficulty.selectedIndex = 0;
```

### Listing the Options

You can step through a dropdown's options using `length()` and `item(i)` (where each option provides `text` and `selected`, and `selectedOptions()` returns a snapshot of active choices):

```cpp
for (size_t i = 0; i < difficulty.length(); i++)
  Log(difficulty.item(i).text);
```

### Reading a Radio Group

A radio button group has no shared value of its own on the form. To find the selected option, query the container for the checked input element:

```cpp
auto mode = form.querySelector("input[name=mode]:checked");
std::string chosen = mode.value;
```

## Validating Input

Validation constraints come from HTML attributes like `required`, `pattern`, `maxlength`, and `type`. To check whether all fields satisfy their rules without reporting errors to the user, call `checkValidity()` on the form or any control:

```cpp
if (form.checkValidity())
  SaveProfile();
```

### Showing Your Own Messages

Ultralight draws no popup bubbles when validation fails— `reportValidity()` only moves keyboard focus to the first failing field. Because the `invalid` event doesn't bubble, listen on the form with `dom::Capture` to intercept failing fields and read `validationMessage()`:

```cpp
form.On("input", "invalid", [](dom::Element field) {
  ShowError(field, field.AsInput().validationMessage());
}, dom::Capture);
```

### Styling Invalid Fields

The CSS `:invalid` pseudo-class matches required fields as soon as the page loads, even before the user types anything. To style controls only after the user has interacted with them, use `:user-invalid` instead:

```css
input:user-invalid { border-color: crimson; }
```

### Custom Rules

Calling `setCustomValidity()` marks a field invalid with your own error message (such as a username that is already taken). The error persists until you pass an empty string, so you should recheck the constraint as the user types:

```cpp
name.addEventListener("input", [name] {
  name.setCustomValidity(IsNameTaken(name.value) ? "That name is taken" : "");
});
```

### Why a Field Is Invalid

To inspect the exact reason a control failed validation, call `validity()` to receive a snapshot of constraint flags:

```cpp
dom::ValidityState state = email.validity();
if (state.typeMismatch)
  ShowError(email, "Enter a valid email address");
```

## Handling Submission Natively

To process form submissions in C++, listen for the `submit` event, call `preventDefault()`, and read control values through the `elements()` snapshot. Calling `requestSubmit()` validates all controls first— it fires `submit` only when every field is valid:

```cpp
form.addEventListener("submit", [form](dom::Event event) {
  event.preventDefault();
  std::map<std::string, std::string> data;
  for (auto field : form.elements())
    data[field.name] = field.value;
  SaveProfile(data);
});

form.requestSubmit();
```

### Submitting or Resetting Directly

Calling `submit()` sends the form immediately without validating fields or firing a `submit` event. To restore all controls to their initial defaults instead, call `reset()`:

```cpp
form.submit();
form.reset();
```

## Selecting Text in Fields

You can select text in any editable field using `select()` or `setSelectionRange()` (which also focuses the element and fires a `select` event on the next update). Selection calls and properties like `selectionStart` work across text-based input types (including `text`, `search`, `tel`, `url`, and `password`):

```cpp
name.select();
name.setSelectionRange(0, 3);
```

### Replacing Text

To replace the selected range with new text without firing `input` or `change` events, call `setRangeText()`:

```cpp
name.setRangeText("Dr.");
```

> 🚧 Offsets Are UTF-16
>
> Selection offsets count UTF-16 code units, like JavaScript. They don't match byte positions in the UTF-8 `std::string` that `value` returns once the text contains non-ASCII characters (eg, `"é"` is 2 bytes but 1 offset).

## Differences from JavaScript

While Ultralight's DOM API mirrors JavaScript, several collection types and behaviors differ in C++.

| JavaScript | C++ |
| :--- | :--- |
| `form.elements` and `select.selectedOptions` are live collections | `elements()` and `selectedOptions()` return snapshots |
| `input.validity` is a live object | `validity()` returns a snapshot struct |
| `reportValidity()` displays a popup bubble | Focuses the field (you display the message yourself) |
| Firing `input` and `change` events manually | `SetValue()` updates the value and fires both events |
| Selection offsets index the `value` string | Offsets count UTF-16 code units rather than UTF-8 bytes |
