use MaybeRwSignal where applicable. Fixes #4

This commit is contained in:
Maccesch 2023-07-15 16:48:29 +01:00
parent 6b9a272b12
commit e55f253e8d
12 changed files with 197 additions and 151 deletions

View file

@ -3,7 +3,13 @@
{ {
"type": "cargo", "type": "cargo",
"name": "Tests", "name": "Tests",
"cargoArgs": ["test", "--all-features"], "cargoArgs": ["test", "--features", "math,storage,docs"],
},
{
"type": "cargo",
"name": "Clippy",
"cargoArgs": ["+nightly", "clippy", "--features", "math,storage,docs", "--tests", "--", "-D", "warnings"],
"workingDir": "./",
}, },
] ]

View file

@ -3,6 +3,29 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] -
### Braking Changes 🛠
- The following functions now accept a `MaybeRwSignal` as their initial/default value which means
you can use a synchronized `RwSignal` in those places.
- `use_color_mode`
- `use_cycle_list`
- `use_favicon`
- `use_storage`
- `use_local_storage`
- `use_session_storage`
- Instead of returning `ReadSignal`, the following functions now return `Signal`.
- `use_color_mode`
- `use_favicon`
- `use_storage`
- `use_local_storage`
- `use_session_storage`
### Fixes 🍕
- `use_drop_zone` now uses `.get_untracked()` in event handlers
## [0.5.0] - 2023-07-15 ## [0.5.0] - 2023-07-15
### New Functions 🚀 ### New Functions 🚀
@ -26,8 +49,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.4.0] - 2023-07-03 ## [0.4.0] - 2023-07-03
### Braking Changes 🛠 ### Braking Changes 🛠
- Required `leptos` version is now 0.4 - Required `leptos` version is now 0.4
- Following the changes in `leptos` there is no longer a `stable` crate feature required in order to use this library with a stable toolchain. - Following the changes in `leptos` there is no longer a `stable` crate feature required in order to use this library
with a stable toolchain.
If you want to use it with a nightly toolchain you have to enable the `nightly` feature only on `leptos` directly. If you want to use it with a nightly toolchain you have to enable the `nightly` feature only on `leptos` directly.
No change is required for `leptos-use` itself. No change is required for `leptos-use` itself.
@ -61,17 +86,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.3.0] - 2023-06-13 ## [0.3.0] - 2023-06-13
### Braking Changes 🛠 ### Braking Changes 🛠
- `use_event_listener` no longer returns a `Box<dyn Fn()>` but a `impl Fn() + Clone` - `use_event_listener` no longer returns a `Box<dyn Fn()>` but a `impl Fn() + Clone`
### Changes 🔥 ### Changes 🔥
- You can now specify a `&str` or `Signal<String>` with CSS selectors wherever a node ref is accepted - You can now specify a `&str` or `Signal<String>` with CSS selectors wherever a node ref is accepted
- Callbacks of the following functions no longer require `Clone` - Callbacks of the following functions no longer require `Clone`
- `use_resize_observer` - `use_resize_observer`
- `use_intersection_observer` - `use_intersection_observer`
- These functions now also accept multiple target elements in addition to a single one: - These functions now also accept multiple target elements in addition to a single one:
- `use_resize_observer` - `use_resize_observer`
- `use_intersection_observer` - `use_intersection_observer`
### New Functions 🚀 ### New Functions 🚀

View file

@ -53,7 +53,7 @@ To run all tests run
cargo test --all-features cargo test --all-features
``` ```
## Book ### Book
First you need to install First you need to install

View file

@ -77,14 +77,20 @@ impl From<&str> for MaybeRwSignal<String> {
} }
impl<T: Clone> MaybeRwSignal<T> { impl<T: Clone> MaybeRwSignal<T> {
pub fn to_signal(&self, cx: Scope) -> (Signal<T>, WriteSignal<T>) { pub fn into_signal(self, cx: Scope) -> (Signal<T>, WriteSignal<T>) {
match self { match self {
Self::DynamicRead(s) => { Self::DynamicRead(s) => {
// TODO : this feels hacky let (r, w) = create_signal(cx, s.get_untracked());
let (_, w) = create_signal(cx, s.get_untracked());
(*s, w) create_effect(cx, move |_| {
w.update(move |w| {
*w = s.get();
});
});
(r.into(), w)
} }
Self::DynamicRw(r, w) => (*r, *w), Self::DynamicRw(r, w) => (r, w),
Self::Static(v) => { Self::Static(v) => {
let (r, w) = create_signal(cx, v.clone()); let (r, w) = create_signal(cx, v.clone());
(Signal::from(r), w) (Signal::from(r), w)

View file

@ -15,10 +15,10 @@ macro_rules! use_specific_storage {
cx: Scope, cx: Scope,
key: &str, key: &str,
defaults: D, defaults: D,
) -> (ReadSignal<T>, WriteSignal<T>, impl Fn() + Clone) ) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where where
for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
D: Into<MaybeSignal<T>>, D: Into<MaybeRwSignal<T>>,
T: Clone, T: Clone,
{ {
[<use_ $storage_name _storage_with_options>](cx, key, defaults, UseSpecificStorageOptions::default()) [<use_ $storage_name _storage_with_options>](cx, key, defaults, UseSpecificStorageOptions::default())
@ -34,10 +34,10 @@ macro_rules! use_specific_storage {
key: &str, key: &str,
defaults: D, defaults: D,
options: UseSpecificStorageOptions<T>, options: UseSpecificStorageOptions<T>,
) -> (ReadSignal<T>, WriteSignal<T>, impl Fn() + Clone) ) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where where
for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
D: Into<MaybeSignal<T>>, D: Into<MaybeRwSignal<T>>,
T: Clone, T: Clone,
{ {
use_storage_with_options(cx, key, defaults, options.into_storage_options(StorageType::[<$storage_name:camel>])) use_storage_with_options(cx, key, defaults, options.into_storage_options(StorageType::[<$storage_name:camel>]))

View file

@ -1,3 +1,4 @@
use crate::core::MaybeRwSignal;
use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions}; use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions};
use crate::storage::{use_storage_with_options, StorageType}; use crate::storage::{use_storage_with_options, StorageType};
use leptos::*; use leptos::*;

View file

@ -1,3 +1,4 @@
use crate::core::MaybeRwSignal;
use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions}; use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions};
use crate::storage::{use_storage_with_options, StorageType}; use crate::storage::{use_storage_with_options, StorageType};
use leptos::*; use leptos::*;

View file

@ -1,5 +1,6 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))] #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))]
use crate::core::MaybeRwSignal;
use crate::utils::{CloneableFn, CloneableFnWithArg, FilterOptions}; use crate::utils::{CloneableFn, CloneableFnWithArg, FilterOptions};
use crate::{ use crate::{
filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions, filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions,
@ -160,10 +161,10 @@ pub fn use_storage<T, D>(
cx: Scope, cx: Scope,
key: &str, key: &str,
defaults: D, defaults: D,
) -> (ReadSignal<T>, WriteSignal<T>, impl Fn() + Clone) ) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where where
for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
D: Into<MaybeSignal<T>>, D: Into<MaybeRwSignal<T>>,
T: Clone, T: Clone,
{ {
use_storage_with_options(cx, key, defaults, UseStorageOptions::default()) use_storage_with_options(cx, key, defaults, UseStorageOptions::default())
@ -176,10 +177,10 @@ pub fn use_storage_with_options<T, D>(
key: &str, key: &str,
defaults: D, defaults: D,
options: UseStorageOptions<T>, options: UseStorageOptions<T>,
) -> (ReadSignal<T>, WriteSignal<T>, impl Fn() + Clone) ) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
where where
for<'de> T: Serialize + Deserialize<'de> + Clone + 'static, for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
D: Into<MaybeSignal<T>>, D: Into<MaybeRwSignal<T>>,
T: Clone, T: Clone,
{ {
let defaults = defaults.into(); let defaults = defaults.into();
@ -193,7 +194,9 @@ where
filter, filter,
} = options; } = options;
let (data, set_data) = create_signal(cx, defaults.get_untracked()); let (data, set_data) = defaults.into_signal(cx);
let raw_init = data.get();
cfg_if! { if #[cfg(feature = "ssr")] { cfg_if! { if #[cfg(feature = "ssr")] {
let remove: Box<dyn CloneableFn> = Box::new(|| {}); let remove: Box<dyn CloneableFn> = Box::new(|| {});
@ -202,101 +205,102 @@ where
let remove: Box<dyn CloneableFn> = match storage { let remove: Box<dyn CloneableFn> = match storage {
Ok(Some(storage)) => { Ok(Some(storage)) => {
let on_err = on_error.clone(); let write = {
let on_error = on_error.clone();
let storage = storage.clone();
let key = key.to_string();
let store = storage.clone(); move |v: &T| {
let k = key.to_string(); match serde_json::to_string(&v) {
Ok(ref serialized) => match storage.get_item(&key) {
Ok(old_value) => {
if old_value.as_ref() != Some(serialized) {
if let Err(e) = storage.set_item(&key, serialized) {
on_error(UseStorageError::StorageAccessError(e));
} else {
let mut event_init = web_sys::CustomEventInit::new();
event_init.detail(
&StorageEventDetail {
key: Some(key.clone()),
old_value,
new_value: Some(serialized.clone()),
storage_area: Some(storage.clone()),
}
.into(),
);
let write = move |v: &T| { // importantly this should _not_ be a StorageEvent since those cannot
match serde_json::to_string(&v) { // be constructed with a non-built-in storage area
Ok(ref serialized) => match store.get_item(&k) { let _ = window().dispatch_event(
Ok(old_value) => { &web_sys::CustomEvent::new_with_event_init_dict(
if old_value.as_ref() != Some(serialized) { CUSTOM_STORAGE_EVENT_NAME,
if let Err(e) = store.set_item(&k, serialized) { &event_init,
on_err(UseStorageError::StorageAccessError(e)); )
} else { .expect("Failed to create CustomEvent"),
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_error.clone()(UseStorageError::StorageAccessError(e));
}
},
Err(e) => { Err(e) => {
on_err.clone()(UseStorageError::StorageAccessError(e)); on_error.clone()(UseStorageError::SerializationError(e));
} }
},
Err(e) => {
on_err.clone()(UseStorageError::SerializationError(e));
} }
} }
}; };
let store = storage.clone(); let read = {
let on_err = on_error.clone(); let storage = storage.clone();
let k = key.to_string(); let on_error = on_error.clone();
let def = defaults.clone(); let key = key.to_string();
let raw_init = raw_init.clone();
let read = move |event_detail: Option<StorageEventDetail>| -> Option<T> { move |event_detail: Option<StorageEventDetail>| -> Option<T> {
let raw_init = match serde_json::to_string(&def.get_untracked()) { let serialized_init = match serde_json::to_string(&raw_init) {
Ok(serialized) => Some(serialized), Ok(serialized) => Some(serialized),
Err(e) => { Err(e) => {
on_err.clone()(UseStorageError::DefaultSerializationError(e)); on_error.clone()(UseStorageError::DefaultSerializationError(e));
None None
} }
}; };
let raw_value = if let Some(event_detail) = event_detail { let raw_value = if let Some(event_detail) = event_detail {
event_detail.new_value event_detail.new_value
} else { } else {
match store.get_item(&k) { match storage.get_item(&key) {
Ok(raw_value) => match raw_value { Ok(raw_value) => match raw_value {
Some(raw_value) => { Some(raw_value) => Some(merge_defaults(&raw_value, &raw_init)),
Some(merge_defaults(&raw_value, &def.get_untracked())) None => serialized_init.clone(),
},
Err(e) => {
on_error.clone()(UseStorageError::StorageAccessError(e));
None
} }
None => raw_init.clone(),
},
Err(e) => {
on_err.clone()(UseStorageError::StorageAccessError(e));
None
} }
} };
};
match raw_value { match raw_value {
Some(raw_value) => match serde_json::from_str(&raw_value) { Some(raw_value) => match serde_json::from_str(&raw_value) {
Ok(v) => Some(v), Ok(v) => Some(v),
Err(e) => { Err(e) => {
on_err.clone()(UseStorageError::SerializationError(e)); on_error.clone()(UseStorageError::SerializationError(e));
None None
} }
}, },
None => { None => {
if let Some(raw_init) = &raw_init { if let Some(serialized_init) = &serialized_init {
if write_defaults { if write_defaults {
if let Err(e) = store.set_item(&k, raw_init) { if let Err(e) = storage.set_item(&key, serialized_init) {
on_err(UseStorageError::StorageAccessError(e)); on_error(UseStorageError::StorageAccessError(e));
}
} }
} }
}
Some(def.get_untracked()) Some(raw_init)
}
} }
} }
}; };
@ -312,40 +316,43 @@ where
WatchOptions::default().filter(filter), WatchOptions::default().filter(filter),
); );
let k = key.to_string(); let update = {
let store = storage.clone(); let key = key.to_string();
let storage = storage.clone();
let raw_init = raw_init.clone();
let update = move |event_detail: Option<StorageEventDetail>| { move |event_detail: Option<StorageEventDetail>| {
if let Some(event_detail) = &event_detail { if let Some(event_detail) = &event_detail {
if event_detail.storage_area != Some(store) { if event_detail.storage_area != Some(storage) {
return;
}
match &event_detail.key {
None => {
set_data.set(defaults.get_untracked());
return; return;
} }
Some(event_key) => {
if event_key != &k { match &event_detail.key {
None => {
set_data.set(raw_init);
return; return;
} }
} Some(event_key) => {
}; if event_key != &key {
} return;
}
}
};
}
pause_watch(); pause_watch();
if let Some(value) = read(event_detail.clone()) { if let Some(value) = read(event_detail.clone()) {
set_data.set(value); set_data.set(value);
} }
if event_detail.is_some() { if event_detail.is_some() {
// use timeout to avoid inifinite loop // use timeout to avoid inifinite loop
let resume = resume_watch.clone(); let resume = resume_watch.clone();
let _ = set_timeout_with_handle(resume, Duration::ZERO); let _ = set_timeout_with_handle(resume, Duration::ZERO);
} else { } else {
resume_watch(); resume_watch();
}
} }
}; };

View file

@ -1,4 +1,4 @@
use crate::core::ElementMaybeSignal; use crate::core::{ElementMaybeSignal, MaybeRwSignal};
#[cfg(feature = "storage")] #[cfg(feature = "storage")]
use crate::storage::{use_storage_with_options, UseStorageOptions}; use crate::storage::{use_storage_with_options, UseStorageOptions};
#[cfg(feature = "storage")] #[cfg(feature = "storage")]
@ -248,7 +248,7 @@ where
UseColorModeReturn { UseColorModeReturn {
mode, mode,
set_mode: set_store, set_mode: set_store,
store: store.into(), store,
set_store, set_store,
system, system,
state, state,
@ -269,20 +269,21 @@ pub enum ColorMode {
cfg_if! { if #[cfg(feature = "storage")] { cfg_if! { if #[cfg(feature = "storage")] {
fn get_store_signal( fn get_store_signal(
cx: Scope, cx: Scope,
initial_value: MaybeSignal<ColorMode>, initial_value: MaybeRwSignal<ColorMode>,
storage_signal: Option<RwSignal<ColorMode>>, storage_signal: Option<RwSignal<ColorMode>>,
storage_key: &str, storage_key: &str,
storage_enabled: bool, storage_enabled: bool,
storage: StorageType, storage: StorageType,
listen_to_storage_changes: bool, listen_to_storage_changes: bool,
) -> (ReadSignal<ColorMode>, WriteSignal<ColorMode>) { ) -> (Signal<ColorMode>, WriteSignal<ColorMode>) {
if let Some(storage_signal) = storage_signal { if let Some(storage_signal) = storage_signal {
storage_signal.split() let (store, set_store) = storage_signal.split();
(store.into(), set_store)
} else if storage_enabled { } else if storage_enabled {
let (store, set_store, _) = use_storage_with_options( let (store, set_store, _) = use_storage_with_options(
cx, cx,
storage_key, storage_key,
initial_value.get_untracked(), initial_value,
UseStorageOptions::default() UseStorageOptions::default()
.listen_to_storage_changes(listen_to_storage_changes) .listen_to_storage_changes(listen_to_storage_changes)
.storage_type(storage), .storage_type(storage),
@ -290,23 +291,24 @@ cfg_if! { if #[cfg(feature = "storage")] {
(store, set_store) (store, set_store)
} else { } else {
create_signal(cx, initial_value.get_untracked()) initial_value.into_signal(cx)
} }
} }
} else { } else {
fn get_store_signal( fn get_store_signal(
cx: Scope, cx: Scope,
initial_value: MaybeSignal<ColorMode>, initial_value: MaybeRwSignal<ColorMode>,
storage_signal: Option<RwSignal<ColorMode>>, storage_signal: Option<RwSignal<ColorMode>>,
_storage_key: &String, _storage_key: &String,
_storage_enabled: bool, _storage_enabled: bool,
_storage: StorageType, _storage: StorageType,
_listen_to_storage_changes: bool, _listen_to_storage_changes: bool,
) -> (ReadSignal<ColorMode>, WriteSignal<ColorMode>) { ) -> (Signal<ColorMode>, WriteSignal<ColorMode>) {
if let Some(storage_signal) = storage_signal { if let Some(storage_signal) = storage_signal {
storage_signal.split() let (store, set_store) = storage_signal.split();
(store.into(), set_store)
} else { } else {
create_signal(cx, initial_value.get_untracked()) initial_value.into_signal(cx)
} }
} }
}} }}
@ -368,7 +370,7 @@ where
/// Initial value of the color mode. Defaults to `"Auto"`. /// Initial value of the color mode. Defaults to `"Auto"`.
#[builder(into)] #[builder(into)]
initial_value: MaybeSignal<ColorMode>, initial_value: MaybeRwSignal<ColorMode>,
/// Custom modes that you plan to use as `ColorMode::Custom(x)`. Defaults to `vec![]`. /// Custom modes that you plan to use as `ColorMode::Custom(x)`. Defaults to `vec![]`.
custom_modes: Vec<String>, custom_modes: Vec<String>,

View file

@ -84,7 +84,7 @@ where
} }
}; };
let (state, set_state) = get_initial_value().to_signal(cx); let (state, set_state) = get_initial_value().into_signal(cx);
let index = { let index = {
let list = list.clone(); let list = list.clone();

View file

@ -100,7 +100,7 @@ where
update_files(&event); update_files(&event);
on_enter(UseDropZoneEvent { on_enter(UseDropZoneEvent {
files: files.get(), files: files.get_untracked(),
event, event,
}); });
}); });
@ -109,7 +109,7 @@ where
event.prevent_default(); event.prevent_default();
update_files(&event); update_files(&event);
on_over(UseDropZoneEvent { on_over(UseDropZoneEvent {
files: files.get(), files: files.get_untracked(),
event, event,
}); });
}); });
@ -124,7 +124,7 @@ where
update_files(&event); update_files(&event);
on_leave(UseDropZoneEvent { on_leave(UseDropZoneEvent {
files: files.get(), files: files.get_untracked(),
event, event,
}); });
}); });
@ -137,7 +137,7 @@ where
update_files(&event); update_files(&event);
on_drop(UseDropZoneEvent { on_drop(UseDropZoneEvent {
files: files.get(), files: files.get_untracked(),
event, event,
}); });
}); });

View file

@ -1,5 +1,6 @@
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))] #![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports))]
use crate::core::MaybeRwSignal;
use crate::watch; use crate::watch;
use cfg_if::cfg_if; use cfg_if::cfg_if;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
@ -59,7 +60,7 @@ use wasm_bindgen::JsCast;
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// On the server only the signals work but no favicon will be changed obviously. /// On the server only the signals work but no favicon will be changed obviously.
pub fn use_favicon(cx: Scope) -> (ReadSignal<Option<String>>, WriteSignal<Option<String>>) { pub fn use_favicon(cx: Scope) -> (Signal<Option<String>>, WriteSignal<Option<String>>) {
use_favicon_with_options(cx, UseFaviconOptions::default()) use_favicon_with_options(cx, UseFaviconOptions::default())
} }
@ -67,18 +68,14 @@ pub fn use_favicon(cx: Scope) -> (ReadSignal<Option<String>>, WriteSignal<Option
pub fn use_favicon_with_options( pub fn use_favicon_with_options(
cx: Scope, cx: Scope,
options: UseFaviconOptions, options: UseFaviconOptions,
) -> (ReadSignal<Option<String>>, WriteSignal<Option<String>>) { ) -> (Signal<Option<String>>, WriteSignal<Option<String>>) {
let UseFaviconOptions { let UseFaviconOptions {
new_icon, new_icon,
base_url, base_url,
rel, rel,
} = options; } = options;
let (favicon, set_favicon) = create_signal(cx, new_icon.get_untracked()); let (favicon, set_favicon) = new_icon.into_signal(cx);
if matches!(&new_icon, MaybeSignal::Dynamic(_)) {
create_effect(cx, move |_| set_favicon.set(new_icon.get()));
}
cfg_if! { if #[cfg(not(feature = "ssr"))] { cfg_if! { if #[cfg(not(feature = "ssr"))] {
let link_selector = format!("link[rel*=\"{rel}\"]"); let link_selector = format!("link[rel*=\"{rel}\"]");
@ -116,9 +113,9 @@ pub fn use_favicon_with_options(
/// Options for [`use_favicon_with_options`]. /// Options for [`use_favicon_with_options`].
#[derive(DefaultBuilder)] #[derive(DefaultBuilder)]
pub struct UseFaviconOptions { pub struct UseFaviconOptions {
/// New input favicon. Can be a `Signal` in which case updates will change the favicon. Defaults to None. /// New input favicon. Can be a `RwSignal` in which case updates will change the favicon. Defaults to None.
#[builder(into)] #[builder(into)]
new_icon: MaybeSignal<Option<String>>, new_icon: MaybeRwSignal<Option<String>>,
/// Base URL of the favicon. Defaults to "". /// Base URL of the favicon. Defaults to "".
#[builder(into)] #[builder(into)]