added macro for simple math functions

This commit is contained in:
Maccesch 2023-06-09 23:10:33 +01:00
parent e9e79a0b69
commit 0a5eb5e895
33 changed files with 1261 additions and 143 deletions

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

@ -18,6 +18,7 @@
<sourceFolder url="file://$MODULE_DIR$/examples/use_floor/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_floor/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/use_element_size/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/use_element_size/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/watch_debounced/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/watch_debounced/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/watch_pausable/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/watch_throttled/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples/watch_throttled/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
@ -33,6 +34,7 @@
<excludeFolder url="file://$MODULE_DIR$/examples/use_floor/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/use_floor/target" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_element_size/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/use_element_size/target" />
<excludeFolder url="file://$MODULE_DIR$/examples/watch_debounced/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/watch_debounced/target" />
<excludeFolder url="file://$MODULE_DIR$/examples/watch_pausable/target" />
<excludeFolder url="file://$MODULE_DIR$/examples/watch_throttled/target" /> <excludeFolder url="file://$MODULE_DIR$/examples/watch_throttled/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

View file

@ -1,12 +1,12 @@
# Changelog # Changelog
## 0.1.10 ## 0.2.0
#### Braking Changes #### Braking Changes
- `watch` doesn't accept `immediate` as an argument anymore. This is only provided by the option variant. - `watch` doesn't accept `immediate` as an argument anymore. This is only provided by the option variant.
#### New Functions #### New Functions
- `use_local_storage` - `use_storage`
- `watch_debounced` - `watch_debounced`
- `watch_throttled` - `watch_throttled`
- `watch_pausable` - `watch_pausable`

View file

@ -17,14 +17,18 @@ homepage = "https://leptos-use.rs"
leptos = "0.3.0" leptos = "0.3.0"
wasm-bindgen = "0.2.86" wasm-bindgen = "0.2.86"
js-sys = "0.3.63" js-sys = "0.3.63"
default-struct-builder = { path = "../default-struct-builder"} default-struct-builder = { path = "../default-struct-builder" }
num = { version = "0.4.0", optional = true } num = { version = "0.4.0", optional = true }
serde = "1.0.163" serde = { version = "1.0.163", optional = true }
serde_json = { version = "1.0.96", optional = true }
paste = { version = "1.0.12", optional = true }
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3.63" version = "0.3.63"
features = [ features = [
"CssStyleDeclaration", "CssStyleDeclaration",
"CustomEvent",
"CustomEventInit",
"DomRectReadOnly", "DomRectReadOnly",
"Element", "Element",
"MouseEvent", "MouseEvent",
@ -36,7 +40,6 @@ features = [
"ResizeObserverSize", "ResizeObserverSize",
"ScrollBehavior", "ScrollBehavior",
"ScrollToOptions", "ScrollToOptions",
"Storage",
"Touch", "Touch",
"TouchEvent", "TouchEvent",
"TouchList", "TouchList",
@ -45,7 +48,8 @@ features = [
[features] [features]
docs = [] docs = []
math = ["num"] math = ["num", "paste"]
storage = ["serde", "serde_json", "web-sys/Storage", "web-sys/StorageEvent"]
[package.metadata."docs.rs"] [package.metadata."docs.rs"]
all-features = true all-features = true

View file

@ -22,4 +22,33 @@
We have only just begun implementing the first dozen functions but they are already very usable and ergonomic. We have only just begun implementing the first dozen functions but they are already very usable and ergonomic.
Missing a function? Open a ticket or PR! Missing a function? Open a ticket or PR!
## Development
To run all tests run
```shell
cargo test --all-features
```
## Book
First you need to install
```shell
cargo install mdbook-cmdrun trunk
```
To build the book go in your terminal into the docs/book folder
and run
```shell
mdbook serve
```
This builds the html version of the book and runs a local dev server.
To also add in the examples open another shell and run
```shell
python3 post_build.py
```

View file

@ -5,6 +5,10 @@
[Functions](functions.md) [Functions](functions.md)
[Changelog](changelog.md) [Changelog](changelog.md)
# @Storage
- [use_storage](storage/use_storage.md)
# Elements # Elements
- [use_element_size](elements/use_element_size.md) - [use_element_size](elements/use_element_size.md)
@ -23,6 +27,7 @@
- [watch](watch/watch.md) - [watch](watch/watch.md)
- [watch_debounced](watch/watch_debounced.md) - [watch_debounced](watch/watch_debounced.md)
- [watch_pausable](watch/watch_pausable.md)
- [watch_throttled](watch/watch_throttled.md) - [watch_throttled](watch/watch_throttled.md)
# Utilities # Utilities

View file

@ -44,15 +44,24 @@
vertical-align: middle; vertical-align: middle;
} }
.demo-container button:hover { .demo-container button:hover:not(:disabled) {
background-color: var(--brand-color-dark); background-color: var(--brand-color-dark);
} }
.demo-container button:active { .demo-container button:active:not(:disabled) {
border-bottom: 0; border-bottom: 0;
border-top: 2px solid var(--brand-color-dark); border-top: 2px solid var(--brand-color-dark);
} }
.demo-container button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.demo-container button ~ button {
margin-left: 0.8rem;
}
.demo-container p { .demo-container p {
margin: 1rem 0; margin: 1rem 0;
} }

View file

@ -1,5 +1,7 @@
# Functions # Functions
<!-- cmdrun python3 generate_function_overview.py storage storage -->
<!-- cmdrun python3 generate_function_overview.py elements --> <!-- cmdrun python3 generate_function_overview.py elements -->
<!-- cmdrun python3 generate_function_overview.py browser --> <!-- cmdrun python3 generate_function_overview.py browser -->

View file

@ -0,0 +1 @@
# use_storage

View file

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

View file

@ -0,0 +1,16 @@
[package]
name = "watch_pausable"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.3"
console_error_panic_hook = "0.1"
console_log = "1"
log = "0.4"
leptos-use = { path = "../..", features = ["docs"] }
web-sys = "0.3"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"

View file

@ -0,0 +1,23 @@
A simple example for `watch_pausable`.
If you don't have it installed already, install [Trunk](https://trunkrs.dev/) and [Tailwind](https://tailwindcss.com/docs/installation)
as well as the nightly toolchain for Rust and the wasm32-unknown-unknown target:
```bash
cargo install trunk
npm install -D tailwindcss @tailwindcss/forms
rustup toolchain install nightly
rustup target add wasm32-unknown-unknown
```
Then, open two terminals. In the first one, run:
```
npx tailwindcss -i ./input.css -o ./style/output.css --watch
```
In the second one, run:
```bash
trunk serve --open
```

View file

@ -0,0 +1,2 @@
[build]
public_url = "/demo/"

View file

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="css" href="style/output.css">
</head>
<body></body>
</html>

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View file

@ -0,0 +1,59 @@
use leptos::*;
use leptos_use::docs::{demo_or_body, Note};
use leptos_use::{watch_pausable, WatchPausableReturn};
#[component]
fn Demo(cx: Scope) -> impl IntoView {
let input = create_node_ref(cx);
let (log, set_log) = create_signal(cx, "".to_string());
let (source, set_source) = create_signal(cx, "".to_string());
let WatchPausableReturn {
stop,
pause,
resume,
is_active,
} = watch_pausable(cx, source, move |v, _, _| {
set_log.update(|log| *log = format!("{log}Changed to \"{v}\"\n"));
});
let clear = move |_| set_log("".to_string());
let pause = move |_| {
set_log.update(|log| *log = format!("{log}Paused\n"));
pause();
};
let resume = move |_| {
set_log.update(|log| *log = format!("{log}Resumed\n"));
resume();
};
view! { cx,
<Note class="mb-2">"Type something below to trigger the watch"</Note>
<input
node_ref=input
class="block"
prop:value=source
on:input=move |e| set_source(event_target_value(&e))
type="text"
/>
<p>"Value: " {source}</p>
<button prop:disabled=move || !is_active() class="orange" on:click=pause>"Pause"</button>
<button prop:disabled=is_active on:click=resume>"Resume"</button>
<button on:click=clear>"Clear Log"</button>
<br />
<br />
<Note>"Log"</Note>
<pre>{log}</pre>
}
}
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to(demo_or_body(), |cx| {
view! { cx, <Demo /> }
})
}

View file

@ -0,0 +1,293 @@
[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
border-radius: 0px;
padding-top: 0.5rem;
padding-right: 0.75rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
font-size: 1rem;
line-height: 1.5rem;
--tw-shadow: 0 0 #0000;
}
[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
border-color: #2563eb;
}
input::-moz-placeholder, textarea::-moz-placeholder {
color: #6b7280;
opacity: 1;
}
input::placeholder,textarea::placeholder {
color: #6b7280;
opacity: 1;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-date-and-time-value {
min-height: 1.5em;
}
::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
padding-top: 0;
padding-bottom: 0;
}
select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
[multiple] {
background-image: initial;
background-position: initial;
background-repeat: unset;
background-size: initial;
padding-right: 0.75rem;
-webkit-print-color-adjust: unset;
print-color-adjust: unset;
}
[type='checkbox'],[type='radio'] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
padding: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
color: #2563eb;
background-color: #fff;
border-color: #6b7280;
border-width: 1px;
--tw-shadow: 0 0 #0000;
}
[type='checkbox'] {
border-radius: 0px;
}
[type='radio'] {
border-radius: 100%;
}
[type='checkbox']:focus,[type='radio']:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 2px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #2563eb;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
[type='checkbox']:checked,[type='radio']:checked {
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
[type='radio']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
}
[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
border-color: transparent;
background-color: currentColor;
}
[type='checkbox']:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
border-color: transparent;
background-color: currentColor;
}
[type='file'] {
background: unset;
border-color: inherit;
border-width: 0;
border-radius: 0;
padding: 0;
font-size: unset;
line-height: inherit;
}
[type='file']:focus {
outline: 1px solid ButtonText;
outline: 1px auto -webkit-focus-ring-color;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.block {
display: block;
}
.text-\[--brand-color\] {
color: var(--brand-color);
}
.text-green-600 {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity));
}
.opacity-75 {
opacity: 0.75;
}
@media (prefers-color-scheme: dark) {
.dark\:text-green-500 {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity));
}
}

View file

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs", "../../src/docs/**/*.rs"],
},
theme: {
extend: {},
},
corePlugins: {
preflight: false,
},
plugins: [
require('@tailwindcss/forms'),
],
}

View file

@ -16,7 +16,6 @@ mod use_mouse;
#[cfg(web_sys_unstable_apis)] #[cfg(web_sys_unstable_apis)]
mod use_resize_observer; mod use_resize_observer;
mod use_scroll; mod use_scroll;
mod use_storage;
mod use_supported; mod use_supported;
mod use_throttle_fn; mod use_throttle_fn;
pub mod utils; pub mod utils;
@ -32,7 +31,6 @@ pub use use_mouse::*;
#[cfg(web_sys_unstable_apis)] #[cfg(web_sys_unstable_apis)]
pub use use_resize_observer::*; pub use use_resize_observer::*;
pub use use_scroll::*; pub use use_scroll::*;
pub use use_storage::*;
pub use use_supported::*; pub use use_supported::*;
pub use use_throttle_fn::*; pub use use_throttle_fn::*;
pub use watch::*; pub use watch::*;

View file

@ -1,3 +1,5 @@
use paste::paste;
macro_rules! use_partial_cmp { macro_rules! use_partial_cmp {
($(#[$outer:meta])* ($(#[$outer:meta])*
$fn_name:ident, $fn_name:ident,
@ -39,4 +41,23 @@ macro_rules! use_partial_cmp {
}; };
} }
macro_rules! use_simple_math {
($(#[$outer:meta])*
$fn_name:ident
) => {
paste! {
$(#[$outer])*
pub fn [<use_ $fn_name>]<S, N>(cx: Scope, x: S) -> Signal<N>
where
S: Into<MaybeSignal<N>>,
N: Float,
{
let x = x.into();
Signal::derive(cx, move || x.get().$fn_name())
}
}
};
}
pub(crate) use use_partial_cmp; pub(crate) use use_partial_cmp;
pub(crate) use use_simple_math;

View file

@ -1,33 +1,30 @@
use crate::math::shared::use_simple_math;
use leptos::*; use leptos::*;
use num::Float; use num::Float;
use paste::paste;
/// Reactive `ceil()`. use_simple_math!(
/// /// Reactive `ceil()`.
/// ## Demo ///
/// /// ## Demo
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_ceil) ///
/// /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_ceil)
/// ## Usage ///
/// /// ## Usage
/// ``` ///
/// # use leptos::*; /// ```
/// # use leptos_use::math::use_ceil; /// # use leptos::*;
/// # /// # use leptos_use::math::use_ceil;
/// # #[component] /// #
/// # fn Demo(cx: Scope) -> impl IntoView { /// # #[component]
/// let (value, set_value) = create_signal(cx, 44.15); /// # fn Demo(cx: Scope) -> impl IntoView {
/// let result: Signal<f64> = use_ceil(cx, value); // 45 /// let (value, set_value) = create_signal(cx, 44.15);
/// # /// let result: Signal<f64> = use_ceil(cx, value); // 45
/// # assert_eq!(result.get(), 45.0); /// #
/// # view! { cx, } /// # assert_eq!(result.get(), 45.0);
/// # } /// # view! { cx, }
/// ``` /// # }
#[doc(cfg(feature = "math"))] /// ```
pub fn use_ceil<S, N>(cx: Scope, x: S) -> Signal<N> #[doc(cfg(feature = "math"))]
where ceil
S: Into<MaybeSignal<N>>, );
N: Float,
{
let x = x.into();
Signal::derive(cx, move || x.get().ceil())
}

View file

@ -1,33 +1,30 @@
use leptos::*; use leptos::*;
use leptos_use::math::shared::use_simple_math;
use num::Float; use num::Float;
use paste::paste;
/// Reactive `floor()`. use_simple_math!(
/// /// Reactive `floor()`.
/// ## Demo ///
/// /// ## Demo
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_floor) ///
/// /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_floor)
/// ## Usage ///
/// /// ## Usage
/// ``` ///
/// # use leptos::*; /// ```
/// # use leptos_use::math::use_floor; /// # use leptos::*;
/// # /// # use leptos_use::math::use_floor;
/// # #[component] /// #
/// # fn Demo(cx: Scope) -> impl IntoView { /// # #[component]
/// let (value, set_value) = create_signal(cx, 45.95); /// # fn Demo(cx: Scope) -> impl IntoView {
/// let result: Signal<f64> = use_floor(cx, value); // 45 /// let (value, set_value) = create_signal(cx, 45.95);
/// # /// let result: Signal<f64> = use_floor(cx, value); // 45
/// # assert_eq!(result.get(), 45.0); /// #
/// # view! { cx, } /// # assert_eq!(result.get(), 45.0);
/// # } /// # view! { cx, }
/// ``` /// # }
#[doc(cfg(feature = "math"))] /// ```
pub fn use_floor<S, N>(cx: Scope, x: S) -> Signal<N> #[doc(cfg(feature = "math"))]
where floor
S: Into<MaybeSignal<N>>, );
N: Float,
{
let x = x.into();
Signal::derive(cx, move || x.get().floor())
}

3
src/storage/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod use_storage;
pub use use_storage::*;

495
src/storage/use_storage.rs Normal file
View file

@ -0,0 +1,495 @@
// use crate::utils::{CloneableFnWithArg, FilterOptions};
// use crate::{
// filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions,
// ThrottleOptions, WatchOptions, WatchPausableReturn,
// };
// use default_struct_builder::DefaultBuilder;
// use js_sys::Reflect;
// use leptos::*;
// use serde::{Deserialize, Serialize};
// use serde_json::Error;
// use std::time::Duration;
// use wasm_bindgen::{JsCast, JsValue};
//
// const CUSTOM_STORAGE_EVENT_NAME: &str = "leptos-use-storage";
//
// /// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) / [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)
// ///
// /// ## Demo
// ///
// /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_storage)
// ///
// /// ## Usage
// ///
// /// It returns a triplet `(read_signal, write_signal, delete_from_storage_func)` of type `(Signal<T>, WriteSignal<T>, Fn())`.
// ///
// /// ```
// /// # use leptos::*;
// /// # use leptos_use::storage::{StorageType, use_storage, use_storage_with_options, UseStorageOptions};
// /// # use serde::{Deserialize, Serialize};
// /// #
// /// #[derive(Serialize, Deserialize, Clone)]
// /// pub struct MyState {
// /// pub hello: String,
// /// pub greeting: String,
// /// }
// /// #
// /// # pub fn Demo(cx: Scope) -> impl IntoView {
// /// // bind struct. Must be serializable.
// /// let (state, set_state, _) = use_storage(
// /// cx,
// /// "my-state",
// /// MyState {
// /// hello: "hi".to_string(),
// /// greeting: "Hello".to_string()
// /// },
// /// ); // returns Signal<MyState>
// ///
// /// // bind bool.
// /// let (flag, set_flag, remove_flag) = use_storage(cx, "my-flag", true); // returns Signal<bool>
// ///
// /// // bind number
// /// let (count, set_count, _) = use_storage(cx, "my-count", 0); // returns Signal<i32>
// ///
// /// // bind string with SessionStorage
// /// let (id, set_id, _) = use_storage_with_options(
// /// cx,
// /// "my-id",
// /// "some_string_id".to_string(),
// /// UseStorageOptions::default().storage_type(StorageType::Session),
// /// );
// /// # view! { cx, }
// /// # }
// /// ```
// ///
// /// ## Merge Defaults
// ///
// /// By default, [`use_storage`] will use the value from storage if it is present and ignores the default value.
// /// Be aware that when you add more properties to the default value, the key might be `None`
// /// (in the case of an `Option<T>` field) if client's storage does not have that key
// /// or deserialization might fail altogether.
// ///
// /// Let's say you had a struct `MyState` that has been saved to storage
// ///
// /// ```ignore
// /// #[derive(Serialize, Deserialize, Clone)]
// /// struct MyState {
// /// hello: String,
// /// }
// ///
// /// let (state, .. ) = use_storage(cx, "my-state", MyState { hello: "hello" });
// /// ```
// ///
// /// Now, in a newer version you added a field `greeting` to `MyState`.
// ///
// /// ```ignore
// /// #[derive(Serialize, Deserialize, Clone)]
// /// struct MyState {
// /// hello: String,
// /// greeting: String,
// /// }
// ///
// /// let (state, .. ) = use_storage(
// /// cx,
// /// "my-state",
// /// MyState { hello: "hi", greeting: "whatsup" },
// /// ); // fails to deserialize -> default value
// /// ```
// ///
// /// This will fail to deserialize the stored string `{"hello": "hello"}` because it has no field `greeting`.
// /// Hence it just uses the new default value provided and the previously saved value is lost.
// ///
// /// To mitigate that you can provide a `merge_defaults` option. This is a pure function pointer
// /// that takes the serialized (to json) stored value and the default value as arguments
// /// and should return the serialized merged value.
// ///
// /// ```
// /// # use leptos::*;
// /// # use leptos_use::storage::{use_storage_with_options, UseStorageOptions};
// /// # use serde::{Deserialize, Serialize};
// /// #
// /// #[derive(Serialize, Deserialize, Clone)]
// /// pub struct MyState {
// /// pub hello: String,
// /// pub greeting: String,
// /// }
// /// #
// /// # pub fn Demo(cx: Scope) -> impl IntoView {
// /// let (state, set_state) = use_storage_with_options(
// /// cx,
// /// "my-state",
// /// MyState {
// /// hello: "hi".to_string(),
// /// greeting: "Hello".to_string()
// /// },
// /// UseStorageOptions::<MyState>::default().merge_defaults(|stored_value, default_value| {
// /// if stored_value.contains(r#""greeting":"#) {
// /// stored_value.to_string()
// /// } else {
// /// // add "greeting": "Hello" to the string
// /// stored_value.replace("}", &format!(r#""greeting": "{}"}}"#, default_value.greeting))
// /// }
// /// }),
// /// );
// /// #
// /// # view! { cx, }
// /// # }
// /// ```
// ///
// /// ## Filter Storage Write
// ///
// /// You can specify `debounce` and `throttle` filter options for writing to storage.
// ///
// /// ## See also
// ///
// ///
// /// * [`use_local_storage`]
// /// * [`use_session_storage`]
// #[doc(cfg(feature = "storage"))]
// pub fn use_storage<T, D>(
// cx: Scope,
// key: &str,
// defaults: D,
// ) -> (Signal<T>, WriteSignal<Option<T>>, impl Fn() + Clone)
// where
// for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
// D: Into<MaybeSignal<T>>,
// T: Clone,
// {
// use_storage_with_options(cx, key, defaults, UseStorageOptions::default())
// }
//
// /// Version of [`use_storage`] that accepts [`UseStorageOptions`]. See [`use_storage`] for how to use.
// #[doc(cfg(feature = "storage"))]
// pub fn use_storage_with_options<T, D>(
// cx: Scope,
// key: &str,
// defaults: D,
// options: UseStorageOptions<T>,
// ) -> (Signal<T>, WriteSignal<Option<T>>, impl Fn() + Clone)
// where
// for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
// D: Into<MaybeSignal<T>>,
// T: Clone,
// {
// let defaults = defaults.into();
//
// let UseStorageOptions {
// storage_type,
// listen_to_storage_changes,
// write_defaults,
// merge_defaults,
// on_error,
// filter,
// } = options;
//
// let (data, set_data) = create_signal(cx, defaults.get_untracked());
//
// let storage = storage_type.into_storage();
//
// let remove = match storage {
// Ok(Some(storage)) => {
// let on_err = on_error.clone();
//
// let store = storage.clone();
// let k = key.to_string();
//
// let write = move |v: &Option<T>| {
// if let Some(v) = v {
// match serde_json::to_string(&v) {
// Ok(ref serialized) => match store.get_item(&k) {
// Ok(old_value) => {
// if old_value.as_ref() != Some(serialized) {
// if let Err(e) = store.set_item(&k, &serialized) {
// on_err(UseStorageError::StorageAccessError(e));
// } else {
// let mut event_init = web_sys::CustomEventInit::new();
// event_init.detail(
// &StorageEventDetail {
// key: Some(k.clone()),
// old_value,
// new_value: Some(serialized.clone()),
// storage_area: Some(store.clone()),
// }
// .into(),
// );
//
// // importantly this should _not_ be a StorageEvent since those cannot
// // be constructed with a non-built-in storage area
// let _ = window().dispatch_event(
// &web_sys::CustomEvent::new_with_event_init_dict(
// CUSTOM_STORAGE_EVENT_NAME,
// &event_init,
// )
// .expect("Failed to create CustomEvent"),
// );
// }
// }
// }
// Err(e) => {
// on_err.clone()(UseStorageError::StorageAccessError(e));
// }
// },
// Err(e) => {
// on_err.clone()(UseStorageError::SerializationError(e));
// }
// }
// } else if let Err(e) = store.remove_item(&k) {
// on_err(UseStorageError::StorageAccessError(e));
// }
// };
//
// let store = storage.clone();
// let on_err = on_error.clone();
// let k = key.to_string();
// let def = defaults.clone();
//
// let read = move |event_detail: Option<StorageEventDetail>| -> Option<T> {
// let raw_init = match serde_json::to_string(&def.get_untracked()) {
// Ok(serialized) => Some(serialized),
// Err(e) => {
// on_err.clone()(UseStorageError::DefaultSerializationError(e));
// None
// }
// };
//
// let raw_value = if let Some(event_detail) = event_detail {
// event_detail.new_value
// } else {
// match store.get_item(&k) {
// Ok(raw_value) => match raw_value {
// Some(raw_value) => {
// Some(merge_defaults(&raw_value, &def.get_untracked()))
// }
// None => raw_init.clone(),
// },
// Err(e) => {
// on_err.clone()(UseStorageError::StorageAccessError(e));
// None
// }
// }
// };
//
// match raw_value {
// Some(raw_value) => match serde_json::from_str(&raw_value) {
// Ok(v) => Some(v),
// Err(e) => {
// on_err.clone()(UseStorageError::SerializationError(e));
// None
// }
// },
// None => {
// if let Some(raw_init) = &raw_init {
// if write_defaults {
// if let Err(e) = store.set_item(&k, raw_init) {
// on_err(UseStorageError::StorageAccessError(e));
// }
// }
// }
//
// Some(def.get_untracked())
// }
// }
// };
//
// let WatchPausableReturn {
// pause: pause_watch,
// resume: resume_watch,
// ..
// } = watch_pausable_with_options(
// cx,
// data,
// move |data, _, _| write.clone()(data),
// WatchOptions::default().immediate(true).filter(filter),
// );
//
// let k = key.to_string();
//
// let update = move |event_detail: Option<StorageEventDetail>| {
// if let Some(event_detail) = &event_detail {
// if event_detail.storage_area != Some(storage) {
// return;
// }
//
// match &event_detail.key {
// None => {
// set_data(Some(defaults.get_untracked()));
// return;
// }
// Some(event_key) => {
// if event_key != &k {
// return;
// }
// }
// };
// }
//
// pause_watch();
//
// set_data(read(event_detail.clone()));
//
// if event_detail.is_some() {
// // use timeout to avoid inifinite loop
// let resume = resume_watch.clone();
// let _ = set_timeout_with_handle(resume, Duration::ZERO);
// } else {
// resume_watch();
// }
// };
//
// let upd = update.clone();
// let update_from_custom_event =
// move |event: web_sys::CustomEvent| upd.clone()(Some(event.into()));
//
// let upd = update.clone();
// let update_from_storage_event =
// move |event: web_sys::StorageEvent| upd.clone()(Some(event.into()));
//
// if listen_to_storage_changes {
// let _ = use_event_listener(cx, window(), ev::storage, update_from_storage_event);
// let _ = use_event_listener(
// cx,
// window(),
// ev::Custom::new(CUSTOM_STORAGE_EVENT_NAME),
// update_from_custom_event,
// );
// }
//
// update(None);
//
// move || storage.remove_item(&k)
// }
// Err(e) => {
// on_error(UseStorageError::NoStorage(e));
// move || {}
// }
// _ => {
// // do nothing
// move || {}
// }
// };
//
// (data, set_data, remove)
// }
//
// #[derive(Clone)]
// pub struct StorageEventDetail {
// pub key: Option<String>,
// pub old_value: Option<String>,
// pub new_value: Option<String>,
// pub storage_area: Option<web_sys::Storage>,
// }
//
// impl From<web_sys::StorageEvent> for StorageEventDetail {
// fn from(event: web_sys::StorageEvent) -> Self {
// Self {
// key: event.key(),
// old_value: event.old_value(),
// new_value: event.new_value(),
// storage_area: event.storage_area(),
// }
// }
// }
//
// impl From<web_sys::CustomEvent> for StorageEventDetail {
// fn from(event: web_sys::CustomEvent) -> Self {
// let detail = event.detail();
// Self {
// key: get_optional_string(&detail, "key"),
// old_value: get_optional_string(&detail, "oldValue"),
// new_value: get_optional_string(&detail, "newValue"),
// storage_area: Reflect::get(&detail, &"storageArea".into())
// .map(|v| v.dyn_into::<web_sys::Storage>().ok())
// .unwrap_or_default(),
// }
// }
// }
//
// impl From<StorageEventDetail> for JsValue {
// fn from(event: StorageEventDetail) -> Self {
// let obj = js_sys::Object::new();
//
// let _ = Reflect::set(&obj, &"key".into(), &event.key.into());
// let _ = Reflect::set(&obj, &"oldValue".into(), &event.old_value.into());
// let _ = Reflect::set(&obj, &"newValue".into(), &event.new_value.into());
// let _ = Reflect::set(&obj, &"storageArea".into(), &event.storage_area.into());
//
// obj.into()
// }
// }
//
// fn get_optional_string(v: &JsValue, key: &str) -> Option<String> {
// Reflect::get(v, &key.into())
// .map(|v| v.as_string())
// .unwrap_or_default()
// }
//
// /// Error type for use_storage_with_options
// #[doc(cfg(feature = "storage"))]
// pub enum UseStorageError<E = ()> {
// NoStorage(JsValue),
// StorageAccessError(JsValue),
// CustomStorageAccessError(E),
// SerializationError(Error),
// DefaultSerializationError(Error),
// }
//
// /// Options for [`use_storage_with_options`].
// #[doc(cfg(feature = "storage"))]
// #[derive(DefaultBuilder)]
// pub struct UseStorageOptions<T> {
// /// Type of storage. Can be `Local` (default), `Session` or `Custom(web_sys::Storage)`
// storage_type: StorageType,
// /// Listen to changes to this storage key from somewhere else. Defaults to true.
// listen_to_storage_changes: bool,
// /// If no value for the give key is found in the storage, write it. Defaults to true.
// write_defaults: bool,
// /// Takes the serialized (json) stored value and the default value and returns a merged version.
// /// Defaults to simply returning the stored value.
// merge_defaults: fn(&str, &T) -> String,
// /// Optional callback whenever an error occurs. The callback takes an argument of type [`UseStorageError`].
// on_error: Box<dyn CloneableFnWithArg<UseStorageError>>,
//
// /// Debounce or throttle the writing to storage whenever the value changes.
// filter: FilterOptions,
// }
//
// impl<T> Default for UseStorageOptions<T> {
// fn default() -> Self {
// Self {
// storage_type: Default::default(),
// listen_to_storage_changes: true,
// write_defaults: true,
// merge_defaults: |stored_value, _default_value| stored_value.to_string(),
// on_error: Box::new(|_| ()),
// filter: Default::default(),
// }
// }
// }
//
// impl<T> UseStorageOptions<T> {
// filter_builder_methods!(
// /// the serializing and storing into storage
// filter
// );
// }
//
// /// Local or session storage or a custom store that is a `web_sys::Storage`.
// #[doc(cfg(feature = "storage"))]
// #[derive(Default)]
// pub enum StorageType {
// #[default]
// Local,
// Session,
// Custom(web_sys::Storage),
// }
//
// impl StorageType {
// pub fn into_storage(self) -> Result<Option<web_sys::Storage>, JsValue> {
// match self {
// StorageType::Local => window().local_storage(),
// StorageType::Session => window().session_storage(),
// StorageType::Custom(storage) => Ok(Some(storage)),
// }
// }
// }

View file

@ -66,6 +66,7 @@ use std::rc::Rc;
/// ## Recommended Reading /// ## Recommended Reading
/// ///
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle)
/// - [Debouncing and Throttling Explained Through Examples](https://css-tricks.com/debouncing-throttling-explained-examples/)
pub fn use_debounce_fn<F, R>( pub fn use_debounce_fn<F, R>(
func: F, func: F,
ms: impl Into<MaybeSignal<f64>> + 'static, ms: impl Into<MaybeSignal<f64>> + 'static,

View file

@ -1,5 +1,4 @@
use crate::core::{ElementMaybeSignal, Size}; use crate::core::{ElementMaybeSignal, Size};
use crate::watch;
use crate::{use_resize_observer_with_options, UseResizeObserverOptions}; use crate::{use_resize_observer_with_options, UseResizeObserverOptions};
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;

View file

@ -1,59 +0,0 @@
use default_struct_builder::DefaultBuilder;
use leptos::*;
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsValue;
// pub fn use_storage_with_options<T, D, MergeFn, ErrorFn>(
// cx: Scope,
// key: &str,
// defaults: D,
// options: UseStorageOptions<T, MergeFn, ErrorFn>,
// ) -> (ReadSignal<T>, WriteSignal<T>)
// where
// for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
// D: Into<MaybeSignal<T>>,
// MergeFn: Fn(T, T) -> T,
// ErrorFn: Fn(JsValue) + Clone,
// {
// let defaults = defaults.into();
//
// let (data, set_data) = create_signal(cx, defaults.get_untracked());
//
// let storage = match options.storage_type {
// StorageType::Local => window().local_storage(),
// StorageType::Session => window().session_storage(),
// };
//
// match storage {
// Ok(Some(storage)) => {}
// Err(e) => options.on_error(e),
// _ => {
// // do nothing
// }
// }
//
// (data, set_data)
// }
#[derive(DefaultBuilder)]
pub struct UseStorageOptions<T, MergeFn, ErrorFn>
where
MergeFn: Fn(T, T) -> T,
ErrorFn: Fn(JsValue),
for<'de> T: Serialize + Deserialize<'de> + 'static,
{
storage_type: StorageType,
listen_to_storage_changes: bool,
write_defaults: bool,
merge_defaults: MergeFn,
on_error: ErrorFn,
_marker: std::marker::PhantomData<T>,
}
#[derive(Default)]
pub enum StorageType {
#[default]
Local,
Session,
}

View file

@ -62,6 +62,7 @@ pub use crate::utils::ThrottleOptions;
/// ## Recommended Reading /// ## Recommended Reading
/// ///
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle) /// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle)
/// - [Debouncing and Throttling Explained Through Examples](https://css-tricks.com/debouncing-throttling-explained-examples/)
pub fn use_throttle_fn<F, R>( pub fn use_throttle_fn<F, R>(
func: F, func: F,
ms: impl Into<MaybeSignal<f64>> + 'static, ms: impl Into<MaybeSignal<f64>> + 'static,

View file

@ -0,0 +1,62 @@
use crate::utils::{CloneableFnWithReturn, FilterOptions};
use default_struct_builder::DefaultBuilder;
use leptos::*;
use std::cell::RefCell;
use std::rc::Rc;
pub struct PausableWrapperReturn<PauseFn, ResumeFn, WrappedFn, Arg, R>
where
PauseFn: Fn() + Clone,
ResumeFn: Fn() + Clone,
WrappedFn: Fn(Arg) -> Option<R> + Clone,
R: 'static,
{
pub pause: PauseFn,
pub resume: ResumeFn,
pub wrapped_fn: WrappedFn,
_marker_arg: std::marker::PhantomData<Arg>,
_marker_r: std::marker::PhantomData<R>,
}
pub fn pausable_wrapper<F, Arg, R>(
cx: Scope,
function: F,
) -> PausableWrapperReturn<
impl Fn() + Clone,
impl Fn() + Clone,
impl Fn(Arg) -> Option<R> + Clone,
Arg,
R,
>
where
R: 'static,
F: Fn(Arg) -> R + Clone,
{
let (active, set_active) = create_signal(cx, true);
let pause = move || {
set_active(false);
};
let resume = move || {
set_active(true);
};
let wrapped_fn = move |arg: Arg| {
if active.get_untracked() {
Some(function(arg))
} else {
None
}
};
PausableWrapperReturn {
pause,
resume,
wrapped_fn,
_marker_arg: std::marker::PhantomData,
_marker_r: std::marker::PhantomData,
}
}

View file

@ -24,16 +24,16 @@ use std::rc::Rc;
/// cx, /// cx,
/// num, /// num,
/// move |num, _, _| { /// move |num, _, _| {
/// log!("number {}", num); /// log!("Number {}", num);
/// }, /// },
/// ); /// );
/// ///
/// set_num(1); // > "number 1" /// set_num(1); // > "Number 1"
/// ///
/// set_timeout_with_handle(move || { /// set_timeout_with_handle(move || {
/// stop(); // stop watching /// stop(); // stop watching
/// ///
/// set_num(2); // nothing happens /// set_num(2); // (nothing happens)
/// }, Duration::from_millis(1000)); /// }, Duration::from_millis(1000));
/// # view! { cx, } /// # view! { cx, }
/// # } /// # }
@ -56,12 +56,12 @@ use std::rc::Rc;
/// cx, /// cx,
/// num, /// num,
/// move |num, _, _| { /// move |num, _, _| {
/// log!("number {}", num); /// log!("Number {}", num);
/// }, /// },
/// WatchOptions::default().immediate(true), /// WatchOptions::default().immediate(true),
/// ); // > "number 0" /// ); // > "Number 0"
/// ///
/// set_num(1); // > "number 1" /// set_num(1); // > "Number 1"
/// # view! { cx, } /// # view! { cx, }
/// # } /// # }
/// ``` /// ```
@ -81,7 +81,7 @@ use std::rc::Rc;
/// cx, /// cx,
/// num, /// num,
/// move |num, _, _| { /// move |num, _, _| {
/// log!("number {}", num); /// log!("Number {}", num);
/// }, /// },
/// WatchOptions::default().throttle(100.0), // there's also `throttle_with_options` /// WatchOptions::default().throttle(100.0), // there's also `throttle_with_options`
/// ); /// );
@ -198,7 +198,6 @@ pub struct WatchOptions {
/// the first change is detected of any signal that is accessed in `deps`. /// the first change is detected of any signal that is accessed in `deps`.
immediate: bool, immediate: bool,
#[builder(skip)]
filter: FilterOptions, filter: FilterOptions,
} }

View file

@ -11,7 +11,6 @@ use leptos_use::{watch_with_options, DebounceOptions, WatchOptions};
/// ## Usage /// ## Usage
/// ///
/// ``` /// ```
/// # use std::time::Duration;
/// # use leptos::*; /// # use leptos::*;
/// # use leptos_use::watch_debounced; /// # use leptos_use::watch_debounced;
/// # /// #
@ -36,7 +35,6 @@ use leptos_use::{watch_with_options, DebounceOptions, WatchOptions};
/// There's also `watch_debounced_with_options` where you can specify the other watch options (except `filter`). /// There's also `watch_debounced_with_options` where you can specify the other watch options (except `filter`).
/// ///
/// ``` /// ```
/// # use std::time::Duration;
/// # use leptos::*; /// # use leptos::*;
/// # use leptos_use::{watch_debounced_with_options, WatchDebouncedOptions}; /// # use leptos_use::{watch_debounced_with_options, WatchDebouncedOptions};
/// # /// #
@ -57,6 +55,11 @@ use leptos_use::{watch_with_options, DebounceOptions, WatchOptions};
/// # } /// # }
/// ``` /// ```
/// ///
/// ## Recommended Reading
///
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle)
/// - [Debouncing and Throttling Explained Through Examples](https://css-tricks.com/debouncing-throttling-explained-examples/)
///
/// ## See also /// ## See also
/// ///
/// * [`watch`] /// * [`watch`]

123
src/watch_pausable.rs Normal file
View file

@ -0,0 +1,123 @@
use crate::{watch_with_options, WatchOptions};
use leptos::*;
/// Pausable [`watch`].
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/watch_pausable)
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::{watch_pausable, WatchPausableReturn};
/// #
/// # pub fn Demo(cx: Scope) -> impl IntoView {
/// let (source, set_source) = create_signal(cx, "foo".to_string());
///
/// let WatchPausableReturn {
/// stop,
/// pause,
/// resume,
/// ..
/// } = watch_pausable(
/// cx,
/// source,
/// |v, _, _| {
/// log!("Changed to {}", v);
/// },
/// );
///
/// set_source("bar".to_string()); // > "Changed to bar"
///
/// pause();
///
/// set_source("foobar".to_string()); // (nothing happens)
///
/// resume();
///
/// set_source("hello".to_string()); // > "Changed to hello"
/// # view! { cx, }
/// # }
/// ```
///
/// There's also [`watch_pausable_with_options`] which takes the same options as [`watch`].
///
/// ## See also
///
/// * [`watch`]
pub fn watch_pausable<W, T, DFn, CFn>(
cx: Scope,
deps: DFn,
callback: CFn,
) -> WatchPausableReturn<impl Fn() + Clone, impl Fn() + Clone, impl Fn() + Clone>
where
DFn: Fn() -> W + 'static,
CFn: Fn(&W, Option<&W>, Option<T>) -> T + Clone + 'static,
W: Clone + 'static,
T: Clone + 'static,
{
watch_pausable_with_options(cx, deps, callback, WatchOptions::default())
}
/// Version of `watch_pausable` that accepts `WatchOptions`. See [`watch_pausable`] for how to use.
pub fn watch_pausable_with_options<W, T, DFn, CFn>(
cx: Scope,
deps: DFn,
callback: CFn,
options: WatchOptions,
) -> WatchPausableReturn<impl Fn() + Clone, impl Fn() + Clone, impl Fn() + Clone>
where
DFn: Fn() -> W + 'static,
CFn: Fn(&W, Option<&W>, Option<T>) -> T + Clone + 'static,
W: Clone + 'static,
T: Clone + 'static,
{
let (is_active, set_active) = create_signal(cx, true);
let pausable_callback = move |val: &W, prev_val: Option<&W>, prev_ret: Option<Option<T>>| {
if is_active.get_untracked() {
Some(callback(val, prev_val, prev_ret.unwrap_or(None)))
} else {
None
}
};
let stop = watch_with_options(cx, deps, pausable_callback, options);
let pause = move || {
set_active(false);
};
let resume = move || {
set_active(true);
};
WatchPausableReturn {
stop,
pause,
resume,
is_active: is_active.into(),
}
}
/// Return type of [`watch_pausable`]
pub struct WatchPausableReturn<StopFn, PauseFn, ResumeFn>
where
StopFn: Fn() + Clone,
PauseFn: Fn() + Clone,
ResumeFn: Fn() + Clone,
{
/// Stops the watcher
pub stop: StopFn,
/// Pauses the watcher
pub pause: PauseFn,
/// Resumes the watcher
pub resume: ResumeFn,
/// Whether the watcher is active (not paused). This doesn't reflect if the watcher has been stopped
pub is_active: Signal<bool>,
}

View file

@ -11,7 +11,6 @@ use leptos_use::{watch_with_options, ThrottleOptions, WatchOptions};
/// ## Usage /// ## Usage
/// ///
/// ``` /// ```
/// # use std::time::Duration;
/// # use leptos::*; /// # use leptos::*;
/// # use leptos_use::watch_throttled; /// # use leptos_use::watch_throttled;
/// # /// #
@ -36,7 +35,6 @@ use leptos_use::{watch_with_options, ThrottleOptions, WatchOptions};
/// There's also `watch_throttled_with_options` where you can specify the other watch options (except `filter`). /// There's also `watch_throttled_with_options` where you can specify the other watch options (except `filter`).
/// ///
/// ``` /// ```
/// # use std::time::Duration;
/// # use leptos::*; /// # use leptos::*;
/// # use leptos_use::{watch_throttled_with_options, WatchThrottledOptions}; /// # use leptos_use::{watch_throttled_with_options, WatchThrottledOptions};
/// # /// #
@ -57,6 +55,11 @@ use leptos_use::{watch_with_options, ThrottleOptions, WatchOptions};
/// # } /// # }
/// ``` /// ```
/// ///
/// ## Recommended Reading
///
/// - [**Debounce vs Throttle**: Definitive Visual Guide](https://redd.one/blog/debounce-vs-throttle)
/// - [Debouncing and Throttling Explained Through Examples](https://css-tricks.com/debouncing-throttling-explained-examples/)
///
/// ## See also /// ## See also
/// ///
/// * [`watch`] /// * [`watch`]