Skip to main content

yggdrasil/hooks/
event_listener.rs

1//! 通用 DOM 事件监听 hook:封装 add/remove_event_listener 生命周期。
2//!
3//! 用 `StoredValue::new_local` 持有 `(Closure, target)`,`Effect` 注册,`on_cleanup`
4//! 清理,把 theme/showcase 等处手写的样板收口到一处。非 wasm32 目标下整体编译为空操作
5//! (服务端无 DOM),调用方无需再写占位变量或 `#[cfg]`。
6//!
7//! 设计要点:
8//! - `target` 通过一个初始化闭包 `acquire` 提供,在 `Effect` 首次运行时调用
9//!   (此时 DOM 一定可用);这样调用方无需自己再包一层 `Effect`。
10//! - `event` 要求 `&'static str`,add 与 remove 复用同一字符串字面量。
11//! - handler 是 `FnMut()`(无参);若调用方需要 target 信息(如 `media.matches()`),
12//!   应在 `acquire` 里 clone 一份后 move 进 handler。需要事件对象(如 `KeyboardEvent`
13//!   的 `key()`)时不要用本 hook,直接用 `window_event_listener` 或元素的 `on:` 绑定。
14//! - `acquire`、`handler` 不要求 `Send + Sync`(可直接捕获 `web_sys` 句柄);
15//!   捕获信号时它们是 `Copy` 句柄,同样无约束。
16
17/// 注册一次性事件监听器,组件卸载时自动移除。
18///
19/// - `acquire`:在 `Effect` 首次运行(DOM 已就绪)时调用,返回要监听的 target
20///   及其克隆(handler 内若要读 target,用这个克隆)。返回 `None` 则不注册。
21/// - `event`:事件名(如 `"scroll"` / `"change"`),需为 `&'static str`。
22/// - `handler`:事件触发时的回调,`FnMut()`(无参)。
23///
24/// 须在组件函数体(reactive owner 内)调用;仅在 wasm32 执行真实注册,非 wasm 为 noop。
25///
26/// # 例
27///
28/// ```ignore
29/// use_event_listener(
30///     web_sys::window,
31///     "scroll",
32///     move || { visible.set(/* ... */); },
33/// );
34/// ```
35#[cfg(target_arch = "wasm32")]
36pub fn use_event_listener<T, A, F>(acquire: A, event: &'static str, handler: F)
37where
38    T: AsRef<web_sys::EventTarget> + Clone + 'static,
39    A: FnOnce() -> Option<T> + 'static,
40    F: FnMut() + 'static,
41{
42    use leptos::prelude::*;
43    use wasm_bindgen::{closure::Closure, JsCast};
44
45    // 持有 (Closure, target),在整个组件生命周期内复用;
46    // on_cleanup 时 take 出来移除监听,防止泄漏。
47    type ListenerState<T> = Option<(Closure<dyn FnMut()>, T)>;
48    let state = StoredValue::<ListenerState<T>, LocalStorage>::new_local(None);
49
50    // Effect 回调是 FnMut,但 acquire 是 FnOnce、handler 要 move 进事件闭包。
51    // 用 Option 包裹后 take(),首次运行消费掉,避免重复 move。
52    let mut acquire = Some(acquire);
53    let mut handler = Some(handler);
54
55    Effect::new(move |_| {
56        let Some(acquire) = acquire.take() else {
57            return;
58        };
59        let Some(mut handler) = handler.take() else {
60            return;
61        };
62        let Some(target) = acquire() else { return };
63        let closure = Closure::wrap(Box::new(move || {
64            handler();
65        }) as Box<dyn FnMut()>);
66        let _ = target
67            .as_ref()
68            .add_event_listener_with_callback(event, closure.as_ref().unchecked_ref());
69        state.set_value(Some((closure, target)));
70    });
71
72    on_cleanup(move || {
73        if let Some(Some((closure, target))) = state.try_update_value(Option::take) {
74            let _ = target
75                .as_ref()
76                .remove_event_listener_with_callback(event, closure.as_ref().unchecked_ref());
77        }
78    });
79}
80
81/// 非 wasm 占位:服务端无 DOM,编译为空操作。
82///
83/// 保留与 wasm 版相同的泛型形态,调用方代码两端完全一致。
84#[cfg(not(target_arch = "wasm32"))]
85#[allow(unused_variables, dead_code)]
86pub fn use_event_listener<T, A, F>(acquire: A, event: &'static str, handler: F)
87where
88    T: Clone + 'static,
89    A: FnOnce() -> Option<T> + 'static,
90    F: FnMut() + 'static,
91{
92    // 服务端无 DOM,空实现。
93}