From dfa30e4aefb33f91d416ca17fa41f3de0fe6c3e2 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Wed, 13 Sep 2023 19:39:56 +0100 Subject: [PATCH] all functions are callable on the server Fixes #31 and #24 --- .idea/misc.xml | 6 + CHANGELOG.md | 48 ++-- docs/book/src/server_side_rendering.md | 31 ++- examples/Cargo.toml | 2 - examples/ssr/Cargo.toml | 10 +- examples/ssr/src/app.rs | 21 +- examples/use_mutation_observer/src/main.rs | 7 +- src/on_click_outside.rs | 274 +++++++++++---------- src/use_active_element.rs | 61 ++--- src/use_css_var.rs | 14 +- src/use_document.rs | 9 +- src/use_drop_zone.rs | 133 +++++----- src/use_element_hover.rs | 39 +-- src/use_element_size.rs | 189 +++++++------- src/use_element_visibility.rs | 40 +-- src/use_event_listener.rs | 127 +++++----- src/use_intersection_observer.rs | 203 +++++++-------- src/use_mouse.rs | 16 +- src/use_mutation_observer.rs | 226 +++++++++++------ src/use_resize_observer.rs | 158 ++++++------ src/use_scroll.rs | 36 +-- src/use_window.rs | 4 +- src/use_window_scroll.rs | 29 +-- 23 files changed, 924 insertions(+), 759 deletions(-) create mode 100644 .idea/misc.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..53e6b55 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 68dcaa7..8d260e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - +## [Unreleased] - ### New Functions 🚀 @@ -20,17 +20,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Please check the release notes of Leptos 0.5 for how to upgrade. - `watch` is now deprecated in favor of `leptos::watch` and will be removed in a future release. `watch_with_options` will continue to exist. -- `use_event_listener_with_options` now takes a `UseEventListenerOptions` instead of a `web_sys::AddEventListenerOptions`. +- `use_event_listener_with_options` now takes a `UseEventListenerOptions` instead of + a `web_sys::AddEventListenerOptions`. +- `use_mutation_observer_with_options` now takes a `UseMutationObserverOptions` instead of + a `web_sys::MutationObserverInit`. - `use_websocket`: - - takes now a `&str` instead of a `String` as its `url` parameter. - - The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`. - - The returned signals `ready_state`, `message`, `message_bytes` have now the type - `Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions. - - The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option` to improve DX. - - The option `manual` has been renamed to `immediate` to make it more consistent with other functions. - To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`). + - takes now a `&str` instead of a `String` as its `url` parameter. + - The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`. + - The returned signals `ready_state`, `message`, `message_bytes` have now the type + `Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions. + - The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option` to improve DX. + - The option `manual` has been renamed to `immediate` to make it more consistent with other functions. + To port please note that `immediate` is the inverse of `manual` (`immediate` = `!manual`). - `use_color_mode`: - - The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details. + - The optional `on_changed` handler parameters have changed slightly. Please refer to the docs for more details. - Throttled or debounced functions cannot be `FnOnce` anymore. - All traits `ClonableFn...` have been removed. @@ -38,9 +41,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Callbacks in options don't require to be cloneable anymore - Callback in `use_raf_fn` doesn't require to be cloneable anymore -- `use_scroll` is now callable on the server -- `use_event_listener` can now be called safely on the server. - +- All (!) functions can now be safely called on the server. Specifically this includes the following that +- panicked on the server: + - `use_scroll` + - `use_event_listener` + - `use_element_hover` + - `on_click_outside` + - `use_drop_zone` + - `use_element_size` + - `use_element_visibility` + - `use_resize_observer` + - `use_intersection_observer` + - `use_mutation_observer` ### Fixes 🍕 @@ -87,11 +99,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `use_local_storage` - `use_session_storage` - Instead of returning `ReadSignal`, the following functions now return `Signal`. - - `use_color_mode` - - `use_favicon` - - `use_storage` - - `use_local_storage` - - `use_session_storage` + - `use_color_mode` + - `use_favicon` + - `use_storage` + - `use_local_storage` + - `use_session_storage` ### Fixes 🍕 diff --git a/docs/book/src/server_side_rendering.md b/docs/book/src/server_side_rendering.md index 8f16416..278c806 100644 --- a/docs/book/src/server_side_rendering.md +++ b/docs/book/src/server_side_rendering.md @@ -32,18 +32,23 @@ and the server. ## Functions with Target Elements -A lot of functions like `use_event_listener` and `use_element_size` are only useful when a target HTML/SVG element is -available. This is not the case on the server. You can simply wrap them in `create_effect` which will cause them to -be only called in the browser. +A lot of functions like `use_resize_observer` and `use_element_size` are only useful when a target HTML/SVG element is +available. This is not always the case on the server. If you use them with `NodeRefs` they will just work in SSR. +But what if you want to use them with `window()` or `document()`? + +To enable that we provide the helper functions [`use_window()`](elements/use_window.md) and [`use_document()`](elements/use_document.md) which return +a new-type-wrapped `Option` or `Option` respectively. These can be +used safely on the server. The following code works on both the client and the server: ```rust -create_effect( - cx, - move |_| { - // window() doesn't work on the server - use_event_listener(window(), "resize", move |_| { - // ... - }) - }, -); -``` \ No newline at end of file +use leptos::*; +use leptos::ev::keyup; +use leptos_use::{use_event_listener, use_window}; + +use_event_listener(use_window(), keyup, |evt| { + ... +}); +``` + +There are some convenience methods provided as well, like `use_document().body()` which +just propagate a `None` on the server. \ No newline at end of file diff --git a/examples/Cargo.toml b/examples/Cargo.toml index e1fe9e5..ee3b98e 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -13,7 +13,6 @@ members = [ "use_css_var", "use_cycle_list", "use_debounce_fn", - "use_document", "use_document_visibility", "use_draggable", "use_drop_zone", @@ -38,7 +37,6 @@ members = [ "use_storage", "use_throttle_fn", "use_websocket", - "use_window", "use_window_focus", "use_window_scroll", "watch_debounced", diff --git a/examples/ssr/Cargo.toml b/examples/ssr/Cargo.toml index 15773d7..d782c10 100644 --- a/examples/ssr/Cargo.toml +++ b/examples/ssr/Cargo.toml @@ -11,16 +11,16 @@ axum = { version = "0.6.4", optional = true } console_error_panic_hook = "0.1" console_log = "1" cfg-if = "1" -leptos = { version = "0.5.0-beta2", features = ["nightly"] } -leptos_axum = { version = "0.5.0-beta2", optional = true } -leptos_meta = { version = "0.5.0-beta2", features = ["nightly"] } -leptos_router = { version = "0.5.0-beta2", features = ["nightly"] } +leptos = { version = "0.5.0-rc1", features = ["nightly"] } +leptos_axum = { version = "0.5.0-rc1", optional = true } +leptos_meta = { version = "0.5.0-rc1", features = ["nightly"] } +leptos_router = { version = "0.5.0-rc1", features = ["nightly"] } leptos-use = { path = "../..", features = ["storage"] } log = "0.4" simple_logger = "4" tokio = { version = "1.25.0", optional = true } tower = { version = "0.4.13", optional = true } -tower-http = { version = "0.5.0-beta2", features = ["fs"], optional = true } +tower-http = { version = "0.4.3", features = ["fs"], optional = true } wasm-bindgen = "=0.2.87" thiserror = "1.0.38" tracing = { version = "0.1.37", optional = true } diff --git a/examples/ssr/src/app.rs b/examples/ssr/src/app.rs index 55e0066..29e62fc 100644 --- a/examples/ssr/src/app.rs +++ b/examples/ssr/src/app.rs @@ -5,7 +5,8 @@ use leptos_meta::*; use leptos_router::*; use leptos_use::storage::use_local_storage; use leptos_use::{ - use_debounce_fn, use_event_listener, use_intl_number_format, UseIntlNumberFormatOptions, + use_debounce_fn, use_event_listener, use_intl_number_format, use_window, + UseIntlNumberFormatOptions, }; #[component] @@ -47,11 +48,9 @@ fn HomePage() -> impl IntoView { let (key, set_key) = create_signal("".to_string()); - create_effect(move |_| { - // window() doesn't work on the server - let _ = use_event_listener(window(), keypress, move |evt: KeyboardEvent| { - set_key(evt.key()) - }); + // window() doesn't work on the server so we provide use_window() + let _ = use_event_listener(use_window(), keypress, move |evt: KeyboardEvent| { + set_key(evt.key()) }); let (debounce_value, set_debounce_value) = create_signal("not called"); @@ -65,10 +64,10 @@ fn HomePage() -> impl IntoView { debounced_fn(); view! { -

"Leptos-Use SSR Example"

- -

"Locale "zh-Hans-CN-u-nu-hanidec": " {zh_count}

-

"Press any key: " {key}

-

"Debounced called: " {debounce_value}

+

Leptos-Use SSR Example

+ +

Locale zh-Hans-CN-u-nu-hanidec: {zh_count}

+

Press any key: {key}

+

Debounced called: {debounce_value}

} } diff --git a/examples/use_mutation_observer/src/main.rs b/examples/use_mutation_observer/src/main.rs index 2d06506..db7cba4 100644 --- a/examples/use_mutation_observer/src/main.rs +++ b/examples/use_mutation_observer/src/main.rs @@ -1,7 +1,7 @@ use leptos::html::Div; use leptos::*; use leptos_use::docs::demo_or_body; -use leptos_use::use_mutation_observer_with_options; +use leptos_use::{use_mutation_observer_with_options, UseMutationObserverOptions}; use std::time::Duration; #[component] @@ -11,9 +11,6 @@ fn Demo() -> impl IntoView { let (class_name, set_class_name) = create_signal(String::new()); let (style, set_style) = create_signal(String::new()); - let mut init = web_sys::MutationObserverInit::new(); - init.attributes(true); - use_mutation_observer_with_options( el, move |mutations, _| { @@ -23,7 +20,7 @@ fn Demo() -> impl IntoView { }); } }, - init, + UseMutationObserverOptions::default().attributes(true), ); let _ = set_timeout_with_handle( diff --git a/src/on_click_outside.rs b/src/on_click_outside.rs index 6db5c63..1ac1cca 100644 --- a/src/on_click_outside.rs +++ b/src/on_click_outside.rs @@ -1,16 +1,20 @@ use crate::core::{ElementMaybeSignal, ElementsMaybeSignal}; -use crate::utils::IS_IOS; -use crate::{use_event_listener, use_event_listener_with_options, UseEventListenerOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; -use leptos::ev::{blur, click, pointerdown}; -use leptos::*; -use std::cell::Cell; -use std::rc::Rc; -use std::sync::RwLock; -use std::time::Duration; -use wasm_bindgen::JsCast; -static IOS_WORKAROUND: RwLock = RwLock::new(false); +cfg_if! { if #[cfg(not(feature = "ssr"))] { + use leptos::*; + use crate::utils::IS_IOS; + use crate::{use_event_listener, use_event_listener_with_options, UseEventListenerOptions}; + use leptos::ev::{blur, click, pointerdown}; + use std::cell::Cell; + use std::rc::Rc; + use std::sync::RwLock; + use std::time::Duration; + use wasm_bindgen::JsCast; + + static IOS_WORKAROUND: RwLock = RwLock::new(false); +}} /// Listen for clicks outside of an element. /// Useful for modals or dropdowns. @@ -48,7 +52,7 @@ static IOS_WORKAROUND: RwLock = RwLock::new(false); /// /// ## Server-Side Rendering /// -/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// On the server this amounts to a no-op. pub fn on_click_outside(target: El, handler: F) -> impl FnOnce() + Clone where El: Clone, @@ -64,6 +68,7 @@ where } /// Version of `on_click_outside` that takes an `OnClickOutsideOptions`. See `on_click_outside` for more details. +#[cfg_attr(feature = "ssr", allow(unused_variables))] pub fn on_click_outside_with_options( target: El, handler: F, @@ -76,147 +81,151 @@ where F: FnMut(web_sys::Event) + Clone + 'static, I: Into + Clone + 'static, { - let OnClickOutsideOptions { - ignore, - capture, - detect_iframes, - } = options; + cfg_if! { if #[cfg(feature = "ssr")] { + || {} + } else { + let OnClickOutsideOptions { + ignore, + capture, + detect_iframes, + } = options; - // Fixes: https://github.com/vueuse/vueuse/issues/1520 - // How it works: https://stackoverflow.com/a/39712411 - if *IS_IOS { - if let Ok(mut ios_workaround) = IOS_WORKAROUND.write() { - if !*ios_workaround { - *ios_workaround = true; - if let Some(body) = document().body() { - let children = body.children(); - for i in 0..children.length() { - let _ = children - .get_with_index(i) - .expect("checked index") - .add_event_listener_with_callback( - "click", - &js_sys::Function::default(), - ); + // Fixes: https://github.com/vueuse/vueuse/issues/1520 + // How it works: https://stackoverflow.com/a/39712411 + if *IS_IOS { + if let Ok(mut ios_workaround) = IOS_WORKAROUND.write() { + if !*ios_workaround { + *ios_workaround = true; + if let Some(body) = document().body() { + let children = body.children(); + for i in 0..children.length() { + let _ = children + .get_with_index(i) + .expect("checked index") + .add_event_listener_with_callback( + "click", + &js_sys::Function::default(), + ); + } } } } } - } - let should_listen = Rc::new(Cell::new(true)); + let should_listen = Rc::new(Cell::new(true)); - let should_ignore = move |event: &web_sys::UiEvent| { - let ignore = ignore.get_untracked(); + let should_ignore = move |event: &web_sys::UiEvent| { + let ignore = ignore.get_untracked(); - ignore.into_iter().flatten().any(|element| { - let element: web_sys::EventTarget = element.into(); + ignore.into_iter().flatten().any(|element| { + let element: web_sys::EventTarget = element.into(); - event_target::(event) == element - || event.composed_path().includes(element.as_ref(), 0) - }) - }; + event_target::(event) == element + || event.composed_path().includes(element.as_ref(), 0) + }) + }; - let target = (target).into(); + let target = (target).into(); - let listener = { - let should_listen = Rc::clone(&should_listen); - let mut handler = handler.clone(); - let target = target.clone(); - let should_ignore = should_ignore.clone(); + let listener = { + let should_listen = Rc::clone(&should_listen); + let mut handler = handler.clone(); + let target = target.clone(); + let should_ignore = should_ignore.clone(); - move |event: web_sys::UiEvent| { - if let Some(el) = target.get_untracked() { - let el = el.into(); - - if el == event_target(&event) || event.composed_path().includes(el.as_ref(), 0) { - return; - } - - if event.detail() == 0 { - should_listen.set(!should_ignore(&event)); - } - - if !should_listen.get() { - should_listen.set(true); - return; - } - - handler(event.into()); - } - } - }; - - let remove_click_listener = { - let mut listener = listener.clone(); - - use_event_listener_with_options::<_, web_sys::Window, _, _>( - window(), - click, - move |event| listener(event.into()), - UseEventListenerOptions::default() - .passive(true) - .capture(capture), - ) - }; - - let remove_pointer_listener = { - let target = target.clone(); - let should_listen = Rc::clone(&should_listen); - - use_event_listener_with_options::<_, web_sys::Window, _, _>( - window(), - pointerdown, - move |event| { + move |event: web_sys::UiEvent| { if let Some(el) = target.get_untracked() { - should_listen.set( - !event.composed_path().includes(el.into().as_ref(), 0) - && !should_ignore(&event), - ); + let el = el.into(); + + if el == event_target(&event) || event.composed_path().includes(el.as_ref(), 0) { + return; + } + + if event.detail() == 0 { + should_listen.set(!should_ignore(&event)); + } + + if !should_listen.get() { + should_listen.set(true); + return; + } + + handler(event.into()); } - }, - UseEventListenerOptions::default().passive(true), - ) - }; + } + }; - let remove_blur_listener = if detect_iframes { - Some(use_event_listener::<_, web_sys::Window, _, _>( - window(), - blur, - move |event| { - let target = target.clone(); - let mut handler = handler.clone(); + let remove_click_listener = { + let mut listener = listener.clone(); - let _ = set_timeout_with_handle( - move || { - if let Some(el) = target.get_untracked() { - if let Some(active_element) = document().active_element() { - if active_element.tag_name() == "IFRAME" - && !el - .into() - .unchecked_into::() - .contains(Some(&active_element.into())) - { - handler(event.into()); + use_event_listener_with_options::<_, web_sys::Window, _, _>( + window(), + click, + move |event| listener(event.into()), + UseEventListenerOptions::default() + .passive(true) + .capture(capture), + ) + }; + + let remove_pointer_listener = { + let target = target.clone(); + let should_listen = Rc::clone(&should_listen); + + use_event_listener_with_options::<_, web_sys::Window, _, _>( + window(), + pointerdown, + move |event| { + if let Some(el) = target.get_untracked() { + should_listen.set( + !event.composed_path().includes(el.into().as_ref(), 0) + && !should_ignore(&event), + ); + } + }, + UseEventListenerOptions::default().passive(true), + ) + }; + + let remove_blur_listener = if detect_iframes { + Some(use_event_listener::<_, web_sys::Window, _, _>( + window(), + blur, + move |event| { + let target = target.clone(); + let mut handler = handler.clone(); + + let _ = set_timeout_with_handle( + move || { + if let Some(el) = target.get_untracked() { + if let Some(active_element) = document().active_element() { + if active_element.tag_name() == "IFRAME" + && !el + .into() + .unchecked_into::() + .contains(Some(&active_element.into())) + { + handler(event.into()); + } } } - } - }, - Duration::ZERO, - ); - }, - )) - } else { - None - }; + }, + Duration::ZERO, + ); + }, + )) + } else { + None + }; - move || { - remove_click_listener(); - remove_pointer_listener(); - if let Some(f) = remove_blur_listener { - f(); + move || { + remove_click_listener(); + remove_pointer_listener(); + if let Some(f) = remove_blur_listener { + f(); + } } - } + }} } /// Options for [`on_click_outside_with_options`]. @@ -226,6 +235,7 @@ where T: Into + Clone + 'static, { /// List of elementss that should not trigger the callback. Defaults to `[]`. + #[cfg_attr(feature = "ssr", allow(dead_code))] ignore: ElementsMaybeSignal, /// Use capturing phase for internal event listener. Defaults to `true`. diff --git a/src/use_active_element.rs b/src/use_active_element.rs index 99bf51f..4a671b0 100644 --- a/src/use_active_element.rs +++ b/src/use_active_element.rs @@ -1,11 +1,9 @@ #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] -use crate::use_event_listener_with_options; -use cfg_if::cfg_if; +use crate::{use_document, use_event_listener_with_options, UseEventListenerOptions}; use leptos::ev::{blur, focus}; use leptos::html::{AnyElement, ToHtmlElement}; use leptos::*; -use web_sys::AddEventListenerOptions; /// Reactive `document.activeElement` /// @@ -36,44 +34,37 @@ use web_sys::AddEventListenerOptions; /// /// On the server this returns a `Signal` that always contains the value `None`. pub fn use_active_element() -> Signal>> { - cfg_if! { if #[cfg(feature = "ssr")] { - let get_active_element = || { None }; - } else { - let get_active_element = move || { - document() - .active_element() - .map(|el| el.to_leptos_element()) - }; - }} + let get_active_element = move || { + use_document() + .active_element() + .map(|el| el.to_leptos_element()) + }; let (active_element, set_active_element) = create_signal(get_active_element()); - cfg_if! { if #[cfg(not(feature = "ssr"))] { - let mut listener_options = AddEventListenerOptions::new(); - listener_options.capture(true); + let listener_options = UseEventListenerOptions::default().capture(true); - let _ = use_event_listener_with_options( - window(), - blur, - move |event| { - if event.related_target().is_some() { - return; - } + let _ = use_event_listener_with_options( + window(), + blur, + move |event| { + if event.related_target().is_some() { + return; + } - set_active_element.update(|el| *el = get_active_element()); - }, - listener_options.clone(), - ); + set_active_element.update(|el| *el = get_active_element()); + }, + listener_options, + ); - let _ = use_event_listener_with_options( - window(), - focus, - move |_| { - set_active_element.update(|el| *el = get_active_element()); - }, - listener_options, - ); - }} + let _ = use_event_listener_with_options( + window(), + focus, + move |_| { + set_active_element.update(|el| *el = get_active_element()); + }, + listener_options, + ); active_element.into() } diff --git a/src/use_css_var.rs b/src/use_css_var.rs index 64185f8..41f1d04 100644 --- a/src/use_css_var.rs +++ b/src/use_css_var.rs @@ -1,13 +1,16 @@ #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] use crate::core::ElementMaybeSignal; -use crate::{use_mutation_observer_with_options, watch_with_options, WatchOptions}; +use crate::{ + use_mutation_observer_with_options, watch_with_options, UseMutationObserverOptions, + WatchOptions, +}; use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; use std::marker::PhantomData; use std::time::Duration; -use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen::JsCast; /// Manipulate CSS variables. /// @@ -127,17 +130,14 @@ where }; if observe { - let mut init = web_sys::MutationObserverInit::new(); let update_css_var = update_css_var.clone(); let el_signal = el_signal.clone(); - init.attribute_filter(&js_sys::Array::from_iter( - vec![JsValue::from_str("style")], - )); use_mutation_observer_with_options::, T, _>( el_signal, move |_, _| update_css_var(), - init, + UseMutationObserverOptions::default() + .attribute_filter(vec!["style".to_string()]), ); } diff --git a/src/use_document.rs b/src/use_document.rs index bcbe4b0..2a7a1bb 100644 --- a/src/use_document.rs +++ b/src/use_document.rs @@ -1,7 +1,9 @@ use cfg_if::cfg_if; -use leptos::*; use std::ops::Deref; +#[cfg(not(feature = "ssr"))] +use leptos::*; + /// SSR safe `document()`. /// This returns just a new-type wrapper around `Option`. /// Calling this amounts to `None` on the server and `Some(Document)` on the client. @@ -44,8 +46,11 @@ impl Deref for UseDocument { } impl UseDocument { - /// Returns `Some(HtmlElement)` in the Browser. `None` otherwise. pub fn body(&self) -> Option { self.0.as_ref().and_then(|d| d.body()) } + + pub fn active_element(&self) -> Option { + self.0.as_ref().and_then(|d| d.active_element()) + } } diff --git a/src/use_drop_zone.rs b/src/use_drop_zone.rs index 164c152..d5e778d 100644 --- a/src/use_drop_zone.rs +++ b/src/use_drop_zone.rs @@ -1,11 +1,15 @@ use crate::core::ElementMaybeSignal; -use crate::use_event_listener; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; -use leptos::ev::{dragenter, dragleave, dragover, drop}; use leptos::*; use std::fmt::{Debug, Formatter}; use std::rc::Rc; +cfg_if! { if #[cfg(not(feature = "ssr"))] { + use crate::use_event_listener; + use leptos::ev::{dragenter, dragleave, dragover, drop}; +}} + /// Create a zone where files can be dropped. /// /// ## Demo @@ -45,7 +49,8 @@ use std::rc::Rc; /// /// ## Server-Side Rendering /// -/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// On the server the returned `file` signal always contains an empty `Vec` and +/// `is_over_drop_zone` contains always `false` pub fn use_drop_zone(target: El) -> UseDropZoneReturn where El: Clone, @@ -56,6 +61,7 @@ where } /// Version of [`use_drop_zone`] that takes a `UseDropZoneOptions`. See [`use_drop_zone`] for how to use. +#[cfg_attr(feature = "ssr", allow(unused_variables))] pub fn use_drop_zone_with_options( target: El, options: UseDropZoneOptions, @@ -65,81 +71,83 @@ where El: Into>, T: Into + Clone + 'static, { - let UseDropZoneOptions { - on_drop, - on_enter, - on_leave, - on_over, - } = options; - let (is_over_drop_zone, set_over_drop_zone) = create_signal(false); let (files, set_files) = create_signal(Vec::::new()); - let counter = store_value(0_usize); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let UseDropZoneOptions { + on_drop, + on_enter, + on_leave, + on_over, + } = options; - let update_files = move |event: &web_sys::DragEvent| { - if let Some(data_transfer) = event.data_transfer() { - let files: Vec = data_transfer - .files() - .map(|f| js_sys::Array::from(&f).to_vec()) - .unwrap_or_default() - .into_iter() - .map(web_sys::File::from) - .collect(); + let counter = store_value(0_usize); - set_files.update(move |f| *f = files); - } - }; + let update_files = move |event: &web_sys::DragEvent| { + if let Some(data_transfer) = event.data_transfer() { + let files: Vec = data_transfer + .files() + .map(|f| js_sys::Array::from(&f).to_vec()) + .unwrap_or_default() + .into_iter() + .map(web_sys::File::from) + .collect(); - let _ = use_event_listener(target.clone(), dragenter, move |event| { - event.prevent_default(); - counter.update_value(|counter| *counter += 1); - set_over_drop_zone.set(true); + set_files.update(move |f| *f = files); + } + }; - update_files(&event); + let _ = use_event_listener(target.clone(), dragenter, move |event| { + event.prevent_default(); + counter.update_value(|counter| *counter += 1); + set_over_drop_zone.set(true); - on_enter(UseDropZoneEvent { - files: files.get_untracked(), - event, + update_files(&event); + + on_enter(UseDropZoneEvent { + files: files.get_untracked(), + event, + }); }); - }); - let _ = use_event_listener(target.clone(), dragover, move |event| { - event.prevent_default(); - update_files(&event); - on_over(UseDropZoneEvent { - files: files.get_untracked(), - event, + let _ = use_event_listener(target.clone(), dragover, move |event| { + event.prevent_default(); + update_files(&event); + on_over(UseDropZoneEvent { + files: files.get_untracked(), + event, + }); }); - }); - let _ = use_event_listener(target.clone(), dragleave, move |event| { - event.prevent_default(); - counter.update_value(|counter| *counter -= 1); - if counter.get_value() == 0 { + let _ = use_event_listener(target.clone(), dragleave, move |event| { + event.prevent_default(); + counter.update_value(|counter| *counter -= 1); + if counter.get_value() == 0 { + set_over_drop_zone.set(false); + } + + update_files(&event); + + on_leave(UseDropZoneEvent { + files: files.get_untracked(), + event, + }); + }); + + let _ = use_event_listener(target, drop, move |event| { + event.prevent_default(); + counter.update_value(|counter| *counter = 0); set_over_drop_zone.set(false); - } - update_files(&event); + update_files(&event); - on_leave(UseDropZoneEvent { - files: files.get_untracked(), - event, + on_drop(UseDropZoneEvent { + files: files.get_untracked(), + event, + }); }); - }); - - let _ = use_event_listener(target, drop, move |event| { - event.prevent_default(); - counter.update_value(|counter| *counter = 0); - set_over_drop_zone.set(false); - - update_files(&event); - - on_drop(UseDropZoneEvent { - files: files.get_untracked(), - event, - }); - }); + }} UseDropZoneReturn { files: files.into(), @@ -149,6 +157,7 @@ where /// Options for [`use_drop_zone_with_options`]. #[derive(DefaultBuilder, Clone)] +#[cfg_attr(feature = "ssr", allow(dead_code))] pub struct UseDropZoneOptions { /// Event handler for the [`drop`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event) event on_drop: Rc, diff --git a/src/use_element_hover.rs b/src/use_element_hover.rs index 6d3e2ce..4b3b412 100644 --- a/src/use_element_hover.rs +++ b/src/use_element_hover.rs @@ -1,10 +1,14 @@ use crate::core::ElementMaybeSignal; use crate::{use_event_listener_with_options, UseEventListenerOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::ev::{mouseenter, mouseleave}; use leptos::leptos_dom::helpers::TimeoutHandle; use leptos::*; -use std::time::Duration; + +cfg_if! { if #[cfg(not(feature = "ssr"))] { + use std::time::Duration; +}} /// Reactive element's hover state. /// @@ -32,7 +36,7 @@ use std::time::Duration; /// /// ## Server-Side Rendering /// -/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// On the server this returns a `Signal` that always contains the value `false`. pub fn use_element_hover(el: El) -> Signal where El: Clone, @@ -44,6 +48,7 @@ where /// Version of [`use_element_hover`] that takes a `UseElementHoverOptions`. See [`use_element_hover`] for how to use. +#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))] pub fn use_element_hover_with_options( el: El, options: UseElementHoverOptions, @@ -63,24 +68,26 @@ where let mut timer: Option = None; let mut toggle = move |entering: bool| { - let delay = if entering { delay_enter } else { delay_leave }; + cfg_if! { if #[cfg(not(feature = "ssr"))] { + let delay = if entering { delay_enter } else { delay_leave }; - if let Some(handle) = timer.take() { - handle.clear(); - } + if let Some(handle) = timer.take() { + handle.clear(); + } - if delay > 0 { - timer = set_timeout_with_handle( - move || set_hovered.set(entering), - Duration::from_millis(delay), - ) - .ok(); - } else { - set_hovered.set(entering); - } + if delay > 0 { + timer = set_timeout_with_handle( + move || set_hovered.set(entering), + Duration::from_millis(delay), + ) + .ok(); + } else { + set_hovered.set(entering); + } + }} }; - let mut listener_options = UseEventListenerOptions::default().passive(true); + let listener_options = UseEventListenerOptions::default().passive(true); let _ = use_event_listener_with_options( el.clone(), diff --git a/src/use_element_size.rs b/src/use_element_size.rs index 64e4f3b..8831505 100644 --- a/src/use_element_size.rs +++ b/src/use_element_size.rs @@ -1,10 +1,14 @@ use crate::core::{ElementMaybeSignal, Size}; -use crate::{use_resize_observer_with_options, UseResizeObserverOptions}; -use crate::{watch_with_options, WatchOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsCast; + +cfg_if! { if #[cfg(not(feature = "ssr"))] { + use crate::{use_resize_observer_with_options, UseResizeObserverOptions}; + use crate::{watch_with_options, WatchOptions}; + use wasm_bindgen::JsCast; +}} /// Reactive size of an HTML element. /// @@ -41,7 +45,7 @@ use wasm_bindgen::JsCast; /// /// ## Server-Side Rendering /// -/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// On the server the returned signals always contain the value of the `initial_size` option. /// /// ## See also /// @@ -56,6 +60,7 @@ where } /// Version of [`use_element_size`] that takes a `UseElementSizeOptions`. See [`use_element_size`] for how to use. +#[cfg_attr(feature = "ssr", allow(unused_variables))] pub fn use_element_size_with_options( target: El, options: UseElementSizeOptions, @@ -65,101 +70,104 @@ where El: Into>, T: Into + Clone + 'static, { - let window = window(); - let box_ = options.box_; - let initial_size = options.initial_size; + let UseElementSizeOptions { box_, initial_size } = options; - let target = (target).into(); + let (width, set_width) = create_signal(initial_size.width); + let (height, set_height) = create_signal(initial_size.height); - let is_svg = { - let target = target.clone(); + cfg_if! { if #[cfg(not(feature = "ssr"))] { - move || { - if let Some(target) = target.get_untracked() { - target - .into() - .namespace_uri() - .map(|ns| ns.contains("svg")) - .unwrap_or(false) - } else { - false + let box_ = box_.unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox); + + let target = target.into(); + + let is_svg = { + let target = target.clone(); + + move || { + if let Some(target) = target.get_untracked() { + target + .into() + .namespace_uri() + .map(|ns| ns.contains("svg")) + .unwrap_or(false) + } else { + false + } } - } - }; + }; - let (width, set_width) = create_signal(options.initial_size.width); - let (height, set_height) = create_signal(options.initial_size.height); + { + let target = target.clone(); - { - let target = target.clone(); + let _ = use_resize_observer_with_options::, _, _>( + target.clone(), + move |entries, _| { + let entry = &entries[0]; - let _ = use_resize_observer_with_options::, _, _>( - target.clone(), - move |entries, _| { - let entry = &entries[0]; - - let box_size = match box_ { - web_sys::ResizeObserverBoxOptions::ContentBox => entry.content_box_size(), - web_sys::ResizeObserverBoxOptions::BorderBox => entry.border_box_size(), - web_sys::ResizeObserverBoxOptions::DevicePixelContentBox => { - entry.device_pixel_content_box_size() - } - _ => unreachable!(), - }; - - if is_svg() { - if let Some(target) = target.get() { - if let Ok(Some(styles)) = window.get_computed_style(&target.into()) { - set_height.set( - styles - .get_property_value("height") - .map(|v| v.parse().unwrap_or_default()) - .unwrap_or_default(), - ); - set_width.set( - styles - .get_property_value("width") - .map(|v| v.parse().unwrap_or_default()) - .unwrap_or_default(), - ); + let box_size = match box_ { + web_sys::ResizeObserverBoxOptions::ContentBox => entry.content_box_size(), + web_sys::ResizeObserverBoxOptions::BorderBox => entry.border_box_size(), + web_sys::ResizeObserverBoxOptions::DevicePixelContentBox => { + entry.device_pixel_content_box_size() } - } - } else if !box_size.is_null() && !box_size.is_undefined() && box_size.length() > 0 { - let format_box_size = if box_size.is_array() { - box_size.to_vec() - } else { - vec![box_size.into()] + _ => unreachable!(), }; - set_width.set(format_box_size.iter().fold(0.0, |acc, v| { - acc + v.as_ref().clone().unchecked_into::().inline_size() - })); - set_height.set(format_box_size.iter().fold(0.0, |acc, v| { - acc + v.as_ref().clone().unchecked_into::().block_size() - })) + if is_svg() { + if let Some(target) = target.get() { + if let Ok(Some(styles)) = window().get_computed_style(&target.into()) { + set_height.set( + styles + .get_property_value("height") + .map(|v| v.parse().unwrap_or_default()) + .unwrap_or_default(), + ); + set_width.set( + styles + .get_property_value("width") + .map(|v| v.parse().unwrap_or_default()) + .unwrap_or_default(), + ); + } + } + } else if !box_size.is_null() && !box_size.is_undefined() && box_size.length() > 0 { + let format_box_size = if box_size.is_array() { + box_size.to_vec() + } else { + vec![box_size.into()] + }; + + set_width.set(format_box_size.iter().fold(0.0, |acc, v| { + acc + v.as_ref().clone().unchecked_into::().inline_size() + })); + set_height.set(format_box_size.iter().fold(0.0, |acc, v| { + acc + v.as_ref().clone().unchecked_into::().block_size() + })) + } else { + // fallback + set_width.set(entry.content_rect().width()); + set_height.set(entry.content_rect().height()) + } + }, + UseResizeObserverOptions::default().box_(box_), + ); + } + + let _ = watch_with_options( + move || target.get(), + move |ele, _, _| { + if ele.is_some() { + set_width.set(initial_size.width); + set_height.set(initial_size.height); } else { - // fallback - set_width.set(entry.content_rect().width()); - set_height.set(entry.content_rect().height()) + set_width.set(0.0); + set_height.set(0.0); } }, - options.into(), + WatchOptions::default().immediate(false), ); - } - - let _ = watch_with_options( - move || target.get(), - move |ele, _, _| { - if ele.is_some() { - set_width.set(initial_size.width); - set_height.set(initial_size.height); - } else { - set_width.set(0.0); - set_height.set(0.0); - } - }, - WatchOptions::default().immediate(false), - ); + }} UseElementSizeReturn { width: width.into(), @@ -175,24 +183,19 @@ pub struct UseElementSizeOptions { initial_size: Size, /// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`. - pub box_: web_sys::ResizeObserverBoxOptions, + #[builder(into)] + pub box_: Option, } impl Default for UseElementSizeOptions { fn default() -> Self { Self { initial_size: Size::default(), - box_: web_sys::ResizeObserverBoxOptions::ContentBox, + box_: None, } } } -impl From for UseResizeObserverOptions { - fn from(options: UseElementSizeOptions) -> Self { - Self::default().box_(options.box_) - } -} - /// The return value of [`use_element_size`]. pub struct UseElementSizeReturn { /// The width of the element. diff --git a/src/use_element_visibility.rs b/src/use_element_visibility.rs index 6be8b51..23a9543 100644 --- a/src/use_element_visibility.rs +++ b/src/use_element_visibility.rs @@ -1,9 +1,12 @@ use crate::core::ElementMaybeSignal; -use crate::{use_intersection_observer_with_options, UseIntersectionObserverOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; use std::marker::PhantomData; +#[cfg(not(feature = "ssr"))] +use crate::{use_intersection_observer_with_options, UseIntersectionObserverOptions}; + /// Tracks the visibility of an element within the viewport. /// /// ## Demo @@ -33,7 +36,7 @@ use std::marker::PhantomData; /// /// ## Server-Side Rendering /// -/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// On the server this returns a `Signal` that always contains the value `false`. /// /// ## See also /// @@ -49,6 +52,8 @@ where ) } +/// Version of [`use_element_visibility`] with that takes a `UseElementVisibilityOptions`. See [`use_element_visibility`] for how to use. +#[cfg_attr(feature = "ssr", allow(unused_variables))] pub fn use_element_visibility_with_options( target: El, options: UseElementVisibilityOptions, @@ -61,20 +66,22 @@ where { let (is_visible, set_visible) = create_signal(false); - use_intersection_observer_with_options( - target.into(), - move |entries, _| { - // In some circumstances Chrome passes a first (or only) entry which has a zero bounding client rect - // and returns `is_intersecting` erroneously as `false`. - if let Some(entry) = entries.into_iter().find(|entry| { - let rect = entry.bounding_client_rect(); - rect.width() > 0.0 || rect.height() > 0.0 - }) { - set_visible.set(entry.is_intersecting()); - } - }, - UseIntersectionObserverOptions::default().root(options.viewport), - ); + cfg_if! { if #[cfg(not(feature = "ssr"))] { + use_intersection_observer_with_options( + target.into(), + move |entries, _| { + // In some circumstances Chrome passes a first (or only) entry which has a zero bounding client rect + // and returns `is_intersecting` erroneously as `false`. + if let Some(entry) = entries.into_iter().find(|entry| { + let rect = entry.bounding_client_rect(); + rect.width() > 0.0 || rect.height() > 0.0 + }) { + set_visible.set(entry.is_intersecting()); + } + }, + UseIntersectionObserverOptions::default().root(options.viewport), + ); + }} is_visible.into() } @@ -92,6 +99,7 @@ where /// Defaults to `None` (which means the root `document` will be used). /// Please note that setting this to a `Some(document)` may not be supported by all browsers. /// See [Browser Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/IntersectionObserver#browser_compatibility) + #[cfg_attr(feature = "ssr", allow(dead_code))] viewport: Option, #[builder(skip)] diff --git a/src/use_event_listener.rs b/src/use_event_listener.rs index e595895..e8c218e 100644 --- a/src/use_event_listener.rs +++ b/src/use_event_listener.rs @@ -1,12 +1,16 @@ use crate::core::ElementMaybeSignal; -use crate::{watch_with_options, WatchOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::ev::EventDescriptor; -use leptos::*; -use std::cell::RefCell; -use std::rc::Rc; -use wasm_bindgen::closure::Closure; -use wasm_bindgen::JsCast; + +cfg_if! { if #[cfg(not(feature = "ssr"))] { + use crate::{watch_with_options, WatchOptions}; + use leptos::*; + use std::cell::RefCell; + use std::rc::Rc; + use wasm_bindgen::closure::Closure; + use wasm_bindgen::JsCast; +}} /// Use EventListener with ease. /// Register using [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) on mounted, @@ -94,6 +98,7 @@ where } /// Version of [`use_event_listener`] that takes `web_sys::AddEventListenerOptions`. See the docs for [`use_event_listener`] for how to use. +#[cfg_attr(feature = "ssr", allow(unused_variables))] pub fn use_event_listener_with_options( target: El, event: Ev, @@ -106,73 +111,78 @@ where T: Into + Clone + 'static, F: FnMut(::EventType) + 'static, { - let event_name = event.name(); - let closure_js = Closure::wrap(Box::new(handler) as Box).into_js_value(); + cfg_if! { if #[cfg(feature = "ssr")] { + || {} + } else { + let event_name = event.name(); + let closure_js = Closure::wrap(Box::new(handler) as Box).into_js_value(); - let cleanup_fn = { - let closure_js = closure_js.clone(); - let options = options.as_add_event_listener_options(); + let cleanup_fn = { + let closure_js = closure_js.clone(); + let options = options.as_add_event_listener_options(); - move |element: &web_sys::EventTarget| { - let _ = element.remove_event_listener_with_callback_and_event_listener_options( - &event_name, - closure_js.as_ref().unchecked_ref(), - options.unchecked_ref(), - ); - } - }; - - let event_name = event.name(); - - let signal = (target).into(); - - let prev_element = Rc::new(RefCell::new(None::)); - - let cleanup_prev_element = { - let prev_element = prev_element.clone(); - - move || { - if let Some(element) = prev_element.take() { - cleanup_fn(&element); + move |element: &web_sys::EventTarget| { + let _ = element.remove_event_listener_with_callback_and_event_listener_options( + &event_name, + closure_js.as_ref().unchecked_ref(), + options.unchecked_ref(), + ); } - } - }; + }; - let stop_watch = { - let cleanup_prev_element = cleanup_prev_element.clone(); + let event_name = event.name(); - watch_with_options( - move || signal.get().map(|e| e.into()), - move |element, _, _| { - cleanup_prev_element(); - prev_element.replace(element.clone()); + let signal = (target).into(); - if let Some(element) = element { - let options = options.as_add_event_listener_options(); + let prev_element = Rc::new(RefCell::new(None::)); - _ = element.add_event_listener_with_callback_and_add_event_listener_options( - &event_name, - closure_js.as_ref().unchecked_ref(), - &options, - ); + let cleanup_prev_element = { + let prev_element = prev_element.clone(); + + move || { + if let Some(element) = prev_element.take() { + cleanup_fn(&element); } - }, - WatchOptions::default().immediate(true), - ) - }; + } + }; - let stop = move || { - stop_watch(); - cleanup_prev_element(); - }; + let stop_watch = { + let cleanup_prev_element = cleanup_prev_element.clone(); - on_cleanup(stop.clone()); + watch_with_options( + move || signal.get().map(|e| e.into()), + move |element, _, _| { + cleanup_prev_element(); + prev_element.replace(element.clone()); - stop + if let Some(element) = element { + let options = options.as_add_event_listener_options(); + + _ = element.add_event_listener_with_callback_and_add_event_listener_options( + &event_name, + closure_js.as_ref().unchecked_ref(), + &options, + ); + } + }, + WatchOptions::default().immediate(true), + ) + }; + + let stop = move || { + stop_watch(); + cleanup_prev_element(); + }; + + on_cleanup(stop.clone()); + + stop + }} } /// Options for [`use_event_listener_with_options`]. #[derive(DefaultBuilder, Default, Copy, Clone)] +#[cfg_attr(feature = "ssr", allow(dead_code))] pub struct UseEventListenerOptions { /// A boolean value indicating that events of this type will be dispatched to /// the registered `listener` before being dispatched to any `EventTarget` @@ -202,6 +212,7 @@ pub struct UseEventListenerOptions { } impl UseEventListenerOptions { + #[cfg_attr(feature = "ssr", allow(dead_code))] fn as_add_event_listener_options(&self) -> web_sys::AddEventListenerOptions { let UseEventListenerOptions { capture, diff --git a/src/use_intersection_observer.rs b/src/use_intersection_observer.rs index fd462c3..fda59bf 100644 --- a/src/use_intersection_observer.rs +++ b/src/use_intersection_observer.rs @@ -1,11 +1,15 @@ use crate::core::{ElementMaybeSignal, ElementsMaybeSignal}; -use crate::{watch_with_options, WatchOptions}; +use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::*; -use std::cell::RefCell; use std::marker::PhantomData; -use std::rc::Rc; -use wasm_bindgen::prelude::*; + +cfg_if! { if #[cfg(not(feature = "ssr"))] { + use crate::{watch_with_options, WatchOptions}; + use std::cell::RefCell; + use std::rc::Rc; + use wasm_bindgen::prelude::*; +}} /// Reactive [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver). /// @@ -44,7 +48,7 @@ use wasm_bindgen::prelude::*; /// /// ## Server-Side Rendering /// -/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements) +/// On the server this amounts to a no-op. /// /// ## See also /// @@ -66,6 +70,7 @@ where } /// Version of [`use_intersection_observer`] that takes a [`UseIntersectionObserverOptions`]. See [`use_intersection_observer`] for how to use. +#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))] pub fn use_intersection_observer_with_options( target: El, mut callback: F, @@ -86,107 +91,113 @@ where .. } = options; - let closure_js = Closure::::new( - move |entries: js_sys::Array, observer| { - callback( - entries - .to_vec() - .into_iter() - .map(|v| v.unchecked_into::()) - .collect(), - observer, - ); - }, - ) - .into_js_value(); - let (is_active, set_active) = create_signal(immediate); - let observer: Rc>> = Rc::new(RefCell::new(None)); - - let cleanup = { - let obsserver = Rc::clone(&observer); - - move || { - if let Some(o) = obsserver.take() { - o.disconnect(); - } - } - }; - - let targets = (target).into(); - let root = root.map(|root| (root).into()); - - let stop_watch = { - let cleanup = cleanup.clone(); - - watch_with_options( - move || { - ( - targets.get(), - root.as_ref().map(|root| root.get()), - is_active.get(), - ) - }, - move |values, _, _| { - let (targets, root, is_active) = values; - - cleanup(); - - if !is_active { - return; - } - - let mut options = web_sys::IntersectionObserverInit::new(); - options.root_margin(&root_margin).threshold( - &thresholds - .iter() - .copied() - .map(JsValue::from) - .collect::(), + cfg_if! { if #[cfg(feature = "ssr")] { + let pause = || {}; + let cleanup = || {}; + let stop = || {}; + } else { + let closure_js = Closure::::new( + move |entries: js_sys::Array, observer| { + callback( + entries + .to_vec() + .into_iter() + .map(|v| v.unchecked_into::()) + .collect(), + observer, ); - - if let Some(Some(root)) = root { - let root: web_sys::Element = root.clone().into(); - options.root(Some(&root)); - } - - let obs = web_sys::IntersectionObserver::new_with_options( - closure_js.clone().as_ref().unchecked_ref(), - &options, - ) - .expect("failed to create IntersectionObserver"); - - for target in targets.iter().flatten() { - let target: web_sys::Element = target.clone().into(); - obs.observe(&target); - } - - observer.replace(Some(obs)); }, - WatchOptions::default().immediate(immediate), ) - }; + .into_js_value(); - let stop = { - let cleanup = cleanup.clone(); + let observer: Rc>> = Rc::new(RefCell::new(None)); - move || { - cleanup(); - stop_watch(); - } - }; + let cleanup = { + let obsserver = Rc::clone(&observer); - on_cleanup(stop.clone()); + move || { + if let Some(o) = obsserver.take() { + o.disconnect(); + } + } + }; - let pause = { - let cleanup = cleanup.clone(); + let targets = (target).into(); + let root = root.map(|root| (root).into()); - move || { - cleanup(); - set_active.set(false); - } - }; + let stop_watch = { + let cleanup = cleanup.clone(); + + watch_with_options( + move || { + ( + targets.get(), + root.as_ref().map(|root| root.get()), + is_active.get(), + ) + }, + move |values, _, _| { + let (targets, root, is_active) = values; + + cleanup(); + + if !is_active { + return; + } + + let mut options = web_sys::IntersectionObserverInit::new(); + options.root_margin(&root_margin).threshold( + &thresholds + .iter() + .copied() + .map(JsValue::from) + .collect::(), + ); + + if let Some(Some(root)) = root { + let root: web_sys::Element = root.clone().into(); + options.root(Some(&root)); + } + + let obs = web_sys::IntersectionObserver::new_with_options( + closure_js.clone().as_ref().unchecked_ref(), + &options, + ) + .expect("failed to create IntersectionObserver"); + + for target in targets.iter().flatten() { + let target: web_sys::Element = target.clone().into(); + obs.observe(&target); + } + + observer.replace(Some(obs)); + }, + WatchOptions::default().immediate(immediate), + ) + }; + + let stop = { + let cleanup = cleanup.clone(); + + move || { + cleanup(); + stop_watch(); + } + }; + + on_cleanup(stop.clone()); + + let pause = { + let cleanup = cleanup.clone(); + + move || { + cleanup(); + set_active.set(false); + } + }; + }} UseIntersectionObserverReturn { is_active: is_active.into(), diff --git a/src/use_mouse.rs b/src/use_mouse.rs index b76b84b..5e4e17b 100644 --- a/src/use_mouse.rs +++ b/src/use_mouse.rs @@ -1,14 +1,13 @@ #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] use crate::core::{ElementMaybeSignal, Position}; -use crate::use_event_listener_with_options; +use crate::{use_event_listener_with_options, UseEventListenerOptions}; use cfg_if::cfg_if; use default_struct_builder::DefaultBuilder; use leptos::ev::{dragover, mousemove, touchend, touchmove, touchstart}; use leptos::*; use std::marker::PhantomData; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::AddEventListenerOptions; /// Reactive mouse position /// @@ -158,40 +157,39 @@ where cfg_if! { if #[cfg(not(feature = "ssr"))] { let target = options.target; - let mut event_listener_options = AddEventListenerOptions::new(); - event_listener_options.passive(true); + let event_listener_options = UseEventListenerOptions::default().passive(true); let _ = use_event_listener_with_options( target.clone(), mousemove, mouse_handler, - event_listener_options.clone(), + event_listener_options, ); let _ = use_event_listener_with_options( target.clone(), dragover, drag_handler, - event_listener_options.clone(), + event_listener_options, ); if options.touch && !matches!(options.coord_type, UseMouseCoordType::Movement) { let _ = use_event_listener_with_options( target.clone(), touchstart, touch_handler.clone(), - event_listener_options.clone(), + event_listener_options, ); let _ = use_event_listener_with_options( target.clone(), touchmove, touch_handler, - event_listener_options.clone(), + event_listener_options, ); if options.reset_on_touch_ends { let _ = use_event_listener_with_options( target, touchend, move |_| reset(), - event_listener_options.clone(), + event_listener_options, ); } } diff --git a/src/use_mutation_observer.rs b/src/use_mutation_observer.rs index beb217f..f518000 100644 --- a/src/use_mutation_observer.rs +++ b/src/use_mutation_observer.rs @@ -1,10 +1,14 @@ use crate::core::ElementsMaybeSignal; -use crate::use_supported; +use cfg_if::cfg_if; +use default_struct_builder::DefaultBuilder; use leptos::*; -use std::cell::RefCell; -use std::rc::Rc; use wasm_bindgen::prelude::*; -use web_sys::MutationObserverInit; + +cfg_if! { if #[cfg(not(feature = "ssr"))] { + use crate::use_supported; + use std::cell::RefCell; + use std::rc::Rc; +}} /// Reactive [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). /// @@ -19,16 +23,13 @@ use web_sys::MutationObserverInit; /// ``` /// # use leptos::*; /// # use leptos::html::Pre; -/// # use leptos_use::use_mutation_observer_with_options; +/// # use leptos_use::{use_mutation_observer_with_options, UseMutationObserverOptions}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { /// let el = create_node_ref::
();
 /// let (text, set_text) = create_signal("".to_string());
 ///
-/// let mut init = web_sys::MutationObserverInit::new();
-/// init.attributes(true);
-///
 /// use_mutation_observer_with_options(
 ///     el,
 ///     move |mutations, _| {
@@ -36,7 +37,7 @@ use web_sys::MutationObserverInit;
 ///             set_text.update(|text| *text = format!("{text}\n{:?}", mutation.attribute_name()));
 ///         }
 ///     },
-///     init,
+///     UseMutationObserverOptions::default().attributes(true),
 /// );
 ///
 /// view! {
@@ -47,7 +48,7 @@ use web_sys::MutationObserverInit;
 ///
 /// ## Server-Side Rendering
 ///
-/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements)
+/// On the server this amounts to a no-op.
 pub fn use_mutation_observer(
     target: El,
     callback: F,
@@ -57,84 +58,167 @@ where
     T: Into + Clone + 'static,
     F: FnMut(Vec, web_sys::MutationObserver) + 'static,
 {
-    use_mutation_observer_with_options(target, callback, MutationObserverInit::default())
+    use_mutation_observer_with_options(target, callback, UseMutationObserverOptions::default())
 }
 
-/// Version of [`use_mutation_observer`] that takes a `web_sys::MutationObserverInit`. See [`use_mutation_observer`] for how to use.
+/// Version of [`use_mutation_observer`] that takes a `UseMutationObserverOptions`. See [`use_mutation_observer`] for how to use.
+#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
 pub fn use_mutation_observer_with_options(
     target: El,
     mut callback: F,
-    options: web_sys::MutationObserverInit,
+    options: UseMutationObserverOptions,
 ) -> UseMutationObserverReturn
 where
     El: Into>,
     T: Into + Clone + 'static,
     F: FnMut(Vec, web_sys::MutationObserver) + 'static,
 {
-    let closure_js = Closure::::new(
-        move |entries: js_sys::Array, observer| {
-            callback(
-                entries
-                    .to_vec()
-                    .into_iter()
-                    .map(|v| v.unchecked_into::())
-                    .collect(),
-                observer,
-            );
-        },
-    )
-    .into_js_value();
-
-    let observer: Rc>> = Rc::new(RefCell::new(None));
-
-    let is_supported = use_supported(|| JsValue::from("MutationObserver").js_in(&window()));
-
-    let cleanup = {
-        let observer = Rc::clone(&observer);
-
-        move || {
-            let mut observer = observer.borrow_mut();
-            if let Some(o) = observer.as_ref() {
-                o.disconnect();
-                *observer = None;
-            }
+    cfg_if! { if #[cfg(feature = "ssr")] {
+        UseMutationObserverReturn {
+            is_supported: Signal::derive(|| true),
+            stop: || {},
         }
-    };
-
-    let targets = (target).into();
-
-    let stop_watch = {
-        let cleanup = cleanup.clone();
-
-        leptos::watch(
-            move || targets.get(),
-            move |targets, _, _| {
-                cleanup();
-
-                if is_supported.get() && !targets.is_empty() {
-                    let obs = web_sys::MutationObserver::new(closure_js.as_ref().unchecked_ref())
-                        .expect("failed to create MutationObserver");
-
-                    for target in targets.iter().flatten() {
-                        let target: web_sys::Element = target.clone().into();
-                        let _ = obs.observe_with_options(&target, &options.clone());
-                    }
-
-                    observer.replace(Some(obs));
-                }
+    } else {
+        let closure_js = Closure::::new(
+            move |entries: js_sys::Array, observer| {
+                callback(
+                    entries
+                        .to_vec()
+                        .into_iter()
+                        .map(|v| v.unchecked_into::())
+                        .collect(),
+                    observer,
+                );
             },
-            false,
         )
-    };
+        .into_js_value();
 
-    let stop = move || {
-        cleanup();
-        stop_watch();
-    };
+        let observer: Rc>> = Rc::new(RefCell::new(None));
 
-    on_cleanup(stop.clone());
+        let is_supported = use_supported(|| JsValue::from("MutationObserver").js_in(&window()));
 
-    UseMutationObserverReturn { is_supported, stop }
+        let cleanup = {
+            let observer = Rc::clone(&observer);
+
+            move || {
+                let mut observer = observer.borrow_mut();
+                if let Some(o) = observer.as_ref() {
+                    o.disconnect();
+                    *observer = None;
+                }
+            }
+        };
+
+        let targets = (target).into();
+
+        let stop_watch = {
+            let cleanup = cleanup.clone();
+
+            leptos::watch(
+                move || targets.get(),
+                move |targets, _, _| {
+                    cleanup();
+
+                    if is_supported.get() && !targets.is_empty() {
+                        let obs = web_sys::MutationObserver::new(closure_js.as_ref().unchecked_ref())
+                            .expect("failed to create MutationObserver");
+
+                        for target in targets.iter().flatten() {
+                            let target: web_sys::Element = target.clone().into();
+                            let _ = obs.observe_with_options(&target, &options.clone().into());
+                        }
+
+                        observer.replace(Some(obs));
+                    }
+                },
+                false,
+            )
+        };
+
+        let stop = move || {
+            cleanup();
+            stop_watch();
+        };
+
+        on_cleanup(stop.clone());
+
+        UseMutationObserverReturn { is_supported, stop }
+    }}
+}
+
+/// Options for [`use_mutation_observer_with_options`].
+#[derive(DefaultBuilder, Clone, Default)]
+pub struct UseMutationObserverOptions {
+    /// Set to `true` to extend monitoring to the entire subtree of nodes rooted at `target`.
+    /// All of the other properties are then extended to all of the nodes in the subtree
+    /// instead of applying solely to the `target` node. The default value is `false`.
+    subtree: bool,
+
+    /// Set to `true` to monitor the target node (and, if `subtree` is `true`, its descendants)
+    /// for the addition of new child nodes or removal of existing child nodes.
+    /// The default value is `false`.
+    child_list: bool,
+
+    /// Set to `true` to watch for changes to the value of attributes on the node or nodes being
+    /// monitored. The default value is `true` if either of `attribute_filter` or
+    /// `attribute_old_value` is specified, otherwise the default value is `false`.
+    attributes: bool,
+
+    /// An array of specific attribute names to be monitored. If this property isn't included,
+    /// changes to all attributes cause mutation notifications.
+    #[builder(into)]
+    attribute_filter: Option>,
+
+    /// Set to `true` to record the previous value of any attribute that changes when monitoring
+    /// the node or nodes for attribute changes; See
+    /// [Monitoring attribute values](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#monitoring_attribute_values)
+    /// for an example of watching for attribute changes and recording values.
+    /// The default value is `false`.
+    attribute_old_value: bool,
+
+    /// Set to `true` to monitor the specified target node
+    /// (and, if `subtree` is `true`, its descendants)
+    /// for changes to the character data contained within the node or nodes.
+    /// The default value is `true` if `character_data_old_value` is specified,
+    /// otherwise the default value is `false`.
+    #[builder(into)]
+    character_data: Option,
+
+    /// Set to `true` to record the previous value of a node's text whenever the text changes on
+    /// nodes being monitored. The default value is `false`.
+    character_data_old_value: bool,
+}
+
+impl From for web_sys::MutationObserverInit {
+    fn from(val: UseMutationObserverOptions) -> Self {
+        let UseMutationObserverOptions {
+            subtree,
+            child_list,
+            attributes,
+            attribute_filter,
+            attribute_old_value,
+            character_data,
+            character_data_old_value,
+        } = val;
+
+        let mut init = Self::new();
+
+        init.subtree(subtree)
+            .child_list(child_list)
+            .attributes(attributes)
+            .attribute_old_value(attribute_old_value)
+            .character_data_old_value(character_data_old_value);
+
+        if let Some(attribute_filter) = attribute_filter {
+            let array = js_sys::Array::from_iter(attribute_filter.into_iter().map(JsValue::from));
+            init.attribute_filter(array.unchecked_ref());
+        }
+        if let Some(character_data) = character_data {
+            init.character_data(character_data);
+        }
+
+        init
+    }
 }
 
 /// The return value of [`use_mutation_observer`].
diff --git a/src/use_resize_observer.rs b/src/use_resize_observer.rs
index 9dfdaf3..1c61081 100644
--- a/src/use_resize_observer.rs
+++ b/src/use_resize_observer.rs
@@ -1,10 +1,14 @@
 use crate::core::ElementsMaybeSignal;
-use crate::use_supported;
+use cfg_if::cfg_if;
 use default_struct_builder::DefaultBuilder;
 use leptos::*;
-use std::cell::RefCell;
-use std::rc::Rc;
-use wasm_bindgen::prelude::*;
+
+cfg_if! { if #[cfg(not(feature = "ssr"))] {
+    use crate::use_supported;
+    use std::cell::RefCell;
+    use std::rc::Rc;
+    use wasm_bindgen::prelude::*;
+}}
 
 /// Reports changes to the dimensions of an Element's content or the border-box.
 ///
@@ -45,7 +49,7 @@ use wasm_bindgen::prelude::*;
 ///
 /// ## Server-Side Rendering
 ///
-/// Please refer to ["Functions with Target Elements"](https://leptos-use.rs/server_side_rendering.html#functions-with-target-elements)
+/// On the server this amounts to a no-op.
 ///
 /// ## See also
 ///
@@ -63,6 +67,7 @@ where
 }
 
 /// Version of [`use_resize_observer`] that takes a `web_sys::ResizeObserverOptions`. See [`use_resize_observer`] for how to use.
+#[cfg_attr(feature = "ssr", allow(unused_variables, unused_mut))]
 pub fn use_resize_observer_with_options(
     target: El, // TODO : multiple elements?
     mut callback: F,
@@ -73,92 +78,95 @@ where
     T: Into + Clone + 'static,
     F: FnMut(Vec, web_sys::ResizeObserver) + 'static,
 {
-    let closure_js = Closure::::new(
-        move |entries: js_sys::Array, observer| {
-            callback(
-                entries
-                    .to_vec()
-                    .into_iter()
-                    .map(|v| v.unchecked_into::())
-                    .collect(),
-                observer,
-            );
-        },
-    )
-    .into_js_value();
-
-    let observer: Rc>> = Rc::new(RefCell::new(None));
-
-    let is_supported = use_supported(|| JsValue::from("ResizeObserver").js_in(&window()));
-
-    let cleanup = {
-        let observer = Rc::clone(&observer);
-
-        move || {
-            let mut observer = observer.borrow_mut();
-            if let Some(o) = observer.as_ref() {
-                o.disconnect();
-                *observer = None;
-            }
+    cfg_if! { if #[cfg(feature = "ssr")] {
+        UseResizeObserverReturn {
+            is_supported: Signal::derive(|| true),
+            stop: || {}
         }
-    };
-
-    let targets = (target).into();
-
-    let stop_watch = {
-        let cleanup = cleanup.clone();
-
-        watch(
-            move || targets.get(),
-            move |targets, _, _| {
-                cleanup();
-
-                if is_supported.get() && !targets.is_empty() {
-                    let obs =
-                        web_sys::ResizeObserver::new(closure_js.clone().as_ref().unchecked_ref())
-                            .expect("failed to create ResizeObserver");
-
-                    for target in targets.iter().flatten() {
-                        let target: web_sys::Element = target.clone().into();
-                        obs.observe_with_options(&target, &options.clone().into());
-                    }
-
-                    observer.replace(Some(obs));
-                }
+    } else {
+        let closure_js = Closure::::new(
+            move |entries: js_sys::Array, observer| {
+                callback(
+                    entries
+                        .to_vec()
+                        .into_iter()
+                        .map(|v| v.unchecked_into::())
+                        .collect(),
+                    observer,
+                );
             },
-            false,
         )
-    };
+        .into_js_value();
 
-    let stop = move || {
-        cleanup();
-        stop_watch();
-    };
+        let observer: Rc>> = Rc::new(RefCell::new(None));
 
-    on_cleanup(stop.clone());
+        let is_supported = use_supported(|| JsValue::from("ResizeObserver").js_in(&window()));
 
-    UseResizeObserverReturn { is_supported, stop }
+        let cleanup = {
+            let observer = Rc::clone(&observer);
+
+            move || {
+                let mut observer = observer.borrow_mut();
+                if let Some(o) = observer.as_ref() {
+                    o.disconnect();
+                    *observer = None;
+                }
+            }
+        };
+
+        let targets = (target).into();
+
+        let stop_watch = {
+            let cleanup = cleanup.clone();
+
+            watch(
+                move || targets.get(),
+                move |targets, _, _| {
+                    cleanup();
+
+                    if is_supported.get() && !targets.is_empty() {
+                        let obs =
+                            web_sys::ResizeObserver::new(closure_js.clone().as_ref().unchecked_ref())
+                                .expect("failed to create ResizeObserver");
+
+                        for target in targets.iter().flatten() {
+                            let target: web_sys::Element = target.clone().into();
+                            obs.observe_with_options(&target, &options.clone().into());
+                        }
+
+                        observer.replace(Some(obs));
+                    }
+                },
+                false,
+            )
+        };
+
+        let stop = move || {
+            cleanup();
+            stop_watch();
+        };
+
+        on_cleanup(stop.clone());
+
+        UseResizeObserverReturn { is_supported, stop }
+    }}
 }
 
 /// Options for [`use_resize_observer_with_options`].
-#[derive(DefaultBuilder, Clone)]
+#[derive(DefaultBuilder, Clone, Default)]
 pub struct UseResizeObserverOptions {
     /// The box that is used to determine the dimensions of the target. Defaults to `ContentBox`.
-    pub box_: web_sys::ResizeObserverBoxOptions,
-}
-
-impl Default for UseResizeObserverOptions {
-    fn default() -> Self {
-        Self {
-            box_: web_sys::ResizeObserverBoxOptions::ContentBox,
-        }
-    }
+    #[builder(into)]
+    pub box_: Option,
 }
 
 impl From for web_sys::ResizeObserverOptions {
     fn from(val: UseResizeObserverOptions) -> Self {
         let mut options = web_sys::ResizeObserverOptions::new();
-        options.box_(val.box_);
+        options.box_(
+            val.box_
+                .unwrap_or(web_sys::ResizeObserverBoxOptions::ContentBox),
+        );
         options
     }
 }
diff --git a/src/use_scroll.rs b/src/use_scroll.rs
index 9913fb8..94c2c12 100644
--- a/src/use_scroll.rs
+++ b/src/use_scroll.rs
@@ -1,13 +1,25 @@
 use crate::core::ElementMaybeSignal;
-use crate::use_event_listener::use_event_listener_with_options;
-use crate::{use_debounce_fn_with_arg, use_throttle_fn_with_arg_and_options, ThrottleOptions};
+use crate::UseEventListenerOptions;
 use cfg_if::cfg_if;
 use default_struct_builder::DefaultBuilder;
-use leptos::ev::scrollend;
 use leptos::*;
 use std::rc::Rc;
+
+cfg_if! { if #[cfg(not(feature = "ssr"))] {
+use crate::use_event_listener::use_event_listener_with_options;
+use crate::{
+    use_debounce_fn_with_arg, use_throttle_fn_with_arg_and_options, ThrottleOptions,
+};
+use leptos::ev::scrollend;
 use wasm_bindgen::JsCast;
 
+/// We have to check if the scroll amount is close enough to some threshold in order to
+/// more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
+/// numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
+/// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
+const ARRIVED_STATE_THRESHOLD_PIXELS: f64 = 1.0;
+}}
+
 /// Reactive scroll position and state.
 ///
 /// ## Demo
@@ -172,7 +184,7 @@ where
 }
 
 /// Version of [`use_scroll`] with options. See [`use_scroll`] for how to use.
-#[allow(unused_variables)]
+#[cfg_attr(feature = "ssr", allow(unused_variables))]
 pub fn use_scroll_with_options(element: El, options: UseScrollOptions) -> UseScrollReturn
 where
     El: Clone,
@@ -377,7 +389,7 @@ where
                 target,
                 ev::scroll,
                 handler,
-                options.event_listener_options.clone().unwrap_or_default(),
+                options.event_listener_options,
             );
         } else {
             let _ = use_event_listener_with_options::<
@@ -389,7 +401,7 @@ where
                 target,
                 ev::scroll,
                 on_scroll_handler,
-                options.event_listener_options.clone().unwrap_or_default(),
+                options.event_listener_options,
             );
         }
 
@@ -402,7 +414,7 @@ where
             target,
             scrollend,
             on_scroll_end,
-            options.event_listener_options.unwrap_or_default(),
+            options.event_listener_options,
         );
 
         let measure = Box::new(move || {
@@ -426,15 +438,10 @@ where
     }
 }
 
-/// We have to check if the scroll amount is close enough to some threshold in order to
-/// more accurately calculate arrivedState. This is because scrollTop/scrollLeft are non-rounded
-/// numbers, while scrollHeight/scrollWidth and clientHeight/clientWidth are rounded.
-/// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
-const ARRIVED_STATE_THRESHOLD_PIXELS: f64 = 1.0;
-
 /// Options for [`use_scroll`].
 #[derive(DefaultBuilder)]
 /// Options for [`use_scroll_with_options`].
+#[cfg_attr(feature = "ssr", allow(dead_code))]
 pub struct UseScrollOptions {
     /// Throttle time in milliseconds for the scroll events. Defaults to 0 (disabled).
     throttle: f64,
@@ -453,8 +460,7 @@ pub struct UseScrollOptions {
     on_stop: Rc,
 
     /// Options passed to the `addEventListener("scroll", ...)` call
-    #[builder(into)]
-    event_listener_options: Option,
+    event_listener_options: UseEventListenerOptions,
 
     /// When changing the `x` or `y` signals this specifies the scroll behaviour.
     /// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`.
diff --git a/src/use_window.rs b/src/use_window.rs
index 42c31ff..b9dae5a 100644
--- a/src/use_window.rs
+++ b/src/use_window.rs
@@ -1,8 +1,10 @@
 use crate::{use_document, UseDocument};
 use cfg_if::cfg_if;
-use leptos::*;
 use std::ops::Deref;
 
+#[cfg(not(feature = "ssr"))]
+use leptos::*;
+
 /// SSR safe `window()`.
 /// This returns just a new-type wrapper around `Option`.
 /// Calling this amounts to `None` on the server and `Some(Window)` on the client.
diff --git a/src/use_window_scroll.rs b/src/use_window_scroll.rs
index 8906a38..9b1fbd9 100644
--- a/src/use_window_scroll.rs
+++ b/src/use_window_scroll.rs
@@ -1,10 +1,9 @@
 #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
 
-use crate::use_event_listener_with_options;
+use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions};
 use cfg_if::cfg_if;
 use leptos::ev::scroll;
 use leptos::*;
-use web_sys::AddEventListenerOptions;
 
 /// Reactive window scroll.
 ///
@@ -40,21 +39,17 @@ pub fn use_window_scroll() -> (Signal, Signal) {
     let (x, set_x) = create_signal(initial_x);
     let (y, set_y) = create_signal(initial_y);
 
-    cfg_if! { if #[cfg(not(feature = "ssr"))] {
-        let mut options = AddEventListenerOptions::new();
-        options.capture(false);
-        options.passive(true);
-
-        let _ = use_event_listener_with_options(
-                        window(),
-            scroll,
-            move |_| {
-                set_x.set(window().scroll_x().unwrap_or_default());
-                set_y.set(window().scroll_y().unwrap_or_default());
-            },
-            options,
-        );
-    }}
+    let _ = use_event_listener_with_options(
+        use_window(),
+        scroll,
+        move |_| {
+            set_x.set(window().scroll_x().unwrap_or_default());
+            set_y.set(window().scroll_y().unwrap_or_default());
+        },
+        UseEventListenerOptions::default()
+            .capture(false)
+            .passive(true),
+    );
 
     (x.into(), y.into())
 }