输入与变更事件
表单处理是前端开发中最常见的任务之一。euv 通过 oninput 和 onchange 事件提供了简洁的用户输入响应模式,使构建实时响应用户操作的交互式表单变得轻而易举。
理解 Input 和 Change 事件
在深入代码之前,了解 oninput 和 onchange 之间的区别很重要:
- oninput — 每当输入元素的值发生变化时触发(每次按键、粘贴等)
- onchange — 当值发生变化且元素失去焦点时触发(或用户确认更改时,例如按 Enter)
在 euv 中,这两种事件都作为 html! 宏中的属性指定:
use euv::*;
fn app() -> VirtualNode {
html! {
div {
input {
oninput: move || {
// 每次按键都触发
},
onchange: move || {
// 失去焦点或按 Enter 时触发
}
}
}
}
}
mount("#app", app);
文本输入处理
基础文本输入
最常见的用例是从输入框捕获文本。使用响应式信号存储当前值:
use euv::*;
fn app() -> VirtualNode {
let name: Signal<String> = use_signal(|| String::from(""));
html! {
div {
label { "Name: " }
input {
oninput: move || {
name.set(String::from("Alice"));
}
}
p { "Hello, " {name} "!" }
}
}
}
mount("#app", app);
在这里,name 信号存储用户输入的内容。段落中的 {name} 表达式会自动订阅该信号,因此问候语会实时更新。
文本区域
文本区域的工作方式与文本输入相同:
use euv::*;
fn app() -> VirtualNode {
let bio: Signal<String> = use_signal(|| String::from(""));
html! {
div {
label { "Bio: " }
textarea {
oninput: move || {
bio.set(String::from("A Rust developer"));
}
}
p { "Bio: " {bio} }
}
}
}
mount("#app", app);
复选框处理
复选框表示布尔状态。使用 Signal<bool> 追踪复选框是否被选中:
use euv::*;
fn app() -> VirtualNode {
let agreed: Signal<bool> = use_signal(|| false);
html! {
div {
label {
input {
type: "checkbox",
onchange: move || {
agreed.set(!agreed.get());
}
}
" I agree to the terms"
}
p { if agreed.get() { "Thank you!" } else { "Please agree to continue." } }
}
}
}
mount("#app", app);
对于复选框,推荐使用 onchange 事件,因为它在复选框状态切换时触发,而不是在每次交互时触发。
下拉菜单处理
对于下拉菜单,使用 onchange 捕获选中的值:
use euv::*;
fn app() -> VirtualNode {
let selected: Signal<String> = use_signal(|| String::from(""));
html! {
div {
label { "Choose a color: " }
select {
onchange: move || {
selected.set(String::from("red"));
},
option { value: "", "-- Select --" }
option { value: "red", "Red" }
option { value: "green", "Green" }
option { value: "blue", "Blue" }
}
p { "Selected: " {selected} }
}
}
}
mount("#app", app);
文件输入处理
对于文件上传,使用带有 onchange 的文件输入元素:
use euv::*;
fn app() -> VirtualNode {
let file_name: Signal<String> = use_signal(|| String::from("No file selected"));
html! {
div {
label { "Upload a file: " }
input {
type: "file",
onchange: move || {
file_name.set(String::from("example.txt"));
}
}
p { {file_name} }
}
}
}
mount("#app", app);
构建完整的表单
以下是一个更完整的表单示例,结合了多种输入类型:
use euv::*;
fn app() -> VirtualNode {
let name: Signal<String> = use_signal(|| String::from(""));
let email: Signal<String> = use_signal(|| String::from(""));
let age: Signal<i32> = use_signal(|| 0);
let subscribe: Signal<bool> = use_signal(|| false);
let plan: Signal<String> = use_signal(|| String::from("free"));
html! {
div {
class: class!("form"),
h1 { "Registration Form" }
div {
class: class!("form-group"),
label { "Name: " }
input {
oninput: move || {
name.set(String::from("Alice"));
}
}
}
div {
class: class!("form-group"),
label { "Email: " }
input {
type: "email",
oninput: move || {
email.set(String::from("alice@example.com"));
}
}
}
div {
class: class!("form-group"),
label { "Age: " }
input {
type: "number",
oninput: move || {
age.set(25);
}
}
}
div {
class: class!("form-group"),
label {
input {
type: "checkbox",
onchange: move || {
subscribe.set(!subscribe.get());
}
}
" Subscribe to newsletter"
}
}
div {
class: class!("form-group"),
label { "Plan: " }
select {
onchange: move || {
plan.set(String::from("pro"));
},
option { value: "free", "Free" }
option { value: "pro", "Pro" }
option { value: "enterprise", "Enterprise" }
}
}
div {
class: class!("form-summary"),
h2 { "Summary" }
p { "Name: " {name} }
p { "Email: " {email} }
p { "Age: " {age} }
p { "Subscribed: " {subscribe} }
p { "Plan: " {plan} }
}
}
}
}
mount("#app", app);
实时验证
你可以使用 computed! 从表单输入中派生验证状态:
use euv::*;
fn app() -> VirtualNode {
let email: Signal<String> = use_signal(|| String::from(""));
let is_valid_email = computed!(email, |e| {
e.contains('@')
});
html! {
div {
input {
class: class!(
"input",
if is_valid_email.get() { "valid" } else { "invalid" }
),
oninput: move || {
email.set(String::from("test@example.com"));
}
}
if !is_valid_email.get() {
p { class: class!("error"), "Please enter a valid email address." }
}
}
}
}
mount("#app", app);
computed! 宏自动追踪 email 信号,并在每次变化时重新计算。这提供了实时验证反馈,无需任何手动同步。
使用 watch! 处理副作用
当你需要在输入变化时执行副作用(如保存数据或触发搜索)时,使用 watch!:
use euv::*;
fn app() -> VirtualNode {
let search_query: Signal<String> = use_signal(|| String::from(""));
watch!(search_query, |q| {
// 每当 search_query 变化时运行
log::info!("Searching for: ");
});
html! {
div {
input {
oninput: move || {
search_query.set(String::from("euv framework"));
}
}
p { "Search query: " {search_query} }
}
}
}
mount("#app", app);
watch! 宏接受一个信号和一个闭包。每当信号值发生变化时,闭包就会运行,非常适合 API 调用、日志记录或更新其他状态等副作用。
输入事件与响应式信号
输入事件和响应式信号的组合创建了一个强大的模式:
- 信号存储值 — 每个输入都有一个关联的信号,保存其当前值。
- 事件处理程序更新信号 — 当用户与输入交互时,事件处理程序用新值调用
signal.set()。 - UI 响应信号变化 — UI 中引用该信号的任何部分都会在值变化时自动更新。
这种单向数据流使表单变得可预测且易于理解。
输入和变更事件的最佳实践
-
使用
oninput进行实时更新 — 当你需要即时反馈(如实时搜索或字符计数器)时,使用oninput。 -
使用
onchange确认更改 — 对于复选框、选择下拉框,以及当你只需要最终值时,使用onchange。 -
在信号中存储值 — 始终将输入值存储在响应式信号中,而不是尝试直接读取 DOM。
-
使用
computed!派生状态 — 验证、格式化和其他派生值应使用computed!来与输入保持同步。 -
使用
watch!处理副作用 — API 调用、日志记录和其他副作用应使用watch!来响应变化。 -
优先使用受控输入 — 在 euv 中,信号是输入值的"唯一数据源"。信号值始终反映应该显示的内容。
总结
euv 的输入和变更事件处理与其响应式信号系统无缝集成。通过在信号中存储输入值,并通过 oninput 和 onchange 处理程序更新它们,你可以构建响应迅速、可预测且易于维护的表单。computed! 和 watch! 宏通过派生状态和副作用扩展了这一模式,为你提供了表单处理的完整工具集。