R

ReflexKR

Reflex-Dokumentation

Die Komponentenwerkstatt: Reflex' 100 Bibliotheksbausteine

·

Kursüberblick

Layout

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Typografie

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Formulare

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Datenanzeige

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Auf- und Zuklappen

·

Tabs

·

Accordion

·

Segmented Control

Overlays

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Dynamisches Rendern

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Medien

·

Image

·

Audio

·

Video

Tabellen und Datengrids

·

Table

·

Data Table

·

Data Editor

Diagramme

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Diagrammbausteine

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Andere Diagrammbibliotheken

·

Plotly

·

Pyplot

HTML-Elemente

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Weitere Werkzeuge

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

Die Komponentenwerkstatt: Reflex' 100 Bibliotheksbausteine
Formulare

Form

rx.formrx.form.rootrx.form.fieldrx.form.labelrx.form.controlrx.form.messagerx.form.submit

Offizielle Doku

Werkstattauftrag

Bitte baue ein Bestellformular für ein Café. Name, Bechergröße, Abholart, Sonderwünsche und Geschenkverpackung sollen in einem Rutsch aufgenommen werden.

rx.form ist der Rahmen, der mehrere Eingaben bündelt und in einem Rutsch absendet. Hinein kommen Eingaben wie rx.input, rx.checkbox, rx.switch, rx.slider, rx.text_area, rx.radio_group oder rx.select, jeweils mit einem name. Beim Absenden bekommt der on_submit-Handler ein einziges dict {name: Wert}.

Abgesendet wird, wenn man einen Button mit type="submit" drückt oder in einem Eingabefeld Enter drückt. Ein Absende-Button (oder eine Eingabe mit type="submit") muss vorhanden sein. Mit reset_on_submit=True werden die Felder nach dem Absenden geleert.

Results: {}

class FormBasicState(rx.State):
    form_data: dict = {}

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


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.text(rx.flex(rx.switch(name="switch"), "Switched", spacing="2"), as_="label", size="2"),
                spacing="4",
            ),
            rx.button("Submit", type="submit"),
        ),
        on_submit=FormBasicState.handle_submit,
        reset_on_submit=True,
    ),
    rx.divider(),
    rx.text("Results: ", rx.code(FormBasicState.form_data.to_string())),
)

Ausgeschaltete Checkboxen und Switches fehlen im Ergebnis, eingeschaltete kommen als "on" an. Warum, steht unten unter „name und id“.

Props ausprobieren

Vor dem Absenden prüft der Browser Bedingungen wie required, type="email" oder min·max. Mit no_validate wird diese Prüfung übersprungen und alles direkt an den Handler geschickt. Trag ein leeres Feld, abc oder 500 ein und vergleiche beide Fälle.

Props ausprobieren

Submitted: -

required

no_validate

rx.form(
    rx.vstack(
        rx.input(
            name="email",
            type="email",
            placeholder="Email",
            required=True,
        ),
        rx.input(name="age", type="number", min=1, max=120, placeholder="Age (1-120)"),
        rx.button("Submit", type="submit"),
        rx.text("Submitted: ", rx.code(FormLabState.result), size="2"),
    ),
    on_submit=FormLabState.handle_submit,
    no_validate=False,
)
Mit no_validate=True wird ohne Browserprüfung abgesendet. Dann musst du auf dem Server (im Handler) selbst prüfen.
PropWerteStandardVerwendung
on_submitEventBeim Absenden. Übergibt das Formulardaten-dict (oder TypedDict)
reset_on_submitTrue / FalseFalseLeert die Felder nach dem Absenden
no_validateTrue / FalseFalseSchaltet die Browserprüfung ab
as_childTrue / FalseFalseRendert als Kindelement statt als Formular
name, idStringkeinerName des Formulars. Mit id können sich Eingaben an anderer Stelle per form="id" anschließen
action, method, enc_type, targetHTML-Formularattribute. Reflex fängt das Absenden ab, daher meist ungenutzt
handle_submit_unique_nameStringautomatischUm den Namen der Absendefunktion dieses Formulars selbst festzulegen
on_clear_server_errorsEventWenn im Low-Level-Formular die Anzeige von Serverfehlern gelöscht wird

name und id

Formulardaten werden aus zwei Quellen gesammelt. Die erste ist name, also das, was der Browser absendet. Eingaben, die „nur im eingeschalteten Zustand einen Wert haben“, wie Checkbox, Switch oder Radio, fehlen als Schlüssel, wenn sie nicht eingeschaltet sind. Deshalb liest du sie mit form_data.get(...).

Die zweite ist id. In 0.10 landen auch Eingaben mit id unter dieser id als Schlüssel in den Daten, und zwar auch dann, wenn sie nicht eingeschaltet sind. Checkboxen werden zu True/False. Die Standardelemente input·select·textarea werden automatisch so gesammelt; selbstgebaute Eingabekomponenten brauchen in ihrer Klasse _is_form_control = True. Die offizielle Doku kündigt allerdings an, dass diese id-Variante in 0.11 wegfällt. Künftig bist du mit name allein auf der sicheren Seite.

Results: {}

class FormNameIdState(rx.State):
    form_data: dict = {}

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


rx.form(
    rx.vstack(
        rx.checkbox("By name", name="by_name"),
        rx.checkbox("By id", id="form_by_id_box"),
        rx.button("Submit", type="submit"),
        rx.text("Results: ", rx.code(FormNameIdState.form_data.to_string())),
    ),
    on_submit=FormNameIdState.handle_submit,
)

Mehrere Eingaben mit gleichem Namen

Nutzen mehrere Eingaben denselben name, etwa eine Gruppe von Checkboxen, bleibt im dict nur der letzte Wert übrig. Die offizielle Doku (neueste Fassung) typisiert das Handler-Argument als rx.form.FormData und zeigt, wie man mit getlist alle Werte erhält. Im installierten 0.10 gibt es rx.form.FormData aber nicht, deshalb steht der Code unten nur zur Ansicht.

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,
    )

In 0.10 gibst du jeder Eingabe einen eigenen Namen (topping_cheese …) und sammelst sie im Handler anhand des Präfixes ein.

FORM_TOPPINGS = ["cheese", "olives", "mushrooms"]


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

    @rx.event
    def handle_submit(self, form_data: dict):
        self.toppings = [k.removeprefix("topping_") for k in form_data if k.startswith("topping_")]


rx.form(
    rx.vstack(
        rx.hstack(*[rx.checkbox(t, name="topping_" + t) for t in FORM_TOPPINGS], spacing="4"),
        rx.button("Submit", type="submit"),
        rx.hstack(rx.foreach(FormToppingState.toppings, lambda t: rx.badge(t))),
    ),
    on_submit=FormToppingState.handle_submit,
)

Formulardaten mit TypedDict prüfen

Typisierst du das Handler-Argument als TypedDict statt dict, vervollständigt der Editor die Schlüssel, und Reflex prüft beim Erzeugen des Formulars. Für jeden Pflichtschlüssel des TypedDict muss das Formular eine Eingabe mit gleichem name (oder id) enthalten; fehlt auch nur eine, gibt es schon vor dem Start der App einen EventHandlerValueError. Optionale Schlüssel markierst du mit NotRequired.

from typing import TypedDict

from typing_extensions import NotRequired


class FormContactData(TypedDict):
    first_name: str
    last_name: str
    email: str
    message: NotRequired[str]


class FormTypedState(rx.State):
    greeting: str = ""

    @rx.event
    def handle_submit(self, form_data: FormContactData):
        self.greeting = f"Hi {form_data['first_name']} {form_data['last_name']} <{form_data['email']}>"


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=FormTypedState.handle_submit,
        reset_on_submit=True,
    ),
    rx.text(FormTypedState.greeting),
)

Lässt du wie unten die Eingabe email weg, nennt die Fehlermeldung erwartete, fehlende und gefundene Schlüssel. Geprüft wird nur, wenn name ein fester String ist; dynamische Namen aus rx.foreach oder ein Formular mit id werden übersprungen. Zur Laufzeit ist form_data immer ein gewöhnliches dict.

class SignupForm(TypedDict):
    username: str
    email: str


class SignupState(rx.State):
    @rx.event
    def handle_submit(self, form_data: SignupForm): ...


# EventHandlerValueError: the form has no control named "email".
rx.form(
    rx.input(name="username"),
    rx.button("Submit", type="submit"),
    on_submit=SignupState.handle_submit,
)

Die offizielle Doku (neueste Fassung) erklärt, dass ein list[str]-Feld im TypedDict alle Werte gleichen Namens sammelt und ein bool-Feld ausgeschaltete Checkboxen mit False auffüllt. Dieses Auffüllen gibt es in 0.10 nicht. Dort macht TypedDict nur die obige „Prüfung beim Erzeugen“, und ausgeschaltete Checkboxen fehlen weiterhin als Schlüssel, also behandelst du sie mit NotRequired und .get().

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

Dynamische Formulare

Erzeugst du Eingaben per rx.foreach aus einer State-Liste, entsteht ein Formular, in dem Nutzer weitere Felder hinzufügen können. Auch hinzugefügte Felder landen in den gesendeten Daten. Hier ist die Zahl der Felder auf 8 begrenzt. Es stehen zwei Formulare nebeneinander; das untere ist nur fürs Hinzufügen von Feldern zuständig.

Results: {}

class FormDynamicState(rx.State):
    form_data: dict = {}
    form_fields: list[str] = ["first_name", "last_name", "email"]

    @rx.var
    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 or len(self.form_fields) >= 8:
            return
        field_name = new_field.strip().lower().replace(" ", "_")
        if field_name and field_name not in self.form_fields:
            self.form_fields.append(field_name)

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


rx.vstack(
    rx.form(
        rx.vstack(
            rx.foreach(
                FormDynamicState.form_fields,
                lambda field, idx: rx.input(
                    placeholder=FormDynamicState.form_field_placeholders[idx],
                    name=field,
                ),
            ),
            rx.button("Submit", type="submit"),
        ),
        on_submit=FormDynamicState.handle_submit,
        reset_on_submit=True,
    ),
    rx.form(
        rx.hstack(
            rx.input(placeholder="New Field", name="new_field"),
            rx.button("+", type="submit", variant="soft"),
        ),
        on_submit=FormDynamicState.add_form_field,
        reset_on_submit=True,
    ),
    rx.divider(),
    rx.text("Results: ", rx.code(FormDynamicState.form_data.to_string())),
)

Low-Level-Formular: field · label · control · message · submit

Die offizielle Doku bezeichnet das Low-Level-Formular als experimentell und empfiehlt für den Produktiveinsatz das High-Level-Formular von oben. Sein Vorteil: Prüfmeldungen werden automatisch mit der jeweiligen Eingabe verknüpft. Der Aufbau sieht so aus.

form.root(
    form.field(
        form.label(...),
        form.control(...),
        form.message(...),
    ),
    form.submit(...),
)
  • rx.form.root: das ganze Formular. on_submit und reset_on_submit wie beim High-Level-Formular.
  • rx.form.field: eine Eingabe samt Label und Meldung. name wird zum Namen der inneren Eingabe, und server_invalid markiert eine fehlgeschlagene Serverprüfung.
  • rx.form.label: das Label. rx.form.control: der Platz für die Eingabe. Für andere Komponenten umschließt du sie mit as_child=True. In 0.10 passt nur rx.input in ein control (Checkboxen, Selects usw. kommen außerhalb des field oder direkt unter root).
  • rx.form.message: die Prüfmeldung. Mit match legst du fest, bei welchem Fehler sie erscheint ("valueMissing", "typeMismatch", "patternMismatch", "tooShort", "rangeOverflow" …). force_match dient der Serverprüfung.
  • rx.form.submit: der Absende-Button. Für einen anderen Button as_child=True.

Prüfung im Browser (Client-Seite)

type="email" und required prüft der Browser, und die Meldung mit dem passenden match erscheint. Schlägt die Prüfung fehl, wird trotz Klick auf den Absende-Button nichts abgeschickt. In 0.10 hat rx.form.message fest weiße Schrift und ist auf hellem Hintergrund unsichtbar, daher setzt das Beispiel color eigens.

Submitted:

class FormLowState(rx.State):
    email: str = ""

    @rx.event
    def handle_submit(self, form_data: dict):
        self.email = form_data.get("email", "")


rx.vstack(
    rx.form.root(
        rx.form.field(
            rx.flex(
                rx.form.label("Email"),
                rx.form.control(
                    rx.input(placeholder="Email Address", type="email", required=True),
                    as_child=True,
                ),
                rx.form.message("Please enter your email", match="valueMissing", color=rx.color("red", 11)),
                rx.form.message("Please enter a valid email", match="typeMismatch", color=rx.color("red", 11)),
                rx.form.submit(rx.button("Submit"), as_child=True),
                direction="column",
                spacing="2",
                align="stretch",
            ),
            name="email",
        ),
        on_submit=FormLowState.handle_submit,
        reset_on_submit=True,
        width="280px",
    ),
    rx.text("Submitted: ", rx.code(FormLowState.email), size="2"),
)

Prüfung auf dem Server

Die Serverprüfung läuft über eine berechnete Variable im State. Eine bool-Variable, die „ungültig“ bedeutet, verbindest du sowohl mit server_invalid des field als auch mit force_match der message. force_match greift nur, wenn match gesetzt ist (0.10 lässt force_match ohne match weg, und eine Meldung ohne match ist immer sichtbar). Deshalb setzt du match="badInput", das bei Texteingaben nie von selbst auftritt, und lässt force_match über die Sichtbarkeit entscheiden. Gib unten admin ein: Die Meldung erscheint, und der Button ist gesperrt.

Signed up:

class FormServerCheckState(rx.State):
    username: str = ""
    submitted: str = ""

    @rx.event
    def set_username(self, value: str):
        self.username = value

    @rx.var
    def username_taken(self) -> bool:
        return self.username.strip().lower() in ["admin", "root", "reflex"]

    @rx.event
    def handle_submit(self, form_data: dict):
        if not self.username_taken:
            self.submitted = form_data.get("username", "")
            self.username = ""


rx.vstack(
    rx.form.root(
        rx.form.field(
            rx.flex(
                rx.form.label("Username (try 'admin')"),
                rx.form.control(
                    rx.input(
                        placeholder="Username",
                        value=FormServerCheckState.username,
                        on_change=FormServerCheckState.set_username,
                        required=True,
                    ),
                    as_child=True,
                ),
                rx.form.message(
                    "This name is taken",
                    match="badInput",
                    force_match=FormServerCheckState.username_taken,
                    color=rx.color("red", 11),
                ),
                rx.form.submit(rx.button("Sign up", disabled=FormServerCheckState.username_taken), as_child=True),
                direction="column",
                spacing="2",
                align="stretch",
            ),
            name="username",
            server_invalid=FormServerCheckState.username_taken,
        ),
        on_submit=FormServerCheckState.handle_submit,
        width="280px",
    ),
    rx.text("Signed up: ", rx.code(FormServerCheckState.submitted), size="2"),
)

Dateien laufen separat

Hochgeladene Dateien landen nicht im on_submit des Formulars. Steckt ein Upload im Formular, schickst du die Dateien über einen eigenen Button mit type="button" oder per on_drop mit rx.upload_files() und nimmst nur die übrigen Felder per on_submit entgegen. Mehr dazu auf der Seite Upload.

Auftrag erledigt

Fünf verschiedene Eingaben bekommen je einen name und werden von einem einzigen Handler empfangen. Das Select ist required, also wird erst nach der Auswahl abgeschickt, und weil eine ausgeschaltete Checkbox als Schlüssel fehlt, wird sie mit .get("gift") gelesen und in "yes"/"no" umgewandelt.

class FormOrderState(rx.State):
    last_order: dict = {}

    @rx.event
    def place_order(self, form_data: dict):
        self.last_order = {
            "name": form_data.get("name", ""),
            "size": form_data.get("size", ""),
            "pickup": form_data.get("pickup", ""),
            "gift": "yes" if form_data.get("gift") else "no",
            "note": form_data.get("note", "")[:100],
        }


rx.card(
    rx.form(
        rx.vstack(
            rx.input(name="name", placeholder="Your name", required=True),
            rx.select(["Small", "Medium", "Large"], name="size", placeholder="Cup size", required=True),
            rx.radio_group(["Store", "Delivery"], name="pickup", default_value="Store"),
            rx.text_area(name="note", placeholder="Note (optional)", max_length=100, rows="2"),
            rx.checkbox("Gift wrap", name="gift"),
            rx.button("Place order", type="submit"),
            rx.cond(
                FormOrderState.last_order,
                rx.code_block(FormOrderState.last_order.to_string(), language="json", width="100%"),
            ),
            align="stretch",
        ),
        on_submit=FormOrderState.place_order,
        reset_on_submit=True,
    ),
    width="100%",
    max_width="360px",
)

Werkstattnotizen

  • rx.form(..., on_submit=handler) + name für jede Eingabe darin. Der Handler bekommt ein dict {name: Wert}.

  • Ein Button mit type="submit" ist Pflicht. Mit reset_on_submit=True wird nach dem Absenden geleert.

  • Ausgeschaltete Checkboxen · Switches · Radios fehlen als Schlüssel; mit form_data.get() lesen. Die id-Variante fällt in 0.11 weg.

  • Typisierst du das Handler-Argument als TypedDict, werden fehlende Felder schon beim Erzeugen des Formulars erkannt.

  • Prüfmeldungen direkt an der Eingabe gibt es mit dem Low-Level-field · control · message (match, force_match + server_invalid).

← Zurück

Formulare · Slider

Weiter →

Formulare · Upload

Über neue Lektionen informiert werden

Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.

Erhoben: deine E-Mail-Adresse · Zweck: Hinweise auf neue Lektionen oder den Start dieses Kurses · Speicherdauer: bis du die Löschung verlangst. Keine andere Nutzung, keine Weitergabe. Antworte einfach auf eine Benachrichtigung, dann löschen wir sie sofort.

Fragen & Antworten

Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.

Wird als reiner Text angezeigt, ohne Links oder Formatierung.

R

ReflexKR

Python und Reflex zum Anfassen. Alle Beispiele wurden mit Reflex 0.10.0 ausgeführt und geprüft.

ReflexKR ist kostenlos und ohne Werbebanner. Wenn es dir geholfen hat, kannst du es mit einem Kaffee unterstützen. ☕ Spendier uns einen Kaffee

Hinweis zu Affiliate-Links
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex