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}