added use_throttle_fn

This commit is contained in:
Maccesch 2023-05-19 00:58:48 +01:00
parent ed8e2ba465
commit e539181907
12 changed files with 339 additions and 12 deletions

6
.idea/leptos-use.iml generated
View file

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="FacetManager">
<facet type="Python" name="Python facet">
<configuration sdkName="Python 3.9" />
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
@ -10,5 +15,6 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Python 3.9 interpreter library" level="application" />
</component>
</module>

View file

@ -16,3 +16,4 @@ repository = "https://github.com/Synphonyte/leptos-use"
leptos = "0.3"
web-sys = "0.3"
wasm-bindgen = "0.2"
js-sys = "0.3"

View file

@ -10,3 +10,7 @@
# Browser
- [use_event_listener](browser/use_event_listener.md)
# Utilities
- [use_throttle_fn](utilities/use_throttle_fn.md)

View file

@ -6,8 +6,10 @@ def main():
file_name = f"../../../../src/{name}.rs"
with open(file_name) as f:
in_code_block = False
doc_comment_started = False
for line in f.readlines():
if line.startswith("///"):
doc_comment_started = True
line = line.strip().replace("/// ", "").replace("///", "")
if "```" in line:
if not in_code_block:
@ -15,6 +17,8 @@ def main():
in_code_block = not in_code_block
print(line)
elif doc_comment_started:
return
if __name__ == '__main__':

View file

@ -0,0 +1,3 @@
# use_throttle_fn
<!-- cmdrun python3 ../extract_doc_comment.py use_throttle_fn -->

View file

@ -1,6 +1,9 @@
pub mod core;
pub mod use_event_listener;
pub mod use_scroll;
pub mod use_throttle_fn;
pub mod utils;
pub use use_event_listener::use_event_listener;
pub use use_scroll::use_scroll;
pub use use_scroll::*;
pub use use_throttle_fn::*;

View file

@ -1,9 +1,7 @@
use crate::core::EventTargetMaybeSignal;
use leptos::ev::EventDescriptor;
use leptos::html::ElementDescriptor;
use leptos::*;
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;

View file

@ -1,6 +1,20 @@
use crate::core::EventTargetMaybeSignal;
use crate::use_event_listener;
use leptos::*;
// pub fn use_scroll<El, T, Fx, Fy>(
// cx: Scope,
// element: El,
// options: UseScrollOptions,
// ) -> UseScrollReturn<Fx, Fy>
// where
// (Scope, El): Into<EventTargetMaybeSignal<T>>,
// T: Into<web_sys::EventTarget> + Clone + 'static,
// {
// }
/// Options for [`use_scroll`].
#[derive(Default)]
pub struct UseScrollOptions {
/// Throttle time in milliseconds for the scroll events. Defaults to 0 (disabled).
pub throttle: u32,
@ -8,18 +22,46 @@ pub struct UseScrollOptions {
/// After scrolling ends we wait idle + throttle milliseconds before we consider scrolling to have stopped.
/// Defaults to 200.
pub idle: u32,
/// Threshold in pixels when we consider a side to have arrived (`UseScrollReturn::arrived_state`).
pub offset: ScrollOffset,
/// Callback when scrolling is happening.
pub on_scroll: Option<Box<dyn Fn()>>,
/// Callback when scrolling stops (after `idle` + `throttle` milliseconds have passed).
pub on_stop: Option<Box<dyn Fn()>>,
/// Options passed to the `addEventListener("scroll", ...)` call
pub event_listener_options: Option<web_sys::AddEventListenerOptions>,
/// When changing the `x` or `y` signals this specifies the scroll behaviour.
/// Can be `Auto` (= not smooth) or `Smooth`. Defaults to `Auto`.
pub behavior: ScrollBehavior,
}
pub struct UseScrollReturn {
pub x: ReadSignal<f64>,
pub setX: WriteSignal<f64>,
pub y: ReadSignal<f64>,
pub setY: WriteSignal<f64>,
pub isScrolling: ReadSignal<bool>,
pub arrivedState: ReadSignal<Directions>,
pub directions: ReadSignal<Directions>,
#[derive(Default)]
pub enum ScrollBehavior {
#[default]
Auto,
Smooth,
}
pub struct UseScrollReturn<Fx, Fy>
where
Fx: FnMut(f64),
Fy: FnMut(f64),
{
pub x: Signal<f64>,
pub set_x: Fx,
pub y: Signal<f64>,
pub set_y: Fy,
pub is_scrolling: Signal<bool>,
pub arrived_state: Signal<Directions>,
pub directions: Signal<Directions>,
}
#[derive(Copy, Clone)]
pub struct Directions {
pub left: bool,
pub right: bool,
@ -27,4 +69,10 @@ pub struct Directions {
pub bottom: bool,
}
pub fn use_scroll() {}
#[derive(Default)]
pub struct ScrollOffset {
pub left: f64,
pub top: f64,
pub right: f64,
pub bottom: f64,
}

116
src/use_throttle_fn.rs Normal file
View file

@ -0,0 +1,116 @@
use crate::utils::{create_filter_wrapper, create_filter_wrapper_with_arg, throttle_filter};
use leptos::MaybeSignal;
use std::cell::RefCell;
use std::rc::Rc;
pub use crate::utils::ThrottleOptions;
/// Throttle execution of a function.
/// Especially useful for rate limiting execution of handlers on events like resize and scroll.
///
/// > Throttle is a spring that throws balls: after a ball flies out it needs some time to shrink back, so it cannot throw any more balls unless it's ready.
///
/// ## Demo
///
/// TODO
///
/// ## Usage
///
/// ```
/// use leptos::*;
/// use leptos_use::use_throttle_fn;
///
/// #[component]
/// fn Demo(cx: Scope) -> impl IntoView {
/// let mut throttled_fn = use_throttle_fn(
/// || {
/// // do something, it will be called at most 1 time per second
/// },
/// 1000.0,
/// );
/// view! { cx,
/// <button on:click=move |_| { throttled_fn(); }>
/// "Smash me!"
/// </button>
/// }
/// }
/// ```
///
/// You can provide options when you use [`use_throttle_fn_with_options`].
///
/// ```
/// # use leptos::*;
/// # use leptos_use::{ThrottleOptions, use_throttle_fn_with_options};
///
/// # #[component]
/// # fn Demo(cx: Scope) -> impl IntoView {
/// let throttled_fn = use_throttle_fn_with_options(
/// || {
/// // do something, it will be called at most 1 time per second
/// },
/// 1000.0,
/// ThrottleOptions {
/// leading: true,
/// trailing: true,
/// }
/// );
/// # view! { cx, }
/// # }
/// ```
///
/// If your function that you want to throttle takes an argument there are also the versions
/// [`use_throttle_fn_with_args`] and [`use_throttle_fn_with_args_and_options`].
///
/// ## Recommended Reading
///
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle)
pub fn use_throttle_fn<F, R>(
func: F,
ms: impl Into<MaybeSignal<f64>>,
) -> impl FnMut() -> Rc<RefCell<Option<R>>>
where
F: FnMut() -> R + Clone + 'static,
R: 'static,
{
use_throttle_fn_with_options(func, ms, Default::default())
}
/// 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<F, R>(
func: F,
ms: impl Into<MaybeSignal<f64>>,
options: ThrottleOptions,
) -> impl FnMut() -> Rc<RefCell<Option<R>>>
where
F: FnMut() -> R + Clone + 'static,
R: 'static,
{
create_filter_wrapper(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<F, Arg, R>(
func: F,
ms: impl Into<MaybeSignal<f64>>,
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
where
F: FnMut(Arg) -> R + Clone + 'static,
Arg: 'static,
R: 'static,
{
use_throttle_fn_with_arg_and_options(func, ms, Default::default())
}
/// 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<F, Arg, R>(
func: F,
ms: impl Into<MaybeSignal<f64>>,
options: ThrottleOptions,
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
where
F: FnMut(Arg) -> R + Clone + 'static,
Arg: 'static,
R: 'static,
{
create_filter_wrapper_with_arg(throttle_filter(ms, options), func)
}

136
src/utils/filters.rs Normal file
View file

@ -0,0 +1,136 @@
use js_sys::Date;
use leptos::leptos_dom::helpers::TimeoutHandle;
use leptos::{set_timeout_with_handle, MaybeSignal, Scope, SignalGetUntracked};
use std::cell::{Cell, RefCell};
use std::cmp::max;
use std::rc::Rc;
use std::time::Duration;
pub fn create_filter_wrapper<F, R, Filter>(
mut filter: Filter,
func: F,
) -> impl FnMut() -> Rc<RefCell<Option<R>>>
where
F: FnMut() -> R + Clone + 'static,
R: 'static,
Filter: FnMut(Box<dyn FnOnce() -> R>) -> Rc<RefCell<Option<R>>>,
{
move || {
let wrapped_func = Box::new(func.clone());
filter(wrapped_func)
}
}
pub fn create_filter_wrapper_with_arg<F, Arg, R, Filter>(
mut filter: Filter,
func: F,
) -> impl FnMut(Arg) -> Rc<RefCell<Option<R>>>
where
F: FnMut(Arg) -> R + Clone + 'static,
R: 'static,
Arg: 'static,
Filter: FnMut(Box<dyn FnOnce() -> R>) -> Rc<RefCell<Option<R>>>,
{
move |arg: Arg| {
let mut func = func.clone();
let wrapped_func = Box::new(move || func(arg));
filter(wrapped_func)
}
}
#[derive(Copy, Clone)]
pub struct ThrottleOptions {
pub trailing: bool,
pub leading: bool,
}
impl Default for ThrottleOptions {
fn default() -> Self {
Self {
trailing: true,
leading: true,
}
}
}
pub fn throttle_filter<R>(
ms: impl Into<MaybeSignal<f64>>,
options: ThrottleOptions,
) -> impl FnMut(Box<dyn FnOnce() -> R>) -> Rc<RefCell<Option<R>>>
where
R: 'static,
{
let last_exec = Rc::new(Cell::new(0_f64));
let timer = Rc::new(Cell::new(None::<TimeoutHandle>));
let is_leading = Rc::new(Cell::new(true));
let last_value: Rc<RefCell<Option<R>>> = Rc::new(RefCell::new(None));
let t = Rc::clone(&timer);
let clear = move || {
if let Some(handle) = t.get() {
handle.clear();
t.set(None);
}
};
let ms = ms.into();
move |mut _invoke: Box<dyn FnOnce() -> R>| {
let duration = ms.get_untracked();
let elapsed = Date::now() - last_exec.get();
let last_val = Rc::clone(&last_value);
let mut invoke = move || {
let return_value = _invoke();
let mut val_mut = last_val.borrow_mut();
*val_mut = Some(return_value);
};
let clear = clear.clone();
clear();
if duration <= 0.0 {
last_exec.set(Date::now());
invoke();
return Rc::clone(&last_value);
}
if elapsed > duration && (options.leading || !is_leading.get()) {
last_exec.set(Date::now());
invoke();
} else if options.trailing {
let last_exec = Rc::clone(&last_exec);
let is_leading = Rc::clone(&is_leading);
timer.set(
set_timeout_with_handle(
move || {
last_exec.set(Date::now());
is_leading.set(true);
invoke();
clear();
},
Duration::from_millis(max(0, (duration - elapsed) as u64)),
)
.ok(),
);
}
if !options.leading && timer.get().is_none() {
let is_leading = Rc::clone(&is_leading);
timer.set(
set_timeout_with_handle(
move || {
is_leading.set(true);
},
Duration::from_millis(duration as u64),
)
.ok(),
);
}
is_leading.set(false);
Rc::clone(&last_value)
}
}

3
src/utils/is.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn noop() -> Box<dyn FnMut()> {
Box::new(|| {})
}

5
src/utils/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod filters;
mod is;
pub use filters::*;
pub use is::*;