第八章:表单处理与用户输入
博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)
第八章:表单处理与用户输入
1. 受控组件
在 Dioxus 中,表单控件的值与 Signal 绑定,通过事件同步更新:
#[component]
fn LoginForm() -> Element {
// Signal 是唯一数据源
let username = use_signal(String::new);
let password = use_signal(String::new);
rsx! {
form { class: "space-y-4",
div { class: "mb-4",
label { class: "block text-sm mb-1", "用户名" }
input {
class: "w-full border rounded-lg px-3 py-2",
value: "{username}", // 显示 Signal 的值
oninput: move |e| username.set(e.value()), // 用户输入时更新
placeholder: "请输入用户名",
}
}
div { class: "mb-4",
label { class: "block text-sm mb-1", "密码" }
input {
r#type: "password",
class: "w-full border rounded-lg px-3 py-2",
value: "{password}",
oninput: move |e| password.set(e.value()),
}
}
}
}
}
2. 输入事件
2.1 常见事件
// oninput —— 输入时触发(推荐)
input {
oninput: move |e| println!("输入: {}", e.value()),
}
// onchange —— 失焦且值变化时触发
input {
onchange: move |e| println!("最终值: {}", e.value()),
}
// onkeydown —— 按键
input {
onkeydown: move |e| {
if e.key() == Key::Enter {
println!("按下了回车");
}
if e.key() == Key::Escape {
println!("按下了 Escape");
}
},
}
2.2 不同类型输入
#[component]
fn AllInputs() -> Element {
let text = use_signal(String::new);
let number = use_signal(|| 0u32);
let checked = use_signal(|| false);
let selected = use_signal(|| "rust".to_string());
rsx! {
// 文本
input { value: "{text}", oninput: move |e| text.set(e.value()) }
// 数字
input {
r#type: "number",
value: "{number}",
oninput: move |e| {
if let Ok(n) = e.value().parse::<u32>() {
number.set(n);
}
},
}
// 复选框
label {
input {
r#type: "checkbox",
checked: "{checked}",
oninput: move |e| checked.set(e.checked()),
}
"同意条款"
}
// 下拉选择
select {
value: "{selected}",
oninput: move |e| selected.set(e.value()),
option { value: "rust", "Rust" }
option { value: "python", "Python" }
option { value: "js", "JavaScript" }
}
// 文本域
textarea {
value: "{text}",
oninput: move |e| text.set(e.value()),
}
}
}
3. 表单提交
3.1 基础提交
#[component]
fn RegisterForm() -> Element {
let username = use_signal(String::new);
let email = use_signal(String::new);
let submitting = use_signal(|| false);
let handle_submit = move |_| {
if submitting() { return; }
submitting.set(true);
// 异步提交
spawn(async move {
let payload = serde_json::json!({
"username": username(),
"email": email(),
});
// 调用 API...
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
submitting.set(false);
});
};
rsx! {
form {
class: "max-w-md mx-auto",
onsubmit: handle_submit,
// 表单字段...
button {
class: "w-full py-2 rounded-lg text-white font-medium",
style: "background: var(--primary);",
disabled: submitting(),
if submitting() { "提交中..." } else { "注册" }
}
}
}
}
3.2 阻止默认提交
rsx! {
form {
onsubmit: move |e| {
e.prevent_default(); // 阻止浏览器默认提交
// 处理表单数据...
},
// ...
}
}
4. 表单验证
4.1 实时验证
#[component]
fn ValidatedInput() -> Element {
let email = use_signal(String::new);
// 派生验证状态
let email_valid = use_memo(move || {
let val = email();
if val.is_empty() {
None // 未输入
} else if val.contains('@') && val.contains('.') {
Some(true)
} else {
Some(false)
}
});
rsx! {
div { class: "mb-4",
input {
class: "w-full border rounded-lg px-3 py-2
{match email_valid() {
Some(false) => \"border-red-500\",
_ => \"border-gray-300\",
}}",
value: "{email}",
oninput: move |e| email.set(e.value()),
}
match email_valid() {
Some(false) => rsx! {
p { class: "text-red-500 text-xs mt-1", "邮箱格式不正确" }
},
Some(true) => rsx! {
p { class: "text-green-500 text-xs mt-1", "✓ 邮箱格式正确" }
},
None => rsx! { div {} },
}
}
}
}
4.2 验证函数
fn validate_username(name: &str) -> Result<(), &'static str> {
if name.len() < 3 { return Err("用户名至少3个字符"); }
if name.len() > 20 { return Err("用户名不超过20个字符"); }
if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err("只能包含字母、数字和下划线");
}
Ok(())
}
fn validate_email(email: &str) -> Result<(), &'static str> {
if !email.contains('@') { return Err("邮箱必须包含 @"); }
if !email.contains('.') { return Err("邮箱格式不正确"); }
Ok(())
}
5. 表单组组件
#[component]
fn FormField(
label: String,
error: Option<String>,
children: Element,
) -> Element {
rsx! {
div { class: "mb-4",
label { class: "block text-sm font-medium mb-1",
style: "color: var(--text);",
"{label}"
}
{children}
if let Some(ref msg) = error {
p { class: "text-red-500 text-xs mt-1",
role: "alert",
"{msg}"
}
}
}
}
}
// 使用
FormField {
label: "邮箱".to_string(),
error: email_error(),
input {
class: "w-full border rounded-lg px-3 py-2",
value: "{email}",
oninput: move |e| email.set(e.value()),
}
}
6. 搜索输入框
#[component]
fn SearchInput(on_search: EventHandler<String>) -> Element {
let value = use_signal(String::new);
rsx! {
div { class: "relative",
input {
class: "w-full border rounded-lg pl-10 pr-4 py-2",
value: "{value}",
oninput: move |e| value.set(e.value()),
onkeydown: move |e| {
if e.key() == Key::Enter {
on_search.call(value());
}
},
placeholder: "搜索文章...",
}
// 搜索图标
svg { class: "absolute left-3 top-2.5 w-4 h-4 text-gray-400", ... }
// 清除按钮
if !value().is_empty() {
button {
class: "absolute right-3 top-2.5 text-gray-400",
onclick: move |_| value.set(String::new()),
"×"
}
}
}
}
}
7. 实战:评论表单
#[component]
fn CommentForm(article_slug: String) -> Element {
let content = use_signal(String::new);
let submitting = use_signal(|| false);
let error = use_signal(|| Option::<String>::None);
let success = use_signal(|| false);
let submit = move |_| {
if content().trim().is_empty() {
error.set(Some("评论内容不能为空".to_string()));
return;
}
error.set(None);
submitting.set(true);
let slug = article_slug.clone();
let body = content();
spawn(async move {
match post_comment(&slug, &body).await {
Ok(_) => {
content.set(String::new());
success.set(true);
}
Err(e) => error.set(Some(e)),
}
submitting.set(false);
});
};
rsx! {
form { class: "space-y-3", onsubmit: submit,
h3 { class: "font-semibold", "发表评论" }
if success() {
div { class: "bg-green-100 text-green-700 px-4 py-2 rounded text-sm",
"评论发表成功!"
}
}
if let Some(ref msg) = error() {
div { class: "bg-red-100 text-red-700 px-4 py-2 rounded text-sm",
"{msg}"
}
}
textarea {
class: "w-full border rounded-lg p-3 h-24 resize-none",
value: "{content}",
oninput: move |e| content.set(e.value()),
placeholder: "写下你的评论...",
}
div { class: "flex items-center justify-between",
span { class: "text-xs",
style: "color: var(--tertiary);",
"{content().len()} / 500"
}
button {
class: "px-4 py-2 rounded-lg text-white text-sm",
style: "background: var(--primary);",
disabled: submitting() || content().len() > 500,
if submitting() { "提交中..." } else { "发表评论" }
}
}
}
}
}
8. 小结
- 使用
value+oninput实现受控组件模式 - 不同类型输入(text, number, checkbox, select, textarea)各有对应事件
onsubmit处理表单提交,prevent_default()阻止浏览器默认行为use_memo派生验证状态,实时显示错误提示- 封装
FormField组件复用标签+错误提示结构 - 提交时禁用按钮、显示加载状态,提升用户体验
- 下一章将学习异步编程与数据获取
dioxusforminputeventvalidation