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_element_size/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" />
<excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/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_element_size/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" />
</content>
<orderEntry type="inheritedJdk" />

View file

@ -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`

View file

@ -19,12 +19,16 @@ wasm-bindgen = "0.2.86"
js-sys = "0.3.63"
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

View file

@ -23,3 +23,32 @@
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!
## 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)
[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

View file

@ -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;
}

View file

@ -1,5 +1,7 @@
# Functions
<!-- cmdrun python3 generate_function_overview.py storage storage -->
<!-- cmdrun python3 generate_function_overview.py elements -->
<!-- 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)]
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::*;

View file

@ -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 [<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_simple_math;

View file

@ -1,6 +1,9 @@
use crate::math::shared::use_simple_math;
use leptos::*;
use num::Float;
use paste::paste;
use_simple_math!(
/// Reactive `ceil()`.
///
/// ## Demo
@ -23,11 +26,5 @@ use num::Float;
/// # }
/// ```
#[doc(cfg(feature = "math"))]
pub fn use_ceil<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().ceil())
}
ceil
);

View file

@ -1,6 +1,9 @@
use leptos::*;
use leptos_use::math::shared::use_simple_math;
use num::Float;
use paste::paste;
use_simple_math!(
/// Reactive `floor()`.
///
/// ## Demo
@ -23,11 +26,5 @@ use num::Float;
/// # }
/// ```
#[doc(cfg(feature = "math"))]
pub fn use_floor<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().floor())
}
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
///
/// - [**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>(
func: F,
ms: impl Into<MaybeSignal<f64>> + 'static,

View file

@ -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::*;

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
///
/// - [**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>(
func: F,
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,
/// 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,
}

View file

@ -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`]

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
///
/// ```
/// # 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`]