组件工坊: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
工坊订单
请做一个咖啡店点单表单。姓名、杯型、取餐方式、备注、礼品包装要一次收齐并提交。
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,比较一下两种情况。
试着改属性
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,
)| 属性 | 取值 | 默认值 | 用途 |
|---|---|---|---|
on_submit | 事件 | 提交时触发,传入表单数据 dict(或 TypedDict) | |
reset_on_submit | True / False | False | 提交后清空输入 |
no_validate | True / False | False | 关闭浏览器检查 |
as_child | True / False | False | 不渲染 form,改用子元素渲染 |
name, id | 字符串 | 无 | 表单名。有 id 时,别处的输入可以用 form="id" 关联进来 |
action, method, enc_type, target | HTML 表单属性。Reflex 会拦截提交,所以一般不用 | ||
handle_submit_unique_name | 字符串 | 自动 | 自己指定这个表单的提交函数名 |
on_clear_server_errors | 事件 | 底层表单中服务器错误提示被清除时触发 |
表单数据从两处收集。一处是 name,即浏览器提交的值。复选框·开关·单选这类“只在选中时才有值”的输入,没选中时键就不会出现。所以要用 form_data.get(...) 读取。
另一处是 id。在 0.10 中,带 id 的输入也会以这个 id 为键放入数据,而且没选中也有值,复选框会变成 True/False。原生的 input·select·textarea 会自动这样收集,自己写的输入组件则需要在类里设置 _is_form_control = True。不过官方文档预告这种 id 方式将在 0.11 中移除。今后只用 name 更安全。
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,
)把处理器参数从 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())),
)官方文档说明底层表单还处于实验阶段,正式服务推荐使用上面的高层表单。底层表单的优点是能把校验信息自动接在输入旁边。结构如下。
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)。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。