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
rx.formrx.form.rootrx.form.fieldrx.form.labelrx.form.controlrx.form.messagerx.form.submitOffizielle 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“.
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
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,
)| Prop | Werte | Standard | Verwendung |
|---|---|---|---|
on_submit | Event | Beim Absenden. Übergibt das Formulardaten-dict (oder TypedDict) | |
reset_on_submit | True / False | False | Leert die Felder nach dem Absenden |
no_validate | True / False | False | Schaltet die Browserprüfung ab |
as_child | True / False | False | Rendert als Kindelement statt als Formular |
name, id | String | keiner | Name des Formulars. Mit id können sich Eingaben an anderer Stelle per form="id" anschließen |
action, method, enc_type, target | HTML-Formularattribute. Reflex fängt das Absenden ab, daher meist ungenutzt | ||
handle_submit_unique_name | String | automatisch | Um den Namen der Absendefunktion dieses Formulars selbst festzulegen |
on_clear_server_errors | Event | Wenn im Low-Level-Formular die Anzeige von Serverfehlern gelöscht wird |
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.
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,
)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,
)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 offErzeugst 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())),
)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.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"),
)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"),
)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.
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).
Über neue Lektionen informiert werden
Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.
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© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Fragen & Antworten
Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.