Input-and-Change-Events[20260804055231]

0 阅读1分钟

输入与变更事件

项目代码:github.com/euv-dev/euv

表单处理是前端开发中最常见的任务之一。euv 通过 oninputonchange 事件提供了简洁的用户输入响应模式,使构建实时响应用户操作的交互式表单变得轻而易举。

理解 Input 和 Change 事件

在深入代码之前,了解 oninputonchange 之间的区别很重要:

  • 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 调用、日志记录或更新其他状态等副作用。

输入事件与响应式信号

输入事件和响应式信号的组合创建了一个强大的模式:

  1. 信号存储值 — 每个输入都有一个关联的信号,保存其当前值。
  2. 事件处理程序更新信号 — 当用户与输入交互时,事件处理程序用新值调用 signal.set()
  3. UI 响应信号变化 — UI 中引用该信号的任何部分都会在值变化时自动更新。

这种单向数据流使表单变得可预测且易于理解。

输入和变更事件的最佳实践

  1. 使用 oninput 进行实时更新 — 当你需要即时反馈(如实时搜索或字符计数器)时,使用 oninput

  2. 使用 onchange 确认更改 — 对于复选框、选择下拉框,以及当你只需要最终值时,使用 onchange

  3. 在信号中存储值 — 始终将输入值存储在响应式信号中,而不是尝试直接读取 DOM。

  4. 使用 computed! 派生状态 — 验证、格式化和其他派生值应使用 computed! 来与输入保持同步。

  5. 使用 watch! 处理副作用 — API 调用、日志记录和其他副作用应使用 watch! 来响应变化。

  6. 优先使用受控输入 — 在 euv 中,信号是输入值的"唯一数据源"。信号值始终反映应该显示的内容。

总结

euv 的输入和变更事件处理与其响应式信号系统无缝集成。通过在信号中存储输入值,并通过 oninputonchange 处理程序更新它们,你可以构建响应迅速、可预测且易于维护的表单。computed!watch! 宏通过派生状态和副作用扩展了这一模式,为你提供了表单处理的完整工具集。


项目代码:github.com/euv-dev/euv