From 0a5eb5e8954f183562c042ee876e84b81a3fcb91 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Fri, 9 Jun 2023 23:10:33 +0100 Subject: [PATCH] added macro for simple math functions --- .idea/leptos-use.iml | 2 + CHANGELOG.md | 4 +- Cargo.toml | 12 +- README.md | 31 +- docs/book/src/SUMMARY.md | 5 + docs/book/src/demo.css | 13 +- docs/book/src/functions.md | 2 + docs/book/src/storage/use_storage.md | 1 + docs/book/src/watch/watch_pausable.md | 3 + examples/watch_pausable/Cargo.toml | 16 + examples/watch_pausable/README.md | 23 + examples/watch_pausable/Trunk.toml | 2 + examples/watch_pausable/index.html | 7 + examples/watch_pausable/input.css | 3 + examples/watch_pausable/rust-toolchain.toml | 2 + examples/watch_pausable/src/main.rs | 59 +++ examples/watch_pausable/style/output.css | 293 ++++++++++++ examples/watch_pausable/tailwind.config.js | 15 + src/lib.rs | 2 - src/math/shared.rs | 21 + src/math/use_ceil.rs | 57 ++- src/math/use_floor.rs | 57 ++- src/storage/mod.rs | 3 + src/storage/use_storage.rs | 495 ++++++++++++++++++++ src/use_debounce_fn.rs | 1 + src/use_element_size.rs | 1 - src/use_storage.rs | 59 --- src/use_throttle_fn.rs | 1 + src/utils/filters/pausable.rs | 62 +++ src/watch.rs | 15 +- src/watch_debounced.rs | 7 +- src/watch_pausable.rs | 123 +++++ src/watch_throttled.rs | 7 +- 33 files changed, 1261 insertions(+), 143 deletions(-) create mode 100644 docs/book/src/storage/use_storage.md create mode 100644 docs/book/src/watch/watch_pausable.md create mode 100644 examples/watch_pausable/Cargo.toml create mode 100644 examples/watch_pausable/README.md create mode 100644 examples/watch_pausable/Trunk.toml create mode 100644 examples/watch_pausable/index.html create mode 100644 examples/watch_pausable/input.css create mode 100644 examples/watch_pausable/rust-toolchain.toml create mode 100644 examples/watch_pausable/src/main.rs create mode 100644 examples/watch_pausable/style/output.css create mode 100644 examples/watch_pausable/tailwind.config.js create mode 100644 src/storage/mod.rs create mode 100644 src/storage/use_storage.rs delete mode 100644 src/use_storage.rs create mode 100644 src/utils/filters/pausable.rs create mode 100644 src/watch_pausable.rs diff --git a/.idea/leptos-use.iml b/.idea/leptos-use.iml index 8f87229..011b740 100644 --- a/.idea/leptos-use.iml +++ b/.idea/leptos-use.iml @@ -18,6 +18,7 @@ + @@ -33,6 +34,7 @@ + diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be3f1a..18991f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # Changelog -## 0.1.10 +## 0.2.0 #### Braking Changes - `watch` doesn't accept `immediate` as an argument anymore. This is only provided by the option variant. #### New Functions -- `use_local_storage` +- `use_storage` - `watch_debounced` - `watch_throttled` - `watch_pausable` diff --git a/Cargo.toml b/Cargo.toml index 47b007f..ca34c76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,14 +17,18 @@ homepage = "https://leptos-use.rs" leptos = "0.3.0" wasm-bindgen = "0.2.86" 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 } -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] version = "0.3.63" features = [ "CssStyleDeclaration", + "CustomEvent", + "CustomEventInit", "DomRectReadOnly", "Element", "MouseEvent", @@ -36,7 +40,6 @@ features = [ "ResizeObserverSize", "ScrollBehavior", "ScrollToOptions", - "Storage", "Touch", "TouchEvent", "TouchList", @@ -45,7 +48,8 @@ features = [ [features] docs = [] -math = ["num"] +math = ["num", "paste"] +storage = ["serde", "serde_json", "web-sys/Storage", "web-sys/StorageEvent"] [package.metadata."docs.rs"] all-features = true diff --git a/README.md b/README.md index c5dd8df..8f36a08 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,33 @@ 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! \ No newline at end of file +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 +``` \ No newline at end of file diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index a6d5cde..1df21ae 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -5,6 +5,10 @@ [Functions](functions.md) [Changelog](changelog.md) +# @Storage + +- [use_storage](storage/use_storage.md) + # Elements - [use_element_size](elements/use_element_size.md) @@ -23,6 +27,7 @@ - [watch](watch/watch.md) - [watch_debounced](watch/watch_debounced.md) +- [watch_pausable](watch/watch_pausable.md) - [watch_throttled](watch/watch_throttled.md) # Utilities diff --git a/docs/book/src/demo.css b/docs/book/src/demo.css index e88253e..c5cc0f0 100644 --- a/docs/book/src/demo.css +++ b/docs/book/src/demo.css @@ -44,15 +44,24 @@ vertical-align: middle; } -.demo-container button:hover { +.demo-container button:hover:not(:disabled) { background-color: var(--brand-color-dark); } -.demo-container button:active { +.demo-container button:active:not(:disabled) { border-bottom: 0; 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 { margin: 1rem 0; } diff --git a/docs/book/src/functions.md b/docs/book/src/functions.md index 1ae5f19..c58bc55 100644 --- a/docs/book/src/functions.md +++ b/docs/book/src/functions.md @@ -1,5 +1,7 @@ # Functions + + diff --git a/docs/book/src/storage/use_storage.md b/docs/book/src/storage/use_storage.md new file mode 100644 index 0000000..c848ea6 --- /dev/null +++ b/docs/book/src/storage/use_storage.md @@ -0,0 +1 @@ +# use_storage diff --git a/docs/book/src/watch/watch_pausable.md b/docs/book/src/watch/watch_pausable.md new file mode 100644 index 0000000..7cc5419 --- /dev/null +++ b/docs/book/src/watch/watch_pausable.md @@ -0,0 +1,3 @@ +# watch_pausable + + diff --git a/examples/watch_pausable/Cargo.toml b/examples/watch_pausable/Cargo.toml new file mode 100644 index 0000000..5fb0fc1 --- /dev/null +++ b/examples/watch_pausable/Cargo.toml @@ -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" diff --git a/examples/watch_pausable/README.md b/examples/watch_pausable/README.md new file mode 100644 index 0000000..23f17ed --- /dev/null +++ b/examples/watch_pausable/README.md @@ -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 +``` \ No newline at end of file diff --git a/examples/watch_pausable/Trunk.toml b/examples/watch_pausable/Trunk.toml new file mode 100644 index 0000000..3e4be08 --- /dev/null +++ b/examples/watch_pausable/Trunk.toml @@ -0,0 +1,2 @@ +[build] +public_url = "/demo/" \ No newline at end of file diff --git a/examples/watch_pausable/index.html b/examples/watch_pausable/index.html new file mode 100644 index 0000000..ae249a6 --- /dev/null +++ b/examples/watch_pausable/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/watch_pausable/input.css b/examples/watch_pausable/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/watch_pausable/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/watch_pausable/rust-toolchain.toml b/examples/watch_pausable/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/examples/watch_pausable/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/examples/watch_pausable/src/main.rs b/examples/watch_pausable/src/main.rs new file mode 100644 index 0000000..96bbef5 --- /dev/null +++ b/examples/watch_pausable/src/main.rs @@ -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, + "Type something below to trigger the watch" + +

"Value: " {source}

+ + + +
+
+ "Log" +
{log}
+ } +} + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + mount_to(demo_or_body(), |cx| { + view! { cx, } + }) +} diff --git a/examples/watch_pausable/style/output.css b/examples/watch_pausable/style/output.css new file mode 100644 index 0000000..d0d3e07 --- /dev/null +++ b/examples/watch_pausable/style/output.css @@ -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)); + } +} \ No newline at end of file diff --git a/examples/watch_pausable/tailwind.config.js b/examples/watch_pausable/tailwind.config.js new file mode 100644 index 0000000..bc09f5e --- /dev/null +++ b/examples/watch_pausable/tailwind.config.js @@ -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'), + ], +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 68eafef..1091298 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,6 @@ mod use_mouse; #[cfg(web_sys_unstable_apis)] mod use_resize_observer; mod use_scroll; -mod use_storage; mod use_supported; mod use_throttle_fn; pub mod utils; @@ -32,7 +31,6 @@ pub use use_mouse::*; #[cfg(web_sys_unstable_apis)] pub use use_resize_observer::*; pub use use_scroll::*; -pub use use_storage::*; pub use use_supported::*; pub use use_throttle_fn::*; pub use watch::*; diff --git a/src/math/shared.rs b/src/math/shared.rs index 8c2fa9c..c973151 100644 --- a/src/math/shared.rs +++ b/src/math/shared.rs @@ -1,3 +1,5 @@ +use paste::paste; + macro_rules! use_partial_cmp { ($(#[$outer:meta])* $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 [](cx: Scope, x: S) -> Signal + where + S: Into>, + N: Float, + { + let x = x.into(); + Signal::derive(cx, move || x.get().$fn_name()) + } + } + }; +} + pub(crate) use use_partial_cmp; +pub(crate) use use_simple_math; diff --git a/src/math/use_ceil.rs b/src/math/use_ceil.rs index 32c0ddd..7320881 100644 --- a/src/math/use_ceil.rs +++ b/src/math/use_ceil.rs @@ -1,33 +1,30 @@ +use crate::math::shared::use_simple_math; use leptos::*; use num::Float; +use paste::paste; -/// Reactive `ceil()`. -/// -/// ## Demo -/// -/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_ceil) -/// -/// ## Usage -/// -/// ``` -/// # use leptos::*; -/// # use leptos_use::math::use_ceil; -/// # -/// # #[component] -/// # fn Demo(cx: Scope) -> impl IntoView { -/// let (value, set_value) = create_signal(cx, 44.15); -/// let result: Signal = use_ceil(cx, value); // 45 -/// # -/// # assert_eq!(result.get(), 45.0); -/// # view! { cx, } -/// # } -/// ``` -#[doc(cfg(feature = "math"))] -pub fn use_ceil(cx: Scope, x: S) -> Signal -where - S: Into>, - N: Float, -{ - let x = x.into(); - Signal::derive(cx, move || x.get().ceil()) -} +use_simple_math!( + /// Reactive `ceil()`. + /// + /// ## Demo + /// + /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_ceil) + /// + /// ## Usage + /// + /// ``` + /// # use leptos::*; + /// # use leptos_use::math::use_ceil; + /// # + /// # #[component] + /// # fn Demo(cx: Scope) -> impl IntoView { + /// let (value, set_value) = create_signal(cx, 44.15); + /// let result: Signal = use_ceil(cx, value); // 45 + /// # + /// # assert_eq!(result.get(), 45.0); + /// # view! { cx, } + /// # } + /// ``` + #[doc(cfg(feature = "math"))] + ceil +); diff --git a/src/math/use_floor.rs b/src/math/use_floor.rs index d0b9621..1b91d40 100644 --- a/src/math/use_floor.rs +++ b/src/math/use_floor.rs @@ -1,33 +1,30 @@ use leptos::*; +use leptos_use::math::shared::use_simple_math; use num::Float; +use paste::paste; -/// Reactive `floor()`. -/// -/// ## Demo -/// -/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_floor) -/// -/// ## Usage -/// -/// ``` -/// # use leptos::*; -/// # use leptos_use::math::use_floor; -/// # -/// # #[component] -/// # fn Demo(cx: Scope) -> impl IntoView { -/// let (value, set_value) = create_signal(cx, 45.95); -/// let result: Signal = use_floor(cx, value); // 45 -/// # -/// # assert_eq!(result.get(), 45.0); -/// # view! { cx, } -/// # } -/// ``` -#[doc(cfg(feature = "math"))] -pub fn use_floor(cx: Scope, x: S) -> Signal -where - S: Into>, - N: Float, -{ - let x = x.into(); - Signal::derive(cx, move || x.get().floor()) -} +use_simple_math!( + /// Reactive `floor()`. + /// + /// ## Demo + /// + /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_floor) + /// + /// ## Usage + /// + /// ``` + /// # use leptos::*; + /// # use leptos_use::math::use_floor; + /// # + /// # #[component] + /// # fn Demo(cx: Scope) -> impl IntoView { + /// let (value, set_value) = create_signal(cx, 45.95); + /// let result: Signal = use_floor(cx, value); // 45 + /// # + /// # assert_eq!(result.get(), 45.0); + /// # view! { cx, } + /// # } + /// ``` + #[doc(cfg(feature = "math"))] + floor +); diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..2320414 --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,3 @@ +mod use_storage; + +pub use use_storage::*; diff --git a/src/storage/use_storage.rs b/src/storage/use_storage.rs new file mode 100644 index 0000000..6f2fbe9 --- /dev/null +++ b/src/storage/use_storage.rs @@ -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, WriteSignal, 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 +// /// +// /// // bind bool. +// /// let (flag, set_flag, remove_flag) = use_storage(cx, "my-flag", true); // returns Signal +// /// +// /// // bind number +// /// let (count, set_count, _) = use_storage(cx, "my-count", 0); // returns Signal +// /// +// /// // 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` 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::::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( +// cx: Scope, +// key: &str, +// defaults: D, +// ) -> (Signal, WriteSignal>, impl Fn() + Clone) +// where +// for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, +// D: Into>, +// 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( +// cx: Scope, +// key: &str, +// defaults: D, +// options: UseStorageOptions, +// ) -> (Signal, WriteSignal>, impl Fn() + Clone) +// where +// for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, +// D: Into>, +// 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| { +// 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| -> Option { +// 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| { +// 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, +// pub old_value: Option, +// pub new_value: Option, +// pub storage_area: Option, +// } +// +// impl From 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 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::().ok()) +// .unwrap_or_default(), +// } +// } +// } +// +// impl From 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 { +// 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 { +// 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 { +// /// 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>, +// +// /// Debounce or throttle the writing to storage whenever the value changes. +// filter: FilterOptions, +// } +// +// impl Default for UseStorageOptions { +// 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 UseStorageOptions { +// 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, JsValue> { +// match self { +// StorageType::Local => window().local_storage(), +// StorageType::Session => window().session_storage(), +// StorageType::Custom(storage) => Ok(Some(storage)), +// } +// } +// } diff --git a/src/use_debounce_fn.rs b/src/use_debounce_fn.rs index f6b0511..b073fa1 100644 --- a/src/use_debounce_fn.rs +++ b/src/use_debounce_fn.rs @@ -66,6 +66,7 @@ use std::rc::Rc; /// ## 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/) pub fn use_debounce_fn( func: F, ms: impl Into> + 'static, diff --git a/src/use_element_size.rs b/src/use_element_size.rs index 26322d0..4d18267 100644 --- a/src/use_element_size.rs +++ b/src/use_element_size.rs @@ -1,5 +1,4 @@ use crate::core::{ElementMaybeSignal, Size}; -use crate::watch; use crate::{use_resize_observer_with_options, UseResizeObserverOptions}; use default_struct_builder::DefaultBuilder; use leptos::*; diff --git a/src/use_storage.rs b/src/use_storage.rs deleted file mode 100644 index cb0ae0e..0000000 --- a/src/use_storage.rs +++ /dev/null @@ -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( -// cx: Scope, -// key: &str, -// defaults: D, -// options: UseStorageOptions, -// ) -> (ReadSignal, WriteSignal) -// where -// for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, -// D: Into>, -// 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 -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, -} - -#[derive(Default)] -pub enum StorageType { - #[default] - Local, - Session, -} diff --git a/src/use_throttle_fn.rs b/src/use_throttle_fn.rs index 01ab71c..b0c4aa6 100644 --- a/src/use_throttle_fn.rs +++ b/src/use_throttle_fn.rs @@ -62,6 +62,7 @@ pub use crate::utils::ThrottleOptions; /// ## 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/) pub fn use_throttle_fn( func: F, ms: impl Into> + 'static, diff --git a/src/utils/filters/pausable.rs b/src/utils/filters/pausable.rs new file mode 100644 index 0000000..e551a0f --- /dev/null +++ b/src/utils/filters/pausable.rs @@ -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 +where + PauseFn: Fn() + Clone, + ResumeFn: Fn() + Clone, + WrappedFn: Fn(Arg) -> Option + Clone, + R: 'static, +{ + pub pause: PauseFn, + pub resume: ResumeFn, + pub wrapped_fn: WrappedFn, + + _marker_arg: std::marker::PhantomData, + _marker_r: std::marker::PhantomData, +} + +pub fn pausable_wrapper( + cx: Scope, + function: F, +) -> PausableWrapperReturn< + impl Fn() + Clone, + impl Fn() + Clone, + impl Fn(Arg) -> Option + 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, + } +} diff --git a/src/watch.rs b/src/watch.rs index a184a79..560d7d6 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -24,16 +24,16 @@ use std::rc::Rc; /// cx, /// num, /// move |num, _, _| { -/// log!("number {}", num); +/// log!("Number {}", num); /// }, /// ); /// -/// set_num(1); // > "number 1" +/// set_num(1); // > "Number 1" /// /// set_timeout_with_handle(move || { /// stop(); // stop watching /// -/// set_num(2); // nothing happens +/// set_num(2); // (nothing happens) /// }, Duration::from_millis(1000)); /// # view! { cx, } /// # } @@ -56,12 +56,12 @@ use std::rc::Rc; /// cx, /// num, /// move |num, _, _| { -/// log!("number {}", num); +/// log!("Number {}", num); /// }, /// WatchOptions::default().immediate(true), -/// ); // > "number 0" +/// ); // > "Number 0" /// -/// set_num(1); // > "number 1" +/// set_num(1); // > "Number 1" /// # view! { cx, } /// # } /// ``` @@ -81,7 +81,7 @@ use std::rc::Rc; /// cx, /// num, /// move |num, _, _| { -/// log!("number {}", num); +/// log!("Number {}", num); /// }, /// 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`. immediate: bool, - #[builder(skip)] filter: FilterOptions, } diff --git a/src/watch_debounced.rs b/src/watch_debounced.rs index f71338e..3f8640e 100644 --- a/src/watch_debounced.rs +++ b/src/watch_debounced.rs @@ -11,7 +11,6 @@ use leptos_use::{watch_with_options, DebounceOptions, WatchOptions}; /// ## Usage /// /// ``` -/// # use std::time::Duration; /// # use leptos::*; /// # 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`). /// /// ``` -/// # use std::time::Duration; /// # use leptos::*; /// # 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 /// /// * [`watch`] diff --git a/src/watch_pausable.rs b/src/watch_pausable.rs new file mode 100644 index 0000000..fcda2a3 --- /dev/null +++ b/src/watch_pausable.rs @@ -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( + cx: Scope, + deps: DFn, + callback: CFn, +) -> WatchPausableReturn +where + DFn: Fn() -> W + 'static, + CFn: Fn(&W, Option<&W>, Option) -> 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( + cx: Scope, + deps: DFn, + callback: CFn, + options: WatchOptions, +) -> WatchPausableReturn +where + DFn: Fn() -> W + '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 pausable_callback = move |val: &W, prev_val: Option<&W>, prev_ret: Option>| { + 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 +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, +} diff --git a/src/watch_throttled.rs b/src/watch_throttled.rs index bb4857e..3f53bdd 100644 --- a/src/watch_throttled.rs +++ b/src/watch_throttled.rs @@ -11,7 +11,6 @@ use leptos_use::{watch_with_options, ThrottleOptions, WatchOptions}; /// ## Usage /// /// ``` -/// # use std::time::Duration; /// # use leptos::*; /// # 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`). /// /// ``` -/// # use std::time::Duration; /// # use leptos::*; /// # 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 /// /// * [`watch`]