Forms and events
Handlers are Dioxus handlers
You write onclick, oninput, onchange and
onsubmit as you would for the web. A click lands on the element under the
pointer and bubbles; a form does what a browser's form does. Fields are uncontrolled, so
typing never makes a round trip through Rust.
Clicks
A click goes to the topmost element at that point, as decided by paint order and
z-index, and bubbles to the nearest ancestor that listens. Then the browser's
defaults follow:
- a checkbox under the pointer ticks or clears, a radio button is picked, and both send
inputandchange; - a submit button submits its form;
- a
labelclicks the control it is for.
A checkbox changes before the click handlers run, so they see its new state, and it changes
back if one of them calls prevent_default(). On a link,
prevent_default() keeps the page where it is, as in samples/hello.
Fields are uncontrolled
The renderer owns a field's text while the user edits it. Only committed text reaches Rust,
through oninput, never text an input method is still composing. This is the rule
that keeps Korean, Japanese and Chinese input working: a syllable is built from several
keystrokes, and echoing each one back from Rust would reset it.
samples/settings shows the pattern. The fields start from what was saved. What
is typed goes into a draft that nothing renders from, so typing does not render the form
again and never writes a field's text back while it is being edited.
// What was last saved. The fields start from it.
let mut saved = use_signal(Profile::initial);
// What has been typed since. Nothing renders from it.
let mut draft = use_signal(Profile::initial);
let profile = saved();
rsx! {
form {
id: "settings",
class: "settings",
// Pressing Enter in a field submits the form.
onsubmit: move |event| {
event.prevent_default();
saved.set(draft());
},
input {
id: "name",
class: "control",
r#type: "text",
value: "{profile.name}",
placeholder: "Your name",
oninput: move |event| {
draft.write().name = event.value();
},
}
textarea {
id: "bio",
class: "control",
value: "{profile.bio}",
oninput: move |event| {
draft.write().bio = event.value();
},
}
}
}
A field holds what was committed until the app sets its value again, and that
value is what the form submits. Setting value is how the app replaces the text,
for example to clear a field after sending, as samples/chat and
samples/todo do.
Selects, checkboxes and radio buttons
select {
id: "theme",
class: "control",
value: "{profile.theme}",
onchange: move |event| {
draft.write().theme = event.value();
},
option { value: "light", "Light" }
option { value: "dark", "Dark" }
option { value: "system", "Match the system" }
}
input {
id: "weekly",
r#type: "checkbox",
checked: weekly(),
onclick: move |_| weekly.toggle(),
}
for (index, choice) in DENSITIES.into_iter().enumerate() {
label { key: "{choice}", class: "check",
input {
id: "density-{index}",
r#type: "radio",
name: "density",
value: "{choice}",
checked: density() == choice,
onclick: move |_| density.set(choice),
}
"{choice}"
}
}
A select is one field whose options the renderer lists in its own dropdown; the
options are not drawn in the page. Picking one sends input and then
change, as a browser does. Radio buttons with the same name form a
group, so picking one clears the others.
The input types text, password, email,
number, search, tel, url,
checkbox and radio, and textarea and
select, each arrive in the display list as a field with its kind, its starting
value, its placeholder and the box its text goes in.
What the renderer calls
Each user action has one call on HtmlDom. The renderer will make them once it
draws HTML screens. Today tests and embedders make them, and the examples' tests go through
exactly these calls.
| Call | The user | What reaches the app |
|---|---|---|
click(x, y) | clicks a point | onclick, then the browser default |
input(node, value) | commits text in a field | oninput |
change(node, value) | is done with a field | onchange |
select(node, index) | picks an option | oninput, onchange |
check(node, checked) | ticks a checkbox or picks a radio | onclick, oninput, onchange |
submit(node) | presses Enter in a single-line field | the submit button's onclick if there is one, then onsubmit |
assert_eq!(dom.input(name, "Grace Hopper"), Some(name));
assert_eq!(dom.select(theme, 1), Some(theme));
relayout(&mut dom);
// Typing renders nothing: the field's text stays the renderer's.
assert_eq!(entry(&dom, "name").input.as_ref().unwrap().value, "Ada Lovelace");
assert_eq!(dom.submit(name), Some(form));
relayout(&mut dom);
assert_eq!(text_of(entry(&dom, "status")), "Saved");
assert_eq!(entry(&dom, "name").input.as_ref().unwrap().value, "Grace Hopper");
Handlers run inside these calls. render applies what they changed, and the next
layout shows it.
Keyed lists
Give each row a key, as in any Dioxus app. In samples/todo moving an
item up moves its row rather than rewriting the text of every row, ticking an item changes
only that row, and adding one inserts one. The plan's diff reports exactly those
inserts, removals and moves, which is what keeps an update small at the renderer.
ul { id: "todos", class: "todos",
for (index, todo) in items.into_iter().enumerate() {
li {
key: "{todo.id}",
id: "todo-{todo.id}",
class: if todo.done { "todo done" } else { "todo" },
input {
id: "toggle-{todo.id}",
r#type: "checkbox",
checked: todo.done,
onclick: move |_| todos.toggle(todo.id),
}
span { class: "title", "{todo.title}" }
button {
id: "up-{todo.id}",
disabled: index == 0,
onclick: move |_| todos.shift(todo.id, Direction::Up),
"Up"
}
}
}
}
State that is shared
The list in samples/todo and the conversation in samples/chat live in
a struct of signals that the app provides as context, so a child component such as the row
that adds an item reads it with use_context. This is ordinary Dioxus, and nothing
about it is specific to this renderer.
Not yet
- Fields on screen. partial In the plan an
inputortextareabecomes Compose's own text field and aselecta dropdown, so composition is Compose's. Nothing draws them until the renderer bridge lands (#43), and the Korean input checklist is run by hand on the native build once it does. - Keyboard events and focus on HTML screens. planned Key presses and focus moves come from the renderer, so they arrive with the same bridge (#43).