Skip to content

Latest commit

ย 

History

History
439 lines (365 loc) ยท 14.6 KB

form.md

File metadata and controls

439 lines (365 loc) ยท 14.6 KB
components
rx.form
FormRoot lambda **props: rx.form.root( rx.form.field( rx.flex( rx.form.label("Email"), rx.form.control( rx.input( placeholder="Email Address", # type attribute is required for "typeMismatch" validation type="email", ), as_child=True, ), rx.form.message("Please enter a valid email"), rx.form.submit( rx.button("Submit"), as_child=True, ), direction="column", spacing="2", align="stretch", ), name="email", ), **props, )
FormField lambda **props: rx.form.root( rx.form.field( rx.flex( rx.form.label("Email"), rx.form.control( rx.input( placeholder="Email Address", # type attribute is required for "typeMismatch" validation type="email", ), as_child=True, ), rx.form.message("Please enter a valid email", match="typeMismatch"), rx.form.submit( rx.button("Submit"), as_child=True, ), direction="column", spacing="2", align="stretch", ), **props, ), reset_on_submit=True, )
FormLabel lambda **props: rx.form.root( rx.form.field( rx.flex( rx.form.label("Email", **props,), rx.form.control( rx.input( placeholder="Email Address", # type attribute is required for "typeMismatch" validation type="email", ), as_child=True, ), rx.form.message("Please enter a valid email", match="typeMismatch"), rx.form.submit( rx.button("Submit"), as_child=True, ), direction="column", spacing="2", align="stretch", ), ), reset_on_submit=True, )
FormMessage lambda **props: rx.form.root( rx.form.field( rx.flex( rx.form.label("Email"), rx.form.control( rx.input( placeholder="Email Address", # type attribute is required for "typeMismatch" validation type="email", ), as_child=True, ), rx.form.message("Please enter a valid email", **props,), rx.form.submit( rx.button("Submit"), as_child=True, ), direction="column", spacing="2", align="stretch", ), name="email", ), on_submit=lambda form_data: rx.window_alert(form_data.to_string()), reset_on_submit=True, )
import reflex as rx

Form

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.text_area, 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", as_="h2"),
        rx.text(FormState.form_data.to_string()),
    )
# When using the form you must include a button or input with `type='submit'`.
# Form data is keyed by `name`.

Only controls with a `name` are included in the form data; the `id` attribute does not add a field. Following standard HTML form behavior, controls like `rx.switch`, `rx.radio_group`, and `rx.checkbox` are only included when their values are set (e.g., if the checkbox is checked, the switch is toggled, or a radio option is selected), so read them with `form_data.get(...)`, or declare them as `bool` fields of a [TypedDict](#validating-form-data-with-a-typeddict).
# Video: Forms

Fields with the Same Name

Several controls can share a name, such as a group of checkboxes. A dict annotation keeps only the last value submitted for each name. To receive every value in the order the form submitted them, annotate the handler's parameter with rx.form.FormData and read them with getlist (or its alias getAll, as in the browser's FormData):

class ToppingsState(rx.State):
    toppings: list[str] = []

    @rx.event
    def handle_submit(self, form_data: rx.form.FormData[str, str]):
        self.toppings = form_data.getlist("topping")


def toppings_form():
    return rx.form(
        rx.el.input(type="checkbox", name="topping", value="cheese"),
        rx.el.input(type="checkbox", name="topping", value="olives"),
        rx.button("Submit", type="submit"),
        on_submit=ToppingsState.handle_submit,
    )

rx.form.FormData is a read-only mapping: indexing it with form_data["topping"] returns the last value, the same as a dict, and form_data.multi_items() returns every (name, value) pair. A TypedDict can also collect repeated names with a list[str] field.

# Dict fields ending in `[]` hold lists.

With a `dict` annotation, a field name ending in `[]`, such as the `range[]`
that a two-thumb `rx.slider(name="range")` submits, reads as a list of its
values, even when only one was submitted. `rx.form.FormData` treats it like any
other name: `form_data["range[]"]` is the last value and
`form_data.getlist("range[]")` gives them all.

A `TypedDict` instead lets you declare the field without brackets: a list field
`range: list[str]` collects the values submitted as `range[]`, unless the
`TypedDict` also declares a `range[]` field. A declared `name[]` field of any
other type is filled like any other field: a `bool` field is read as described
in [List and bool fields](#list-and-bool-fields), and other types get the last
value.

Validating Form Data with a TypedDict

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, 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", as_="h2"),
        rx.text(TypedFormState.form_data.to_string()),
    )

Required and optional fields

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

List and bool fields

Two kinds of TypedDict fields are filled in even when the form submits no value for them:

  • A list[str] field holds every value submitted under its name, in order, or an empty list when there are none.
  • A bool field is False when no value was submitted under its name, so an unchecked checkbox or switch reads as False instead of a missing key. A submitted value is True unless, ignoring case and surrounding whitespace, it is empty or "false", "off", "no", "n" or "0", so a select, radio group or hidden input submitting "false" reads as False.

When no value is submitted for a field whose type allows None, such as list[str] | None or bool | None, the field is None instead. A field marked NotRequired is left out when no value is submitted for it, whatever its type.

class PreferencesForm(TypedDict):
    toppings: list[str]  # every checked "toppings" checkbox
    subscribe: bool  # False when the checkbox is unchecked
    notify: bool | None  # None when the switch is off


class PreferencesState(rx.State):
    preferences: PreferencesForm | None = None

    @rx.event
    def handle_submit(self, form_data: PreferencesForm):
        self.preferences = form_data


def preferences_form():
    return rx.form(
        rx.el.input(type="checkbox", name="toppings", value="cheese"),
        rx.el.input(type="checkbox", name="toppings", value="olives"),
        rx.checkbox("Subscribe", name="subscribe"),
        rx.switch(name="notify"),
        rx.button("Submit", type="submit"),
        on_submit=PreferencesState.handle_submit,
    )

Other fields keep the last value submitted for their name.

Generic TypedDicts work too, such as class Data(Base[list[str]]). On Python 3.11, a further typing.TypedDict subclass of Data loses those type arguments and Reflex rejects the form when it compiles, so define these TypedDicts with typing_extensions.TypedDict instead.

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` 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.

Dynamic Forms

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_form_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_form_field,
            reset_on_submit=True,
        ),
        rx.divider(),
        rx.heading("Results", as_="h2"),
        rx.text(DynamicFormState.form_data.to_string()),
    )