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}