Skip to main content

yggdrasil/hooks/
event_listener.rs

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