R

ReflexKR

Reflex 官方文档

组件工坊:Reflex 组件库 100 例

·

课程概览

布局

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

排版

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

表单

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

数据展示

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

展开与切换

·

Tabs

·

Accordion

·

Segmented Control

浮层

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

动态渲染

·

Cond

·

Match

·

Foreach

·

Auto Scroll

媒体

·

Image

·

Audio

·

Video

表格与数据网格

·

Table

·

Data Table

·

Data Editor

图表

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

图表部件

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

其他图表库

·

Plotly

·

Pyplot

HTML 元素

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

其他工具

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

组件工坊:Reflex 组件库 100 例
表单

Form

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

官方文档

工坊订单

请做一个咖啡店点单表单。姓名、杯型、取餐方式、备注、礼品包装要一次收齐并提交。

rx.form 是把多个输入组合起来一次提交的框架。在里面放 rx.input、rx.checkbox、rx.switch、rx.slider、rx.text_area、rx.radio_group、rx.select 等输入,并给每个输入加上 name。提交时,on_submit 处理器会收到一个 {name: 值} 的 dict。

点击 type="submit" 的按钮,或在输入框里按 Enter,都会触发提交。必须要有提交按钮(或 type="submit" 的 input)。reset_on_submit=True 时,提交后会清空输入框。

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

未勾选的复选框和关闭的开关不会出现在结果里,打开时以 "on" 进入。原因见下面的“name 与 id”。

试着改属性

提交表单前,浏览器会先检查 required、type="email"、min·max 之类的条件。打开 no_validate 就会跳过这些检查,直接交给处理器。填入空值、abc 或 500,比较一下两种情况。

试着改属性

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,
)
no_validate=True 时不经浏览器检查就提交。这时必须在服务器(处理器)里自己检查。
属性取值默认值用途
on_submit事件提交时触发,传入表单数据 dict(或 TypedDict)
reset_on_submitTrue / FalseFalse提交后清空输入
no_validateTrue / FalseFalse关闭浏览器检查
as_childTrue / FalseFalse不渲染 form,改用子元素渲染
name, id字符串无表单名。有 id 时,别处的输入可以用 form="id" 关联进来
action, method, enc_type, targetHTML 表单属性。Reflex 会拦截提交,所以一般不用
handle_submit_unique_name字符串自动自己指定这个表单的提交函数名
on_clear_server_errors事件底层表单中服务器错误提示被清除时触发

name 与 id

表单数据从两处收集。一处是 name,即浏览器提交的值。复选框·开关·单选这类“只在选中时才有值”的输入,没选中时键就不会出现。所以要用 form_data.get(...) 读取。

另一处是 id。在 0.10 中,带 id 的输入也会以这个 id 为键放入数据,而且没选中也有值,复选框会变成 True/False。原生的 input·select·textarea 会自动这样收集,自己写的输入组件则需要在类里设置 _is_form_control = True。不过官方文档预告这种 id 方式将在 0.11 中移除。今后只用 name 更安全。

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

同名的多个输入

像一组复选框那样多个输入共用同一个 name 时,dict 里只留下最后一个值。官方文档(最新版)把处理器参数写成 rx.form.FormData,并演示用 getlist 取回全部值。但安装的 0.10 里没有 rx.form.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,
    )

在 0.10 中,给每个输入起不同的名字(topping_cheese …),再在处理器里按前缀挑出来汇总即可。

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

用 TypedDict 检查表单数据

把处理器参数从 dict 换成 TypedDict,编辑器就能补全键名,Reflex 也会在构建表单时进行检查。TypedDict 的每个必需键都必须在表单里有同名 name(或 id)的输入,缺一个就会在应用启动前抛出 EventHandlerValueError。可有可无的键用 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),
)

像下面这样漏掉 email 输入,错误信息会列出期望的键、缺失的键和找到的键。只有当 name 是固定字符串时才会检查;用 rx.foreach 生成的动态名字,或表单上有 id 时,会跳过检查。运行时的 form_data 始终是普通 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,
)

官方文档(最新版)说明,TypedDict 的 list[str] 字段会收集同名的所有值,bool 字段会把未勾选的复选框填成 False。0.10 没有这种填充。在 0.10 中 TypedDict 只做上面说的“构建时检查”,未勾选的复选框依然没有键,所以要用 NotRequired 和 .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

动态表单

用 rx.foreach 遍历 State 中的列表来生成输入,就成了用户可以自行增加输入框的表单。新增的输入框也都会进入提交数据。这里限制最多只能增加到 8 个。两个表单并排放置,下面的表单只负责添加输入框。

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

底层表单:field · label · control · message · submit

官方文档说明底层表单还处于实验阶段,正式服务推荐使用上面的高层表单。底层表单的优点是能把校验信息自动接在输入旁边。结构如下。

form.root(
    form.field(
        form.label(...),
        form.control(...),
        form.message(...),
    ),
    form.submit(...),
)
  • rx.form.root:整个表单。on_submit、reset_on_submit 与高层表单相同。
  • rx.form.field:一个输入及其标签·信息的组合。name 会成为内部输入的名字,server_invalid 用来标记服务器端校验失败。
  • rx.form.label:标签。rx.form.control:输入的位置。要用其他组件就用 as_child=True 包起来。在 0.10 中 control 里只能放 rx.input(复选框、select 等放在 field 外面或直接放在 root 下)。
  • rx.form.message:校验信息。用 match 指定在哪种失败时显示("valueMissing"、"typeMismatch"、"patternMismatch"、"tooShort"、"rangeOverflow" …)。force_match 用于服务器端校验。
  • rx.form.submit:提交按钮。要用其他按钮就设 as_child=True。

浏览器校验(客户端)

浏览器检查 type="email" 和 required,并显示对应 match 的信息。校验失败时,点提交按钮也不会提交。0.10 的 rx.form.message 文字颜色固定为白色,在浅色界面上看不见,所以示例里另外给了 color。

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

服务器校验

服务器校验用 State 的计算变量来做。把表示“无效”的 bool 计算变量同时接到 field 的 server_invalid 和 message 的 force_match 上。force_match 只在有 match 时才生效(0.10 在没有 match 时会去掉 force_match,而没有 match 的信息会一直显示)。所以要给一个文本输入不会自然产生的 match="badInput",显示与否交给 force_match 决定。在下面输入 admin 试试,会出现提示信息,按钮也会被禁用。

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

文件另行处理

表单的 on_submit 里不包含上传的文件。如果表单里有 Upload,文件要通过 type="button" 的独立按钮或 on_drop 调用 rx.upload_files() 发送,其余输入再用 on_submit 接收。详见 Upload 页面。

完成订单

给五种输入分别加上 name,用一个处理器接收。select 设了 required,选择之前无法提交;未勾选的复选框没有键,所以用 .get("gift") 读取,再转成 "yes"/"no"。

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

工坊笔记

  • rx.form(..., on_submit=处理器),里面的每个输入都要有 name。处理器收到 {name: 值} dict。

  • 必须有 type="submit" 按钮。reset_on_submit=True 时提交后清空。

  • 未选中的复选框·开关·单选没有键,用 form_data.get() 读取。id 方式将在 0.11 中移除。

  • 把处理器参数写成 TypedDict,构建表单时就能发现缺失的输入。

  • 想把校验信息接在输入旁边,用底层 field · control · message(match、force_match + server_invalid)。

← 上一课

表单 · Slider

下一课 →

表单 · Upload

订阅新课时和新课程通知

本课程新增课时或推出后续课程时,我们会发邮件通知你。

收集项目:电子邮箱 · 用途:本课程的新课时和上线通知 · 保存期限:直到你要求删除。不会用于其他用途,也不会提供给他人。回复任意通知邮件,我们会立即删除。

问答

关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。

以纯文本显示,不支持链接和格式。

R

ReflexKR

动手学 Python 和 Reflex。所有示例均已在 Reflex 0.10.0 上实际运行验证。

ReflexKR 免费,没有横幅广告。如果对你有帮助,欢迎请我们喝杯咖啡。 ☕ 请我们喝杯咖啡

联盟链接与赞助说明
한국어English日本語简体中文EspañolDeutsch

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

Built with Reflex