docs/library/forms/form.md
import reflex as rx
Forms are used to collect user input. The rx.form component is used to group inputs and submit them together.
The form component's children can be form controls such as rx.input, rx.checkbox, rx.slider, rx.textarea, rx.radio_group, rx.select or rx.switch. The controls should have a name attribute that is used to identify the control in the form data. The on_submit event trigger submits the form data as a dictionary to the handle_submit event handler.
The form is submitted when the user clicks the submit button or presses enter on the form controls.
class FormState(rx.State):
form_data: dict = {}
@rx.event
def handle_submit(self, form_data: dict):
"""Handle the form submit."""
self.form_data = form_data
def form_example():
return rx.vstack(
rx.form(
rx.vstack(
rx.input(placeholder="First Name", name="first_name"),
rx.input(placeholder="Last Name", name="last_name"),
rx.hstack(
rx.checkbox("Checked", name="check"),
rx.switch("Switched", name="switch"),
),
rx.button("Submit", type="submit"),
),
on_submit=FormState.handle_submit,
reset_on_submit=True,
),
rx.divider(),
rx.heading("Results"),
rx.text(FormState.form_data.to_string()),
)
# When using the form you must include a button or input with `type='submit'`.
# Using `name` vs `id`.
When using the `name` attribute in form controls like `rx.switch`, `rx.radio_group`, and `rx.checkbox`, these controls will only be included in the form data if their values are set (e.g., if the checkbox is checked, the switch is toggled, or a radio option is selected).
If you need these controls to be passed in the form data even when their values are not set, you can use the `id` attribute instead of name. The id attribute ensures that the control is always included in the submitted form data, regardless of whether its value is set or not.
# Video: Forms
The on_submit handler usually receives the form data as a plain dict, which
means accessing a field is untyped (form_data["email"] returns Any) and a
typo in a name goes unnoticed until runtime.
Instead, you can annotate the handler's parameter with a
TypedDict.
This gives you typed, autocompleted access to each field inside the handler, and
Reflex validates the form at compile time: every required key of the
TypedDict must have a matching form control. If a required field has no
control with that name (or id), Reflex raises an EventHandlerValueError
before the app starts, pointing out exactly which fields are missing.
from typing import TypedDict
from typing_extensions import NotRequired
class ContactForm(TypedDict):
first_name: str
last_name: str
email: str
message: NotRequired[str] # optional field
class TypedFormState(rx.State):
form_data: ContactForm | None = None
@rx.event
def handle_submit(self, form_data: ContactForm):
"""Handle the form submit."""
# form_data is typed: editors autocomplete the keys below.
self.form_data = form_data
def typed_form_example():
return rx.vstack(
rx.form(
rx.vstack(
rx.input(placeholder="First Name", name="first_name"),
rx.input(placeholder="Last Name", name="last_name"),
rx.input(placeholder="Email", name="email", type="email"),
rx.text_area(placeholder="Message", name="message"),
rx.button("Submit", type="submit"),
),
on_submit=TypedFormState.handle_submit,
reset_on_submit=True,
),
rx.divider(),
rx.heading("Results"),
rx.text(TypedFormState.form_data.to_string()),
)
By default every key declared in a TypedDict is required and must be
backed by a form control. Mark a field as optional with NotRequired (or by
inheriting from a total=False base) so Reflex won't require a matching
control:
from typing import TypedDict
from typing_extensions import NotRequired
class ContactForm(TypedDict):
name: str # required: a control named "name" must exist
email: str # required: a control named "email" must exist
message: NotRequired[str] # optional: no control required
If a required field is missing, creating the form fails fast with a message that lists the expected, missing, and matching fields:
class SignupForm(TypedDict):
username: str
email: str
class SignupState(rx.State):
@rx.event
def handle_submit(self, form_data: SignupForm): ...
# Raises EventHandlerValueError: the form has no control named "email".
rx.form(
rx.input(name="username"),
rx.button("Submit", type="submit"),
on_submit=SignupState.handle_submit,
)
# When is validation skipped?
The check only runs when the form fields are statically known. It is
automatically skipped when control `name`/`id` values are dynamic (for example,
built with `rx.foreach`), or when the form has an `id` (since controls can be
associated from elsewhere via the HTML `form` attribute). In those cases the
`TypedDict` still provides typed access inside the handler. At runtime
`form_data` is always a regular dictionary.
Forms can be dynamically created by iterating through state vars using rx.foreach.
This example allows the user to add new fields to the form prior to submit, and all
fields will be included in the form data passed to the handle_submit function.
class DynamicFormState(rx.State):
form_data: dict = {}
form_fields: list[str] = ["first_name", "last_name", "email"]
@rx.var(cache=True)
def form_field_placeholders(self) -> list[str]:
return [
" ".join(w.capitalize() for w in field.split("_"))
for field in self.form_fields
]
@rx.event
def add_field(self, form_data: dict):
new_field = form_data.get("new_field")
if not new_field:
return
field_name = new_field.strip().lower().replace(" ", "_")
self.form_fields.append(field_name)
@rx.event
def handle_submit(self, form_data: dict):
self.form_data = form_data
def dynamic_form():
return rx.vstack(
rx.form(
rx.vstack(
rx.foreach(
DynamicFormState.form_fields,
lambda field, idx: rx.input(
placeholder=DynamicFormState.form_field_placeholders[idx],
name=field,
),
),
rx.button("Submit", type="submit"),
),
on_submit=DynamicFormState.handle_submit,
reset_on_submit=True,
),
rx.form(
rx.hstack(
rx.input(placeholder="New Field", name="new_field"),
rx.button("+", type="submit"),
),
on_submit=DynamicFormState.add_field,
reset_on_submit=True,
),
rx.divider(),
rx.heading("Results"),
rx.text(DynamicFormState.form_data.to_string()),
)