leptos_use/
use_event_listener.rs

1use crate::core::IntoElementMaybeSignal;
2use cfg_if::cfg_if;
3use default_struct_builder::DefaultBuilder;
4use leptos::ev::EventDescriptor;
5
6cfg_if! { if #[cfg(not(feature = "ssr"))] {
7    use crate::{watch_with_options, WatchOptions, sendwrap_fn};
8    use leptos::prelude::*;
9    use std::cell::RefCell;
10    use std::rc::Rc;
11    use wasm_bindgen::closure::Closure;
12    use wasm_bindgen::JsCast;
13}}
14
15/// Use EventListener with ease.
16///
17/// Register using [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) on mounted,
18/// and [removeEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) automatically on cleanup.
19///
20/// ## Usage
21///
22/// ```
23/// # use leptos::prelude::*;
24/// # use leptos::ev::visibilitychange;
25/// # use leptos::logging::log;
26/// # use leptos_use::{use_document, use_event_listener};
27/// #
28/// # #[component]
29/// # fn Demo() -> impl IntoView {
30/// use_event_listener(use_document(), visibilitychange, |evt| {
31///     log!("{:?}", evt);
32/// });
33/// #    view! { }
34/// # }
35/// ```
36///
37/// You can also pass a [`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) as the event target, [`use_event_listener`] will unregister the previous event and register
38/// the new one when you change the target.
39///
40/// ```
41/// # use leptos::prelude::*;
42/// # use leptos::ev::click;
43/// # use leptos::logging::log;
44/// # use leptos_use::use_event_listener;
45/// #
46/// # #[component]
47/// # fn Demo() -> impl IntoView {
48/// let element = NodeRef::new();
49///
50/// use_event_listener(element, click, |evt| {
51///     log!("click from element {:?}", event_target::<web_sys::HtmlDivElement>(&evt));
52/// });
53///
54/// let (cond, set_cond) = signal(true);
55///
56/// view! {
57///     <Show
58///         when=move || cond.get()
59///         fallback=move || view! { <div node_ref=element>"Condition false"</div> }
60///     >
61///         <div node_ref=element>"Condition true"</div>
62///     </Show>
63/// }
64/// # }
65/// ```
66///
67/// You can also call the returned to unregister the listener.
68///
69/// ```
70/// # use leptos::prelude::*;
71/// # use leptos::ev::keydown;
72/// # use leptos::logging::log;
73/// # use web_sys::KeyboardEvent;
74/// # use leptos_use::use_event_listener;
75/// #
76/// # #[component]
77/// # fn Demo() -> impl IntoView {
78/// let cleanup = use_event_listener(document().body(), keydown, |evt: KeyboardEvent| {
79///     log!("{}", &evt.key());
80/// });
81///
82/// cleanup();
83/// #
84/// #    view! { }
85/// # }
86/// ```
87///
88/// ## SendWrapped Return
89///
90/// The returned closure is a sendwrapped function. It can
91/// only be called from the same thread that called `use_event_listener`.
92///
93/// ## Server-Side Rendering
94///
95/// On the server this amounts to a noop.
96pub fn use_event_listener<Ev, El, M, F>(
97    target: El,
98    event: Ev,
99    handler: F,
100) -> impl Fn() + Clone + Send + Sync
101where
102    Ev: EventDescriptor + 'static,
103    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
104    F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
105{
106    use_event_listener_with_options(target, event, handler, UseEventListenerOptions::default())
107}
108
109/// Version of [`use_event_listener`] that takes `web_sys::AddEventListenerOptions`. See the docs for [`use_event_listener`] for how to use.
110#[cfg_attr(feature = "ssr", allow(unused_variables))]
111#[allow(unused_mut)]
112pub fn use_event_listener_with_options<Ev, El, M, F>(
113    target: El,
114    event: Ev,
115    mut handler: F,
116    options: UseEventListenerOptions,
117) -> impl Fn() + Clone + Send + Sync
118where
119    Ev: EventDescriptor + 'static,
120    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
121    F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
122{
123    #[cfg(feature = "ssr")]
124    {
125        || {}
126    }
127
128    #[cfg(not(feature = "ssr"))]
129    {
130        use send_wrapper::SendWrapper;
131        let event_name = event.name();
132        let closure_js = Closure::wrap(Box::new(move |e| {
133            #[cfg(debug_assertions)]
134            let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
135
136            handler(e);
137        }) as Box<dyn FnMut(_)>)
138        .into_js_value();
139
140        let cleanup_fn = {
141            let closure_js = closure_js.clone();
142            let options = options.as_add_event_listener_options();
143
144            move |element: &web_sys::EventTarget| {
145                let _ = element.remove_event_listener_with_callback_and_event_listener_options(
146                    &event_name,
147                    closure_js.as_ref().unchecked_ref(),
148                    options.unchecked_ref(),
149                );
150            }
151        };
152
153        let event_name = event.name();
154
155        let signal = target.into_element_maybe_signal();
156
157        let prev_element = Rc::new(RefCell::new(None::<web_sys::EventTarget>));
158
159        let cleanup_prev_element = {
160            let prev_element = prev_element.clone();
161
162            move || {
163                if let Some(element) = prev_element.take() {
164                    cleanup_fn(&element);
165                }
166            }
167        };
168
169        let stop_watch = {
170            let cleanup_prev_element = cleanup_prev_element.clone();
171
172            watch_with_options(
173                move || signal.get(),
174                move |element, _, _| {
175                    cleanup_prev_element();
176                    prev_element.replace(element.clone());
177
178                    if let Some(element) = element {
179                        let options = options.as_add_event_listener_options();
180
181                        _ = element
182                            .add_event_listener_with_callback_and_add_event_listener_options(
183                                &event_name,
184                                closure_js.as_ref().unchecked_ref(),
185                                &options,
186                            );
187                    }
188                },
189                WatchOptions::default().immediate(true),
190            )
191        };
192
193        let stop = sendwrap_fn!(move || {
194            stop_watch();
195            cleanup_prev_element();
196        });
197
198        on_cleanup({
199            let stop = SendWrapper::new(stop.clone());
200            #[allow(clippy::redundant_closure)]
201            move || stop()
202        });
203
204        stop
205    }
206}
207
208/// Options for [`use_event_listener_with_options`].
209#[derive(DefaultBuilder, Default, Copy, Clone)]
210#[cfg_attr(feature = "ssr", allow(dead_code))]
211pub struct UseEventListenerOptions {
212    /// A boolean value indicating that events of this type will be dispatched to
213    /// the registered `listener` before being dispatched to any `EventTarget`
214    /// beneath it in the DOM tree. If not specified, defaults to `false`.
215    capture: bool,
216
217    /// A boolean value indicating that the `listener` should be invoked at most
218    /// once after being added. If `true`, the `listener` would be automatically
219    /// removed when invoked. If not specified, defaults to `false`.
220    once: bool,
221
222    /// A boolean value that, if `true`, indicates that the function specified by
223    /// `listener` will never call
224    /// [`preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault "preventDefault()").
225    /// If a passive listener does call `preventDefault()`, the user agent will do
226    /// nothing other than generate a console warning. If not specified,
227    /// defaults to `false` – except that in browsers other than Safari,
228    /// defaults to `true` for the
229    /// [`wheel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event "wheel"),
230    /// [`mousewheel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousewheel_event "mousewheel"),
231    /// [`touchstart`](https://developer.mozilla.org/en-US/docs/Web/API/Element/touchstart_event "touchstart") and
232    /// [`touchmove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/touchmove_event "touchmove")
233    /// events. See [Improving scrolling performance with passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners)
234    /// to learn more.
235    #[builder(into)]
236    passive: Option<bool>,
237}
238
239impl UseEventListenerOptions {
240    #[cfg_attr(feature = "ssr", allow(dead_code))]
241    fn as_add_event_listener_options(&self) -> web_sys::AddEventListenerOptions {
242        let UseEventListenerOptions {
243            capture,
244            once,
245            passive,
246        } = self;
247
248        let options = web_sys::AddEventListenerOptions::new();
249        options.set_capture(*capture);
250        options.set_once(*once);
251        if let Some(passive) = passive {
252            options.set_passive(*passive);
253        }
254
255        options
256    }
257}