diff --git a/CHANGELOG.md b/CHANGELOG.md index d9712d4..ea515f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.1.10 + +#### New Functions +- `use_local_storage` + +#### Changed Functions +- `watch` has now variant `watch_with_options` which allows for debouncing and throttling + ## 0.1.8/9 - Fixed documentation and doc tests running for functions behind `#[cfg(web_sys_unstable_apis)]` diff --git a/src/use_debounce_fn.rs b/src/use_debounce_fn.rs index ed1184a..f6b0511 100644 --- a/src/use_debounce_fn.rs +++ b/src/use_debounce_fn.rs @@ -1,6 +1,8 @@ pub use crate::utils::DebounceOptions; use crate::utils::{create_filter_wrapper, create_filter_wrapper_with_arg, debounce_filter}; use leptos::MaybeSignal; +use std::cell::RefCell; +use std::rc::Rc; /// Debounce execution of a function. /// @@ -26,7 +28,7 @@ use leptos::MaybeSignal; /// 1000.0, /// ); /// -/// window_event_listener(resize, move |_| debounced_fn()); +/// window_event_listener(resize, move |_| { debounced_fn(); }); /// # view! { cx, } /// # } /// ``` @@ -51,7 +53,7 @@ use leptos::MaybeSignal; /// .max_wait(Some(5000.0)), /// ); /// -/// window_event_listener(resize, move |_| debounced_fn()); +/// window_event_listener(resize, move |_| { debounced_fn(); }); /// # view! { cx, } /// # } /// ``` @@ -64,46 +66,53 @@ 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() + Clone +pub fn use_debounce_fn( + func: F, + ms: impl Into> + 'static, +) -> impl Fn() -> Rc>> + Clone where - F: FnOnce() + Clone + 'static, + F: FnOnce() -> R + Clone + 'static, + R: 'static, { use_debounce_fn_with_options(func, ms, DebounceOptions::default()) } /// Version of [`use_debounce_fn`] with debounce options. See the docs for [`use_debounce_fn`] for how to use. -pub fn use_debounce_fn_with_options( +pub fn use_debounce_fn_with_options( func: F, - ms: impl Into>, + ms: impl Into> + 'static, options: DebounceOptions, -) -> impl Fn() + Clone +) -> impl Fn() -> Rc>> + Clone where - F: FnOnce() + Clone + 'static, + F: FnOnce() -> R + Clone + 'static, + R: 'static, { - create_filter_wrapper(debounce_filter(ms, options), func) + create_filter_wrapper(Box::new(debounce_filter(ms, options)), func) } /// Version of [`use_debounce_fn`] with an argument for the debounced function. See the docs for [`use_debounce_fn`] for how to use. -pub fn use_debounce_fn_with_arg( +pub fn use_debounce_fn_with_arg( func: F, - ms: impl Into>, -) -> impl Fn(Arg) + Clone + ms: impl Into> + 'static, +) -> impl Fn(Arg) -> Rc>> + Clone where - F: FnOnce(Arg) + Clone + 'static, + F: FnOnce(Arg) -> R + Clone + 'static, Arg: Clone + 'static, + R: 'static, { use_debounce_fn_with_arg_and_options(func, ms, DebounceOptions::default()) } /// Version of [`use_debounce_fn_with_arg`] with debounce options. -pub fn use_debounce_fn_with_arg_and_options( +pub fn use_debounce_fn_with_arg_and_options( func: F, - ms: impl Into>, + ms: impl Into> + 'static, options: DebounceOptions, -) -> impl Fn(Arg) + Clone +) -> impl Fn(Arg) -> Rc>> + Clone where - F: FnOnce(Arg) + Clone + 'static, + F: FnOnce(Arg) -> R + Clone + 'static, Arg: Clone + 'static, + R: 'static, { - create_filter_wrapper_with_arg(debounce_filter(ms, options), func) + create_filter_wrapper_with_arg(Box::new(debounce_filter(ms, options)), func) } diff --git a/src/use_element_size.rs b/src/use_element_size.rs index 2615439..26322d0 100644 --- a/src/use_element_size.rs +++ b/src/use_element_size.rs @@ -3,6 +3,7 @@ use crate::watch; use crate::{use_resize_observer_with_options, UseResizeObserverOptions}; use default_struct_builder::DefaultBuilder; use leptos::*; +use leptos_use::{watch_with_options, WatchOptions}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsCast; @@ -139,7 +140,7 @@ where options.into(), ); - let _ = watch( + let _ = watch_with_options( cx, move || targ.get(), move |ele, _, _| { @@ -151,7 +152,7 @@ where set_height(0.0); } }, - false, + WatchOptions::default().immediate(false), ); UseElementSizeReturn { diff --git a/src/use_resize_observer.rs b/src/use_resize_observer.rs index ad63851..ad1e1f5 100644 --- a/src/use_resize_observer.rs +++ b/src/use_resize_observer.rs @@ -120,7 +120,6 @@ where } } }, - true, ); let stop = move || { diff --git a/src/use_throttle_fn.rs b/src/use_throttle_fn.rs index f007877..01ab71c 100644 --- a/src/use_throttle_fn.rs +++ b/src/use_throttle_fn.rs @@ -1,6 +1,4 @@ -use crate::utils::{ - create_filter_wrapper_with_return, create_filter_wrapper_with_return_and_arg, throttle_filter, -}; +use crate::utils::{create_filter_wrapper, create_filter_wrapper_with_arg, throttle_filter}; use leptos::MaybeSignal; use std::cell::RefCell; use std::rc::Rc; @@ -66,7 +64,7 @@ pub use crate::utils::ThrottleOptions; /// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) pub fn use_throttle_fn( func: F, - ms: impl Into>, + ms: impl Into> + 'static, ) -> impl Fn() -> Rc>> + Clone where F: FnOnce() -> R + Clone + 'static, @@ -78,20 +76,20 @@ where /// Version of [`use_throttle_fn`] with throttle options. See the docs for [`use_throttle_fn`] for how to use. pub fn use_throttle_fn_with_options( func: F, - ms: impl Into>, + ms: impl Into> + 'static, options: ThrottleOptions, ) -> impl Fn() -> Rc>> + Clone where F: FnOnce() -> R + Clone + 'static, R: 'static, { - create_filter_wrapper_with_return(throttle_filter(ms, options), func) + create_filter_wrapper(Box::new(throttle_filter(ms, options)), func) } /// Version of [`use_throttle_fn`] with an argument for the throttled function. See the docs for [`use_throttle_fn`] for how to use. pub fn use_throttle_fn_with_arg( func: F, - ms: impl Into>, + ms: impl Into> + 'static, ) -> impl Fn(Arg) -> Rc>> + Clone where F: FnOnce(Arg) -> R + Clone + 'static, @@ -104,7 +102,7 @@ where /// Version of [`use_throttle_fn_with_arg`] with throttle options. See the docs for [`use_throttle_fn`] for how to use. pub fn use_throttle_fn_with_arg_and_options( func: F, - ms: impl Into>, + ms: impl Into> + 'static, options: ThrottleOptions, ) -> impl Fn(Arg) -> Rc>> + Clone where @@ -112,5 +110,5 @@ where Arg: Clone + 'static, R: 'static, { - create_filter_wrapper_with_return_and_arg(throttle_filter(ms, options), func) + create_filter_wrapper_with_arg(Box::new(throttle_filter(ms, options)), func) } diff --git a/src/utils/clonable_fn.rs b/src/utils/clonable_fn.rs index 45965df..4e8510e 100644 --- a/src/utils/clonable_fn.rs +++ b/src/utils/clonable_fn.rs @@ -24,27 +24,27 @@ impl Default for Box> { } } -pub trait CloneableFnWithReturnAndArg: FnOnce(Arg) -> R { - fn clone_box(&self) -> Box>; +pub trait CloneableFnWithArgAndReturn: FnOnce(Arg) -> R { + fn clone_box(&self) -> Box>; } -impl CloneableFnWithReturnAndArg for F +impl CloneableFnWithArgAndReturn for F where F: FnMut(Arg) -> R + Clone + 'static, R: 'static, { - fn clone_box(&self) -> Box> { + fn clone_box(&self) -> Box> { Box::new(self.clone()) } } -impl Clone for Box> { +impl Clone for Box> { fn clone(&self) -> Self { (**self).clone_box() } } -impl Default for Box> { +impl Default for Box> { fn default() -> Self { Box::new(|_| Default::default()) } diff --git a/src/utils/filters/debounce.rs b/src/utils/filters/debounce.rs index f252cbb..3c8f722 100644 --- a/src/utils/filters/debounce.rs +++ b/src/utils/filters/debounce.rs @@ -6,7 +6,7 @@ use std::cell::{Cell, RefCell}; use std::rc::Rc; use std::time::Duration; -#[derive(Clone, DefaultBuilder)] +#[derive(Copy, Clone, DefaultBuilder)] pub struct DebounceOptions { /// The maximum time allowed to be delayed before it's invoked. /// In milliseconds. @@ -22,12 +22,16 @@ impl Default for DebounceOptions { } } -pub fn debounce_filter( +pub fn debounce_filter( ms: impl Into>, options: DebounceOptions, -) -> impl Fn(Box>) -> Rc>> + Clone { +) -> impl Fn(Box>) -> Rc>> + Clone +where + R: 'static, +{ let timer = Rc::new(Cell::new(None::)); let max_timer = Rc::new(Cell::new(None::)); + let last_return_value: Rc>> = Rc::new(RefCell::new(None)); let clear_timeout = move |timer: &Rc>>| { if let Some(handle) = timer.get() { @@ -39,11 +43,17 @@ pub fn debounce_filter( let ms = ms.into(); let max_wait_signal = options.max_wait; - move |invoke: Box>| { + move |_invoke: Box>| { let duration = ms.get_untracked(); let max_duration = max_wait_signal.get_untracked(); - // TODO : return value like throttle_filter? + let last_return_val = Rc::clone(&last_return_value); + let invoke = move || { + let return_value = _invoke(); + + let mut val_mut = last_return_val.borrow_mut(); + *val_mut = Some(return_value); + }; clear_timeout(&timer); @@ -51,7 +61,7 @@ pub fn debounce_filter( clear_timeout(&max_timer); invoke(); - return Rc::new(RefCell::new(None)); + return Rc::clone(&last_return_value); } // Create the max_timer. Clears the regular timer on invoke @@ -86,6 +96,6 @@ pub fn debounce_filter( .ok(), ); - Rc::new(RefCell::new(None)) + Rc::clone(&last_return_value) } } diff --git a/src/utils/filters/mod.rs b/src/utils/filters/mod.rs index 0eee596..a428845 100644 --- a/src/utils/filters/mod.rs +++ b/src/utils/filters/mod.rs @@ -1,62 +1,145 @@ mod debounce; +mod pausable; mod throttle; pub use debounce::*; +pub use pausable::*; pub use throttle::*; -use crate::utils::CloneableFnWithReturn; +use crate::utils::{CloneableFnWithArgAndReturn, CloneableFnWithReturn}; +use leptos::MaybeSignal; use std::cell::RefCell; use std::rc::Rc; -pub fn create_filter_wrapper(filter: Filter, func: F) -> impl Fn() + Clone -where - F: FnOnce() + Clone + 'static, - Filter: Fn(Box>) -> Rc>> + Clone, +pub trait FilterFn: + CloneableFnWithArgAndReturn>, Rc>>> { - move || { - filter(Box::new(func.clone())); +} + +impl FilterFn for F +where + F: Fn(Box>) -> Rc>> + Clone + 'static, + R: 'static, +{ +} + +impl Clone for Box> { + fn clone(&self) -> Self { + (*self).clone() } } -pub fn create_filter_wrapper_with_arg( - filter: Filter, - func: F, -) -> impl Fn(Arg) + Clone -where - F: FnOnce(Arg) + Clone + 'static, - Arg: Clone + 'static, - Filter: Fn(Box>) -> Rc>> + Clone, -{ - move |arg: Arg| { - let func = func.clone(); - filter(Box::new(move || func(arg))); - } -} - -pub fn create_filter_wrapper_with_return( - filter: Filter, +pub fn create_filter_wrapper( + filter: Box>, func: F, ) -> impl Fn() -> Rc>> + Clone where F: FnOnce() -> R + Clone + 'static, R: 'static, - Filter: Fn(Box>) -> Rc>> + Clone, { - move || filter(Box::new(func.clone())) + move || filter.clone()(Box::new(func.clone())) } -pub fn create_filter_wrapper_with_return_and_arg( - filter: Filter, +pub fn create_filter_wrapper_with_arg( + filter: Box>, func: F, ) -> impl Fn(Arg) -> Rc>> + Clone where F: FnOnce(Arg) -> R + Clone + 'static, R: 'static, Arg: Clone + 'static, - Filter: Fn(Box>) -> Rc>> + Clone, { move |arg: Arg| { let func = func.clone(); - filter(Box::new(move || func(arg))) + filter.clone()(Box::new(move || func(arg))) } } + +#[derive(Default)] +pub enum FilterOptions { + #[default] + None, + Debounce { + ms: MaybeSignal, + options: DebounceOptions, + }, + Throttle { + ms: MaybeSignal, + options: ThrottleOptions, + }, +} + +impl FilterOptions { + pub fn filter_fn(&self) -> Box> + where + R: 'static, + { + match self { + FilterOptions::Debounce { ms, options } => { + Box::new(debounce_filter(ms.clone(), *options)) + } + FilterOptions::Throttle { ms, options } => { + Box::new(throttle_filter(ms.clone(), *options)) + } + FilterOptions::None => Box::new(|invoke: Box>| { + Rc::new(RefCell::new(Some(invoke()))) + }), + } + } +} + +#[macro_export] +macro_rules! filter_builder_methods { + ( + #[$filter_docs:meta] + $filter_field_name:ident + ) => { + /// Debounce + #[$filter_docs] + /// by `ms` milliseconds. + pub fn debounce(self, ms: impl Into>) -> Self { + self.debounce_with_options(ms, DebounceOptions::default()) + } + + /// Debounce + #[$filter_docs] + /// by `ms` milliseconds with additional options. + pub fn debounce_with_options( + self, + ms: impl Into>, + options: DebounceOptions, + ) -> Self { + Self { + $filter_field_name: FilterOptions::Debounce { + ms: ms.into(), + options, + }, + ..self + } + } + + /// Throttle + #[$filter_docs] + /// by `ms` milliseconds. + pub fn throttle(self, ms: impl Into>) -> Self { + self.throttle_with_options(ms, ThrottleOptions::default()) + } + + /// Throttle + #[$filter_docs] + /// by `ms` milliseconds with additional options. + pub fn throttle_with_options( + self, + ms: impl Into>, + options: ThrottleOptions, + ) -> Self { + Self { + $filter_field_name: FilterOptions::Throttle { + ms: ms.into(), + options, + }, + ..self + } + } + }; +} diff --git a/src/utils/filters/throttle.rs b/src/utils/filters/throttle.rs index 85f2997..24eabd9 100644 --- a/src/utils/filters/throttle.rs +++ b/src/utils/filters/throttle.rs @@ -33,7 +33,7 @@ where let last_exec = Rc::new(Cell::new(0_f64)); let timer = Rc::new(Cell::new(None::)); let is_leading = Rc::new(Cell::new(true)); - let last_value: Rc>> = Rc::new(RefCell::new(None)); + let last_return_value: Rc>> = Rc::new(RefCell::new(None)); let t = Rc::clone(&timer); let clear = move || { @@ -49,11 +49,11 @@ where let duration = ms.get_untracked(); let elapsed = Date::now() - last_exec.get(); - let last_val = Rc::clone(&last_value); + let last_return_val = Rc::clone(&last_return_value); let invoke = move || { let return_value = _invoke(); - let mut val_mut = last_val.borrow_mut(); + let mut val_mut = last_return_val.borrow_mut(); *val_mut = Some(return_value); }; @@ -63,7 +63,7 @@ where if duration <= 0.0 { last_exec.set(Date::now()); invoke(); - return Rc::clone(&last_value); + return Rc::clone(&last_return_value); } if elapsed > duration && (options.leading || !is_leading.get()) { @@ -101,6 +101,6 @@ where is_leading.set(false); - Rc::clone(&last_value) + Rc::clone(&last_return_value) } } diff --git a/src/watch.rs b/src/watch.rs index 30a4c24..4800ff1 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,10 +1,12 @@ +use crate::utils::{create_filter_wrapper, create_filter_wrapper_with_arg, FilterOptions}; +use crate::{filter_builder_methods, DebounceOptions, ThrottleOptions}; +use default_struct_builder::DefaultBuilder; use leptos::*; use std::cell::RefCell; +use std::rc::Rc; /// 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. /// @@ -24,8 +26,7 @@ use std::cell::RefCell; /// move |num, _, _| { /// log!("number {}", num); /// }, -/// true, -/// ); +/// ); // > "number 0" /// /// set_num(1); // > "number 1" /// @@ -37,22 +38,123 @@ use std::cell::RefCell; /// # view! { cx, } /// # } /// ``` -pub fn watch( +/// +/// ## Immediate +/// +/// 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`. +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::{watch_with_options, WatchOptions}; +/// # +/// # pub fn Demo(cx: Scope) -> impl IntoView { +/// let (num, set_num) = create_signal(cx, 0); +/// +/// watch_with_options( +/// cx, +/// num, +/// move |num, _, _| { +/// log!("number {}", num); +/// }, +/// WatchOptions::default().immediate(false), +/// ); // nothing happens +/// +/// set_num(1); // > "number 1" +/// # view! { cx, } +/// # } +/// ``` +/// +/// ## Filters +/// +/// The callback can be throttled or debounced. Please see [`watch_throttled`] and [`watch_debounced`] for details. +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::{watch_with_options, WatchOptions}; +/// # +/// # pub fn Demo(cx: Scope) -> impl IntoView { +/// # let (num, set_num) = create_signal(cx, 0); +/// # +/// watch_with_options( +/// cx, +/// num, +/// move |num, _, _| { +/// log!("number {}", num); +/// }, +/// WatchOptions::default().throttle(100.0), // there's also `throttle_with_options` +/// ); +/// # view! { cx, } +/// # } +/// ``` +/// +/// ``` +/// # use leptos::*; +/// # use leptos_use::{watch_with_options, WatchOptions}; +/// # +/// # pub fn Demo(cx: Scope) -> impl IntoView { +/// # let (num, set_num) = create_signal(cx, 0); +/// # +/// watch_with_options( +/// cx, +/// num, +/// move |num, _, _| { +/// log!("number {}", num); +/// }, +/// WatchOptions::default().debounce(100.0), // there's also `debounce_with_options` +/// ); +/// # view! { cx, } +/// # } +/// ``` +/// +/// ## See also +/// +/// * [`watch_throttled`] +/// * [`watch_debounced`] +pub fn watch(cx: Scope, deps: DFn, callback: CFn) -> impl Fn() + Clone +where + DFn: Fn() -> W + 'static, + CFn: Fn(&W, Option<&W>, Option) -> T + Clone + 'static, + W: Clone + 'static, + T: Clone + 'static, +{ + watch_with_options(cx, deps, callback, WatchOptions::default()) +} + +/// Version of `watch` that accepts `WatchOptions`. See [`watch`] for how to use. +pub fn watch_with_options( cx: Scope, deps: DFn, callback: CFn, - immediate: bool, + options: WatchOptions, ) -> impl Fn() + Clone where DFn: Fn() -> W + 'static, - CFn: Fn(&W, Option<&W>, Option) -> T + 'static, - W: 'static, - T: 'static, + CFn: Fn(&W, Option<&W>, Option) -> T + Clone + 'static, + W: Clone + 'static, + T: Clone + '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); + let cur_deps_value: Rc>> = Rc::new(RefCell::new(None)); + let prev_deps_value: Rc>> = Rc::new(RefCell::new(None)); + let prev_callback_value: Rc>> = Rc::new(RefCell::new(None)); + + let cur_val = Rc::clone(&cur_deps_value); + let prev_val = Rc::clone(&prev_deps_value); + let prev_cb_val = Rc::clone(&prev_callback_value); + let wrapped_callback = move || { + callback( + cur_val + .borrow() + .as_ref() + .expect("this will not be called before there is deps value"), + prev_val.borrow().as_ref(), + prev_cb_val.take(), + ) + }; + + let filtered_callback = create_filter_wrapper(options.filter.filter_fn(), wrapped_callback); create_effect(cx, move |did_run_before| { if !is_active() { @@ -61,17 +163,16 @@ where let deps_value = deps(); - if !immediate && did_run_before.is_none() { + if !options.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)); + cur_deps_value.replace(Some(deps_value.clone())); + + let callback_value = filtered_callback().take(); + + prev_callback_value.replace(callback_value); prev_deps_value.replace(Some(deps_value)); @@ -82,3 +183,31 @@ where set_active(false); } } + +/// Options for `watch_with_options` +#[derive(DefaultBuilder)] +pub struct WatchOptions { + /// 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`. + /// Defaults to `true`. + immediate: bool, + + #[builder(skip)] + filter: FilterOptions, +} + +impl Default for WatchOptions { + fn default() -> Self { + Self { + immediate: true, + filter: Default::default(), + } + } +} + +impl WatchOptions { + filter_builder_methods!( + /// the watch callback + filter + ); +}