Forms and Inputs
Forms and form inputs are an important part of interactive apps. There are two basic patterns for interacting with inputs in Leptos, which you may recognize if you’re familiar with React, SolidJS, or a similar framework: using controlled or uncontrolled inputs.
Controlled Inputs
In a "controlled input," the framework controls the state of the input
element. On every input
event, it updates a local signal that holds the current
state, which in turn updates the value
prop of the input.
There are two important things to remember:
- The
input
event fires on (almost) every change to the element, while thechange
event fires (more or less) when you unfocus the input. You probably wanton:input
, but we give you the freedom to choose. - The
value
attribute only sets the initial value of the input, i.e., it only updates the input up to the point that you begin typing. Thevalue
property continues updating the input after that. You usually want to setprop:value
for this reason. (The same is true forchecked
andprop:checked
on an<input type="checkbox">
.)
let (name, set_name) = signal("Controlled".to_string());
view! {
<input type="text"
// adding :target gives us typed access to the element
// that is the target of the event that fires
on:input:target=move |ev| {
// .value() returns the current value of an HTML input element
set_name.set(ev.target().value());
}
// the `prop:` syntax lets you update a DOM property,
// rather than an attribute.
prop:value=name
/>
<p>"Name is: " {name}</p>
}
Why do you need
prop:value
?Web browsers are the most ubiquitous and stable platform for rendering graphical user interfaces in existence. They have also maintained an incredible backwards compatibility over their three decades of existence. Inevitably, this means there are some quirks.
One odd quirk is that there is a distinction between HTML attributes and DOM element properties, i.e., between something called an “attribute” which is parsed from HTML and can be set on a DOM element with
.setAttribute()
, and something called a “property” which is a field of the JavaScript class representation of that parsed HTML element.In the case of an
<input value=...>
, setting thevalue
attribute is defined as setting the initial value for the input, and settingvalue
property sets its current value. It maybe easiest to understand this by openingabout:blank
and running the following JavaScript in the browser console, line by line:// create an input and append it to the DOM const el = document.createElement("input"); document.body.appendChild(el); el.setAttribute("value", "test"); // updates the input el.setAttribute("value", "another test"); // updates the input again // now go and type into the input: delete some characters, etc. el.setAttribute("value", "one more time?"); // nothing should have changed. setting the "initial value" does nothing now // however... el.value = "But this works";
Many other frontend frameworks conflate attributes and properties, or create a special case for inputs that sets the value correctly. Maybe Leptos should do this too; but for now, I prefer giving users the maximum amount of control over whether they’re setting an attribute or a property, and doing my best to educate people about the actual underlying browser behavior rather than obscuring it.
Uncontrolled Inputs
In an "uncontrolled input," the browser controls the state of the input element.
Rather than continuously updating a signal to hold its value, we use a
NodeRef
to access
the input when we want to get its value.
In this example, we only notify the framework when the <form>
fires a submit
event.
Note the use of the leptos::html
module, which provides a bunch of types for every HTML element.
let (name, set_name) = signal("Uncontrolled".to_string());
let input_element: NodeRef<html::Input> = NodeRef::new();
view! {
<form on:submit=on_submit> // on_submit defined below
<input type="text"
value=name
node_ref=input_element
/>
<input type="submit" value="Submit"/>
</form>
<p>"Name is: " {name}</p>
}
The view should be pretty self-explanatory by now. Note two things:
- Unlike in the controlled input example, we use
value
(notprop:value
). This is because we’re just setting the initial value of the input, and letting the browser control its state. (We could useprop:value
instead.) - We use
node_ref=...
to fill theNodeRef
. (Older examples sometimes use_ref
. They are the same thing, butnode_ref
has better rust-analyzer support.)
NodeRef
is a kind of reactive smart pointer: we can use it to access the
underlying DOM node. Its value will be set when the element is rendered.
let on_submit = move |ev: SubmitEvent| {
// stop the page from reloading!
ev.prevent_default();
// here, we'll extract the value from the input
let value = input_element
.get()
// event handlers can only fire after the view
// is mounted to the DOM, so the `NodeRef` will be `Some`
.expect("<input> should be mounted")
// `leptos::HtmlElement<html::Input>` implements `Deref`
// to a `web_sys::HtmlInputElement`.
// this means we can call`HtmlInputElement::value()`
// to get the current value of the input
.value();
set_name(value);
};
Our on_submit
handler will access the input’s value and use it to call set_name
.
To access the DOM node stored in the NodeRef
, we can simply call it as a function
(or using .get()
). This will return Option<leptos::HtmlElement<html::Input>>
, but we
know that the element has already been mounted (how else did you fire this event!), so
it's safe to unwrap here.
We can then call .value()
to get the value out of the input, because NodeRef
gives us access to a correctly-typed HTML element.
Take a look at web_sys
and HtmlElement
to learn more about using a leptos::HtmlElement
.
Also see the full CodeSandbox example at the end of this page.
Special Cases: <textarea>
and <select>
Two form elements tend to cause some confusion, in different ways.
<textarea>
Unlike <input>
, the <textarea>
element does not support a value
attribute.
Instead, it receives its value as a plain text node in its HTML children.
In the current version of Leptos (in fact in Leptos 0.1-0.6), creating a dynamic child
inserts a comment marker node. This can cause incorrect <textarea>
rendering (and issues
during hydration) if you try to use it to show dynamic content.
Instead, you can pass a non-reactive initial value as a child, and use prop:value
to
set its current value. (<textarea>
doesn’t support the value
attribute, but does
support the value
property...)
view! {
<textarea
prop:value=move || some_value.get()
on:input:target=move |ev| some_value.set(ev.target().value())
>
/* plain-text initial value, does not change if the signal changes */
{some_value.get_untracked()}
</textarea>
}
<select>
The <select>
element can likewise be controlled via a value
property on the <select>
itself,
which will select whichever <option>
has that value.
let (value, set_value) = signal(0i32);
view! {
<select
on:change:target=move |ev| {
set_value(ev.target().value().parse().unwrap());
}
prop:value=move || value.get().to_string()
>
<option value="0">"0"</option>
<option value="1">"1"</option>
<option value="2">"2"</option>
</select>
// a button that will cycle through the options
<button on:click=move |_| set_value.update(|n| {
if *n == 2 {
*n = 0;
} else {
*n += 1;
}
})>
"Next Option"
</button>
}
Controlled vs uncontrolled forms CodeSandbox
CodeSandbox Source
use leptos::{ev::SubmitEvent};
use leptos::prelude::*;
#[component]
fn App() -> impl IntoView {
view! {
<h2>"Controlled Component"</h2>
<ControlledComponent/>
<h2>"Uncontrolled Component"</h2>
<UncontrolledComponent/>
}
}
#[component]
fn ControlledComponent() -> impl IntoView {
// create a signal to hold the value
let (name, set_name) = signal("Controlled".to_string());
view! {
<input type="text"
// fire an event whenever the input changes
// adding :target after the event gives us access to
// a correctly-typed element at ev.target()
on:input:target=move |ev| {
set_name.set(ev.target().value());
}
// the `prop:` syntax lets you update a DOM property,
// rather than an attribute.
//
// IMPORTANT: the `value` *attribute* only sets the
// initial value, until you have made a change.
// The `value` *property* sets the current value.
// This is a quirk of the DOM; I didn't invent it.
// Other frameworks gloss this over; I think it's
// more important to give you access to the browser
// as it really works.
//
// tl;dr: use prop:value for form inputs
prop:value=name
/>
<p>"Name is: " {name}</p>
}
}
#[component]
fn UncontrolledComponent() -> impl IntoView {
// import the type for <input>
use leptos::html::Input;
let (name, set_name) = signal("Uncontrolled".to_string());
// we'll use a NodeRef to store a reference to the input element
// this will be filled when the element is created
let input_element: NodeRef<Input> = NodeRef::new();
// fires when the form `submit` event happens
// this will store the value of the <input> in our signal
let on_submit = move |ev: SubmitEvent| {
// stop the page from reloading!
ev.prevent_default();
// here, we'll extract the value from the input
let value = input_element.get()
// event handlers can only fire after the view
// is mounted to the DOM, so the `NodeRef` will be `Some`
.expect("<input> to exist")
// `NodeRef` implements `Deref` for the DOM element type
// this means we can call`HtmlInputElement::value()`
// to get the current value of the input
.value();
set_name.set(value);
};
view! {
<form on:submit=on_submit>
<input type="text"
// here, we use the `value` *attribute* to set only
// the initial value, letting the browser maintain
// the state after that
value=name
// store a reference to this input in `input_element`
node_ref=input_element
/>
<input type="submit" value="Submit"/>
</form>
<p>"Name is: " {name}</p>
}
}
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
leptos::mount::mount_to_body(App)
}