Forms and Inputs
Read and update form controls and handle submissions in native code.
On this page
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:
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():
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:
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):
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):
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):
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:
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:
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():
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:
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:
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:
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:
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():
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):
name.select();
name.setSelectionRange(0, 3);
Replacing Text
To replace the selected range with new text without firing input or change events, call setRangeText():
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::stringthatvaluereturns 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 |