diff --git a/CHANGELOG.md b/CHANGELOG.md index 047d98b..a2c83d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.1.4 + +#### New Functions +- `use_supported` +- `use_resize_observer` +- `watch` + ## 0.1.3 #### New Functions diff --git a/Cargo.toml b/Cargo.toml index 180bc0c..cedd725 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,23 @@ repository = "https://github.com/Synphonyte/leptos-use" [dependencies] leptos = "0.3.0" -web-sys = { version = "0.3.63", features = ["ScrollToOptions", "ScrollBehavior", "CssStyleDeclaration"] } wasm-bindgen = "0.2.86" js-sys = "0.3.63" default-struct-builder = "0.1.0" +[dependencies.web-sys] +version = "0.3.63" +features = [ + "ScrollToOptions", + "ScrollBehavior", + "CssStyleDeclaration", + "ResizeObserver", + "ResizeObserverOptions", + "ResizeObserverBoxOptions", + "ResizeObserverSize", + "ResizeObserverEntry", + "Navigator", +] + [features] docs = [] \ No newline at end of file diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 2b5eb9f..8d86c14 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -16,7 +16,12 @@ - [use_scroll](sensors/use_scroll.md) +# Watch + +- [watch](watch/watch.md) + # Utilities - [use_debounce_fn](utilities/use_debounce_fn.md) +- [use_supported](utilities/use_supported.md) - [use_throttle_fn](utilities/use_throttle_fn.md) \ No newline at end of file diff --git a/docs/book/src/custom.css b/docs/book/src/custom.css index 78faaf9..e3844ec 100644 --- a/docs/book/src/custom.css +++ b/docs/book/src/custom.css @@ -20,4 +20,17 @@ pre > code { border-radius: 5px; padding: 1.5rem; -} \ No newline at end of file +} + +.meta-data { + font-size: 87.5%; + display: grid; + grid-template-columns: 70px auto; + gap: 2rem; + align-items: flex-start; + margin: 2rem 0 4rem; +} + +.meta-data > div:first-child { + opacity: 0.5; +} diff --git a/docs/book/src/extract_doc_comment.py b/docs/book/src/extract_doc_comment.py index 436fb18..7215c4d 100644 --- a/docs/book/src/extract_doc_comment.py +++ b/docs/book/src/extract_doc_comment.py @@ -1,8 +1,18 @@ import sys import re +import os def main(): + category = os.path.split(os.getcwd())[-1] + + print(f""" +
+
Category
+
{category.title()}
+
+""") + name = sys.argv[1] file_name = f"../../../../src/{name}.rs" @@ -49,8 +59,11 @@ def add_source_paragraph(name): demo_url = f"https://github.com/Synphonyte/leptos-use/tree/main/examples/{name}" docs_url = f"https://docs.rs/leptos-use/latest/leptos_use/fn.{name}.html" + demo_link = " • Demo" if os.path.isdir( + os.path.join("..", "..", "..", "..", "examples", name)) else "" + print( - f"SourceDemoDocs") + f"Source{demo_link} • Docs") interal_doc_link_pattern = re.compile(r"\[`([^]]+)\`](?!\()") diff --git a/docs/book/src/utilities/use_supported.md b/docs/book/src/utilities/use_supported.md new file mode 100644 index 0000000..581e8bd --- /dev/null +++ b/docs/book/src/utilities/use_supported.md @@ -0,0 +1,3 @@ +# use_supported + + diff --git a/docs/book/src/watch/watch.md b/docs/book/src/watch/watch.md new file mode 100644 index 0000000..6087512 --- /dev/null +++ b/docs/book/src/watch/watch.md @@ -0,0 +1,3 @@ +# watch + + diff --git a/src/lib.rs b/src/lib.rs index a9a3552..3d44d29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,25 @@ //! Collection of essential Leptos utilities inspired by SolidJS USE / VueUse pub mod core; -mod use_debounce_fn; -mod use_event_listener; -mod use_scroll; -mod use_throttle_fn; -pub mod utils; - #[cfg(feature = "docs")] pub mod docs; +mod use_debounce_fn; +mod use_event_listener; +#[cfg(web_sys_unstable_apis)] +mod use_resize_observer; +mod use_scroll; +mod use_supported; +mod use_throttle_fn; +pub mod utils; +mod watch; pub use use_debounce_fn::*; pub use use_event_listener::*; +#[cfg(web_sys_unstable_apis)] +pub use use_resize_observer::*; pub use use_scroll::*; +pub use use_supported::*; pub use use_throttle_fn::*; +pub use watch::*; extern crate self as leptos_use; diff --git a/src/use_debounce_fn.rs b/src/use_debounce_fn.rs index dece312..ed1184a 100644 --- a/src/use_debounce_fn.rs +++ b/src/use_debounce_fn.rs @@ -64,7 +64,7 @@ use leptos::MaybeSignal; /// ## Recommended Reading /// /// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) -pub fn use_debounce_fn(func: F, ms: impl Into>) -> impl Fn() +pub fn use_debounce_fn(func: F, ms: impl Into>) -> impl Fn() + Clone where F: FnOnce() + Clone + 'static, { @@ -76,7 +76,7 @@ pub fn use_debounce_fn_with_options( func: F, ms: impl Into>, options: DebounceOptions, -) -> impl Fn() +) -> impl Fn() + Clone where F: FnOnce() + Clone + 'static, { diff --git a/src/use_resize_observer.rs b/src/use_resize_observer.rs new file mode 100644 index 0000000..8c980c7 --- /dev/null +++ b/src/use_resize_observer.rs @@ -0,0 +1,59 @@ +use crate::core::ElementMaybeSignal; +use crate::use_supported; +use default_struct_builder::DefaultBuilder; +use leptos::*; +use std::cell::RefCell; +use std::rc::Rc; +use wasm_bindgen::{JsCast, JsValue}; + +pub fn use_resize_observer_with_options( + cx: Scope, + target: El, // TODO : multiple elements? + callback: F, + options: web_sys::ResizeObserverOptions, +) where + (Scope, El): Into>, + T: Into + Clone + 'static, + F: FnMut(Vec, web_sys::ResizeObserver) + 'static, +{ + let observer: Rc>> = Rc::new(RefCell::new(None)); + + let is_supported = use_supported(cx, || JsValue::from("ResizeObserver").js_in(&window())); + + let obs = Rc::clone(&observer); + let cleanup = move || { + let observer = obs.borrow_mut(); + if let Some(o) = *observer { + o.disconnect(); + *observer = None; + } + }; + + let target = target.into(); + + let clean = cleanup.clone(); + create_effect(cx, move |_| { + clean(); + + if is_supported() { + let obs = web_sys::ResizeObserver::new(move |entries: &js_sys::Array, observer| { + callback( + entries + .to_vec() + .into_iter() + .map(|v| v.unchecked_into::(&options)), + observer, + ); + }) + .expect("failed to create ResizeObserver"); + + observer.observe(&target.get()); + + observer.replace(obs); + } + }); + + let stop = move || { + cleanup(); + }; +} diff --git a/src/use_supported.rs b/src/use_supported.rs new file mode 100644 index 0000000..f8dc29c --- /dev/null +++ b/src/use_supported.rs @@ -0,0 +1,26 @@ +use leptos::*; + +/// SSR compatibe `is_supported` +/// +/// ## Usage +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::use_supported; +/// # use wasm_bindgen::JsValue; +/// # +/// # pub fn Demo(cx: Scope) -> impl IntoView { +/// let is_supported = use_supported( +/// cx, +/// || JsValue::from("getBattery").js_in(&window().navigator()) +/// ); +/// +/// if is_supported() { +/// // do something +/// } +/// # view! { cx, } +/// # } +/// ``` +pub fn use_supported(cx: Scope, callback: impl Fn() -> bool + 'static) -> Signal { + Signal::derive(cx, callback) +} diff --git a/src/watch.rs b/src/watch.rs new file mode 100644 index 0000000..30a4c24 --- /dev/null +++ b/src/watch.rs @@ -0,0 +1,84 @@ +use leptos::*; +use std::cell::RefCell; + +/// A version of `create_effect` that listens to any dependency that is accessed inside `deps`. +/// Also a stop handler is returned. +/// If `immediate` is false, the `callback` will not run immediately but only after +/// the first change is detected of any signal that is accessed in `deps`. +/// The return value of `deps` is passed into `callback` as an argument together with the previous value +/// and the previous value that the `callback` itself returned last time. +/// +/// # Usage +/// +/// ``` +/// # use std::time::Duration; +/// # use leptos::*; +/// # use leptos_use::watch; +/// # +/// # pub fn Demo(cx: Scope) -> impl IntoView { +/// let (num, set_num) = create_signal(cx, 0); +/// +/// let stop = watch( +/// cx, +/// num, +/// move |num, _, _| { +/// log!("number {}", num); +/// }, +/// true, +/// ); +/// +/// set_num(1); // > "number 1" +/// +/// set_timeout_with_handle(move || { +/// stop(); // stop watching +/// +/// set_num(2); // nothing happens +/// }, Duration::from_millis(1000)); +/// # view! { cx, } +/// # } +/// ``` +pub fn watch( + cx: Scope, + deps: DFn, + callback: CFn, + immediate: bool, +) -> impl Fn() + Clone +where + DFn: Fn() -> W + 'static, + CFn: Fn(&W, Option<&W>, Option) -> T + 'static, + W: 'static, + T: 'static, +{ + let (is_active, set_active) = create_signal(cx, true); + + let prev_deps_value: RefCell> = RefCell::new(None); + let prev_callback_value: RefCell> = RefCell::new(None); + + create_effect(cx, move |did_run_before| { + if !is_active() { + return (); + } + + let deps_value = deps(); + + if !immediate && did_run_before.is_none() { + prev_deps_value.replace(Some(deps_value)); + return (); + } + + let callback_value = callback( + &deps_value, + prev_deps_value.borrow().as_ref(), + prev_callback_value.take(), + ); + prev_callback_value.replace(Some(callback_value)); + + prev_deps_value.replace(Some(deps_value)); + + () + }); + + move || { + set_active(false); + } +}