mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-01-23 09:09:21 -05:00
Merge branch 'feral-dot-io/main'
This commit is contained in:
commit
8827eefa58
23 changed files with 765 additions and 653 deletions
|
@ -3,14 +3,14 @@
|
|||
{
|
||||
"type": "cargo",
|
||||
"name": "Tests",
|
||||
"cargoArgs": ["test", "--features", "math,storage,docs"],
|
||||
"cargoArgs": ["test", "--features", "math,prost,serde,docs"],
|
||||
},
|
||||
{
|
||||
"type": "cargo",
|
||||
"name": "Clippy",
|
||||
"cargoArgs": ["+nightly", "clippy", "--features", "math,storage,docs", "--tests", "--", "-D", "warnings"],
|
||||
"cargoArgs": ["+nightly", "clippy", "--features", "math,prost,serde,docs", "--tests", "--", "-D", "warnings"],
|
||||
"workingDir": "./",
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -30,7 +30,7 @@ jobs:
|
|||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --features storage,docs,math --tests -- -D warnings
|
||||
run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings
|
||||
- name: Run tests
|
||||
run: cargo test --all-features
|
||||
|
||||
|
@ -110,4 +110,4 @@ jobs:
|
|||
# - name: Publish to Coveralls
|
||||
# uses: coverallsapp/github-action@master
|
||||
# with:
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -23,6 +23,6 @@ jobs:
|
|||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --features storage,docs,math --tests -- -D warnings
|
||||
run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings
|
||||
- name: Run tests
|
||||
run: cargo test --all-features
|
||||
|
|
|
@ -13,6 +13,7 @@ repository = "https://github.com/Synphonyte/leptos-use"
|
|||
homepage = "https://leptos-use.rs"
|
||||
|
||||
[dependencies]
|
||||
base64 = { version = "0.21", optional = true }
|
||||
cfg-if = "1"
|
||||
default-struct-builder = "0.5"
|
||||
futures-util = "0.3"
|
||||
|
@ -22,8 +23,10 @@ lazy_static = "1"
|
|||
leptos = "0.5"
|
||||
num = { version = "0.4", optional = true }
|
||||
paste = "1"
|
||||
prost = { version = "0.12", optional = true }
|
||||
serde = { version = "1", optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
|
@ -81,6 +84,7 @@ features = [
|
|||
"ServiceWorkerRegistration",
|
||||
"ServiceWorkerState",
|
||||
"Storage",
|
||||
"StorageEvent",
|
||||
"Touch",
|
||||
"TouchEvent",
|
||||
"TouchList",
|
||||
|
@ -92,7 +96,8 @@ features = [
|
|||
[features]
|
||||
docs = []
|
||||
math = ["num"]
|
||||
storage = ["serde", "serde_json", "web-sys/StorageEvent"]
|
||||
prost = ["base64", "dep:prost"]
|
||||
serde = ["dep:serde", "serde_json"]
|
||||
ssr = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
[Changelog](changelog.md)
|
||||
[Functions](functions.md)
|
||||
|
||||
# @Storage
|
||||
# Storage
|
||||
|
||||
- [use_local_storage](storage/use_local_storage.md)
|
||||
- [use_session_storage](storage/use_session_storage.md)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# use_local_storage
|
||||
|
||||
<!-- cmdrun python3 ../extract_doc_comment.py storage/use_local_storage storage -->
|
||||
<!-- cmdrun python3 ../extract_doc_comment.py storage/use_local_storage -->
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# use_session_storage
|
||||
|
||||
<!-- cmdrun python3 ../extract_doc_comment.py storage/use_session_storage storage -->
|
||||
<!-- cmdrun python3 ../extract_doc_comment.py storage/use_session_storage -->
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# use_storage
|
||||
|
||||
<!-- cmdrun python3 ../extract_doc_comment.py storage/use_storage storage -->
|
||||
<!-- cmdrun python3 ../extract_doc_comment.py storage/use_storage -->
|
||||
|
|
|
@ -15,7 +15,7 @@ leptos = { version = "0.5", features = ["nightly"] }
|
|||
leptos_axum = { version = "0.5", optional = true }
|
||||
leptos_meta = { version = "0.5", features = ["nightly"] }
|
||||
leptos_router = { version = "0.5", features = ["nightly"] }
|
||||
leptos-use = { path = "../..", features = ["storage"] }
|
||||
leptos-use = { path = "../.." }
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1.25.0", optional = true }
|
||||
|
|
|
@ -8,7 +8,7 @@ leptos = { version = "0.5", features = ["nightly", "csr"] }
|
|||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos-use = { path = "../..", features = ["docs", "storage"] }
|
||||
leptos-use = { path = "../..", features = ["docs"] }
|
||||
web-sys = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -8,7 +8,7 @@ leptos = { version = "0.5", features = ["nightly", "csr"] }
|
|||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos-use = { path = "../..", features = ["docs", "storage"] }
|
||||
leptos-use = { path = "../..", features = ["docs", "prost", "serde"] }
|
||||
web-sys = "0.3"
|
||||
serde = "1.0.163"
|
||||
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
use leptos::*;
|
||||
use leptos_use::docs::{demo_or_body, Note};
|
||||
use leptos_use::storage::use_storage;
|
||||
use leptos_use::storage::{use_local_storage, JsonCodec};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct BananaState {
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
pub size: String,
|
||||
pub wearing: String,
|
||||
pub descending: String,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
impl Default for BananaState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Bananas".to_string(),
|
||||
wearing: "pyjamas".to_string(),
|
||||
descending: "stairs".to_string(),
|
||||
count: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Demo() -> impl IntoView {
|
||||
let the_default = BananaState {
|
||||
name: "Banana".to_string(),
|
||||
color: "Yellow".to_string(),
|
||||
size: "Medium".to_string(),
|
||||
count: 0,
|
||||
};
|
||||
|
||||
let (state, set_state, _) = use_storage("banana-state", the_default.clone());
|
||||
|
||||
let (state2, ..) = use_storage("banana-state", the_default.clone());
|
||||
let (state, set_state, reset) = use_local_storage::<BananaState, JsonCodec>("banana-state");
|
||||
let (state2, _, _) = use_local_storage::<BananaState, JsonCodec>("banana-state");
|
||||
|
||||
view! {
|
||||
<input
|
||||
|
@ -33,14 +36,14 @@ fn Demo() -> impl IntoView {
|
|||
/>
|
||||
<input
|
||||
class="block"
|
||||
prop:value=move || state.get().color
|
||||
on:input=move |e| set_state.update(|s| s.color = event_target_value(&e))
|
||||
prop:value=move || state.get().wearing
|
||||
on:input=move |e| set_state.update(|s| s.wearing = event_target_value(&e))
|
||||
type="text"
|
||||
/>
|
||||
<input
|
||||
class="block"
|
||||
prop:value=move || state.get().size
|
||||
on:input=move |e| set_state.update(|s| s.size = event_target_value(&e))
|
||||
prop:value=move || state.get().descending
|
||||
on:input=move |e| set_state.update(|s| s.descending = event_target_value(&e))
|
||||
type="text"
|
||||
/>
|
||||
<input
|
||||
|
@ -57,6 +60,7 @@ fn Demo() -> impl IntoView {
|
|||
step="1"
|
||||
max="1000"
|
||||
/>
|
||||
<button on:click=move |_| reset()>"Delete from storage"</button>
|
||||
|
||||
<p>
|
||||
"Second " <b>
|
||||
|
@ -67,7 +71,9 @@ fn Demo() -> impl IntoView {
|
|||
<pre>{move || format!("{:#?}", state2.get())}</pre>
|
||||
|
||||
<Note>
|
||||
"The values are persistent. When you reload the page the values will be the same."
|
||||
"The values are persistent. When you reload the page or "
|
||||
<a href="#" target="_blank">"open a second window"</a>
|
||||
", the values will be the same."
|
||||
</Note>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ use leptos::window;
|
|||
use wasm_bindgen::JsValue;
|
||||
|
||||
/// Local or session storage or a custom store that is a `web_sys::Storage`.
|
||||
// #[doc(cfg(feature = "storage"))]
|
||||
#[derive(Default)]
|
||||
pub enum StorageType {
|
||||
#[default]
|
||||
|
|
|
@ -6,7 +6,6 @@ pub mod core;
|
|||
pub mod docs;
|
||||
#[cfg(feature = "math")]
|
||||
pub mod math;
|
||||
#[cfg(feature = "storage")]
|
||||
pub mod storage;
|
||||
pub mod utils;
|
||||
|
||||
|
|
151
src/storage/codec_json.rs
Normal file
151
src/storage/codec_json.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use super::Codec;
|
||||
|
||||
/// A codec for storing JSON messages that relies on [`serde_json`] to parse.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, JsonCodec};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// #
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// // Primitive types:
|
||||
/// let (get, set, remove) = use_local_storage::<i32, JsonCodec>("my-key");
|
||||
///
|
||||
/// // Structs:
|
||||
/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||
/// pub struct MyState {
|
||||
/// pub hello: String,
|
||||
/// }
|
||||
/// let (get, set, remove) = use_local_storage::<MyState, JsonCodec>("my-struct-key");
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Versioning
|
||||
///
|
||||
/// If the JSON decoder fails, the storage hook will return `T::Default` dropping the stored JSON value. See [`Codec`](super::Codec) for general information on codec versioning.
|
||||
///
|
||||
/// ### Rely on serde
|
||||
/// This codec uses [`serde_json`] under the hood. A simple way to avoid complex versioning is to rely on serde's [field attributes](https://serde.rs/field-attrs.html) such as [`serde(default)`](https://serde.rs/field-attrs.html#default) and [`serde(rename = "...")`](https://serde.rs/field-attrs.html#rename).
|
||||
///
|
||||
/// ### String replacement
|
||||
/// Previous versions of leptos-use offered a `merge_defaults` fn to rewrite the encoded value. This is possible by wrapping the codec but should be avoided.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, Codec, JsonCodec};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// #
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||
/// pub struct MyState {
|
||||
/// pub hello: String,
|
||||
/// pub greeting: String,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone, Default)]
|
||||
/// pub struct MyStateCodec();
|
||||
/// impl Codec<MyState> for MyStateCodec {
|
||||
/// type Error = serde_json::Error;
|
||||
///
|
||||
/// fn encode(&self, val: &MyState) -> Result<String, Self::Error> {
|
||||
/// serde_json::to_string(val)
|
||||
/// }
|
||||
///
|
||||
/// fn decode(&self, stored_value: String) -> Result<MyState, Self::Error> {
|
||||
/// let default_value = MyState::default();
|
||||
/// let rewritten = if stored_value.contains(r#""greeting":"#) {
|
||||
/// stored_value
|
||||
/// } else {
|
||||
/// // add "greeting": "Hello" to the string
|
||||
/// stored_value.replace("}", &format!(r#""greeting": "{}"}}"#, default_value.greeting))
|
||||
/// };
|
||||
/// serde_json::from_str(&rewritten)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let (get, set, remove) = use_local_storage::<MyState, MyStateCodec>("my-struct-key");
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ### Transform a `JsValue`
|
||||
/// A better alternative to string replacement might be to parse the JSON then transform the resulting `JsValue` before decoding it to to your struct again.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, Codec, JsonCodec};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use serde_json::json;
|
||||
/// #
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// #[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||
/// pub struct MyState {
|
||||
/// pub hello: String,
|
||||
/// pub greeting: String,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone, Default)]
|
||||
/// pub struct MyStateCodec();
|
||||
/// impl Codec<MyState> for MyStateCodec {
|
||||
/// type Error = serde_json::Error;
|
||||
///
|
||||
/// fn encode(&self, val: &MyState) -> Result<String, Self::Error> {
|
||||
/// serde_json::to_string(val)
|
||||
/// }
|
||||
///
|
||||
/// fn decode(&self, stored_value: String) -> Result<MyState, Self::Error> {
|
||||
/// let mut val: serde_json::Value = serde_json::from_str(&stored_value)?;
|
||||
/// // add "greeting": "Hello" to the object if it's missing
|
||||
/// if let Some(obj) = val.as_object_mut() {
|
||||
/// if !obj.contains_key("greeting") {
|
||||
/// obj.insert("greeting".to_string(), json!("Hello"));
|
||||
/// }
|
||||
/// serde_json::from_value(val)
|
||||
/// } else {
|
||||
/// Ok(MyState::default())
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let (get, set, remove) = use_local_storage::<MyState, MyStateCodec>("my-struct-key");
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
pub struct JsonCodec();
|
||||
|
||||
impl<T: serde::Serialize + serde::de::DeserializeOwned> Codec<T> for JsonCodec {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn encode(&self, val: &T) -> Result<String, Self::Error> {
|
||||
serde_json::to_string(val)
|
||||
}
|
||||
|
||||
fn decode(&self, str: String) -> Result<T, Self::Error> {
|
||||
serde_json::from_str(&str)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_json_codec() {
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
struct Test {
|
||||
s: String,
|
||||
i: i32,
|
||||
}
|
||||
let t = Test {
|
||||
s: String::from("party time 🎉"),
|
||||
i: 42,
|
||||
};
|
||||
let codec = JsonCodec();
|
||||
let enc = codec.encode(&t).unwrap();
|
||||
let dec: Test = codec.decode(enc).unwrap();
|
||||
assert_eq!(dec, t);
|
||||
}
|
||||
}
|
79
src/storage/codec_prost.rs
Normal file
79
src/storage/codec_prost.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use super::Codec;
|
||||
use base64::Engine;
|
||||
use thiserror::Error;
|
||||
|
||||
/// A codec for storing ProtoBuf messages that relies on [`prost`] to parse.
|
||||
///
|
||||
/// [Protocol buffers](https://protobuf.dev/overview/) is a serialisation format useful for long-term storage. It provides semantics for versioning that are not present in JSON or other formats. [`prost`] is a Rust implementation of Protocol Buffers.
|
||||
///
|
||||
/// This codec uses [`prost`] to encode the message and then [`base64`](https://docs.rs/base64) to represent the bytes as a string.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, ProstCodec};
|
||||
/// #
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// // Primitive types:
|
||||
/// let (get, set, remove) = use_local_storage::<i32, ProstCodec>("my-key");
|
||||
///
|
||||
/// // Structs:
|
||||
/// #[derive(Clone, PartialEq, prost::Message)]
|
||||
/// pub struct MyState {
|
||||
/// #[prost(string, tag = "1")]
|
||||
/// pub hello: String,
|
||||
/// }
|
||||
/// let (get, set, remove) = use_local_storage::<MyState, ProstCodec>("my-struct-key");
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Note: we've defined and used the `prost` attribute here for brevity. Alternate usage would be to describe the message in a .proto file and use [`prost_build`](https://docs.rs/prost-build) to auto-generate the Rust code.
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
pub struct ProstCodec();
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum ProstCodecError {
|
||||
#[error("failed to decode base64")]
|
||||
DecodeBase64(base64::DecodeError),
|
||||
#[error("failed to decode protobuf")]
|
||||
DecodeProst(#[from] prost::DecodeError),
|
||||
}
|
||||
|
||||
impl<T: Default + prost::Message> Codec<T> for ProstCodec {
|
||||
type Error = ProstCodecError;
|
||||
|
||||
fn encode(&self, val: &T) -> Result<String, Self::Error> {
|
||||
let buf = val.encode_to_vec();
|
||||
Ok(base64::engine::general_purpose::STANDARD.encode(buf))
|
||||
}
|
||||
|
||||
fn decode(&self, str: String) -> Result<T, Self::Error> {
|
||||
let buf = base64::engine::general_purpose::STANDARD
|
||||
.decode(str)
|
||||
.map_err(ProstCodecError::DecodeBase64)?;
|
||||
T::decode(buf.as_slice()).map_err(ProstCodecError::DecodeProst)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_prost_codec() {
|
||||
#[derive(Clone, PartialEq, prost::Message)]
|
||||
struct Test {
|
||||
#[prost(string, tag = "1")]
|
||||
s: String,
|
||||
#[prost(int32, tag = "2")]
|
||||
i: i32,
|
||||
}
|
||||
let t = Test {
|
||||
s: String::from("party time 🎉"),
|
||||
i: 42,
|
||||
};
|
||||
let codec = ProstCodec();
|
||||
assert_eq!(codec.decode(codec.encode(&t).unwrap()), Ok(t));
|
||||
}
|
||||
}
|
44
src/storage/codec_string.rs
Normal file
44
src/storage/codec_string.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use super::Codec;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A codec for strings that relies on [`FromStr`] and [`ToString`] to parse.
|
||||
///
|
||||
/// This makes simple key / value easy to use for primitive types. It is also useful for encoding simple data structures without depending on serde.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, StringCodec};
|
||||
/// #
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// let (get, set, remove) = use_local_storage::<i32, StringCodec>("my-key");
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
pub struct StringCodec();
|
||||
|
||||
impl<T: FromStr + ToString> Codec<T> for StringCodec {
|
||||
type Error = T::Err;
|
||||
|
||||
fn encode(&self, val: &T) -> Result<String, Self::Error> {
|
||||
Ok(val.to_string())
|
||||
}
|
||||
|
||||
fn decode(&self, str: String) -> Result<T, Self::Error> {
|
||||
T::from_str(&str)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_string_codec() {
|
||||
let s = String::from("party time 🎉");
|
||||
let codec = StringCodec();
|
||||
assert_eq!(codec.encode(&s), Ok(s.clone()));
|
||||
assert_eq!(codec.decode(s.clone()), Ok(s));
|
||||
}
|
||||
}
|
|
@ -1,8 +1,18 @@
|
|||
mod shared;
|
||||
#[cfg(feature = "serde")]
|
||||
mod codec_json;
|
||||
#[cfg(feature = "prost")]
|
||||
mod codec_prost;
|
||||
mod codec_string;
|
||||
mod use_local_storage;
|
||||
mod use_session_storage;
|
||||
mod use_storage;
|
||||
|
||||
pub use crate::core::StorageType;
|
||||
#[cfg(feature = "serde")]
|
||||
pub use codec_json::*;
|
||||
#[cfg(feature = "prost")]
|
||||
pub use codec_prost::*;
|
||||
pub use codec_string::*;
|
||||
pub use use_local_storage::*;
|
||||
pub use use_session_storage::*;
|
||||
pub use use_storage::*;
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
use crate::filter_builder_methods;
|
||||
use crate::storage::{StorageType, UseStorageError, UseStorageOptions};
|
||||
use crate::utils::{DebounceOptions, FilterOptions, ThrottleOptions};
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
macro_rules! use_specific_storage {
|
||||
($(#[$outer:meta])*
|
||||
$storage_name:ident
|
||||
#[$simple_func:meta]
|
||||
) => {
|
||||
paste! {
|
||||
$(#[$outer])*
|
||||
pub fn [<use_ $storage_name _storage>]<T, D>(
|
||||
key: &str,
|
||||
defaults: D,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
|
||||
D: Into<MaybeRwSignal<T>>,
|
||||
T: Clone,
|
||||
{
|
||||
[<use_ $storage_name _storage_with_options>](key, defaults, UseSpecificStorageOptions::default())
|
||||
}
|
||||
|
||||
/// Version of
|
||||
#[$simple_func]
|
||||
/// that accepts [`UseSpecificStorageOptions`]. See
|
||||
#[$simple_func]
|
||||
/// for how to use.
|
||||
pub fn [<use_ $storage_name _storage_with_options>]<T, D>(
|
||||
key: &str,
|
||||
defaults: D,
|
||||
options: UseSpecificStorageOptions<T>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
|
||||
D: Into<MaybeRwSignal<T>>,
|
||||
T: Clone,
|
||||
{
|
||||
use_storage_with_options(key, defaults, options.into_storage_options(StorageType::[<$storage_name:camel>]))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use use_specific_storage;
|
||||
|
||||
/// Options for [`use_local_storage_with_options`].
|
||||
// #[doc(cfg(feature = "storage"))]
|
||||
#[derive(DefaultBuilder)]
|
||||
pub struct UseSpecificStorageOptions<T> {
|
||||
/// 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: Rc<dyn Fn(UseStorageError)>,
|
||||
|
||||
/// Debounce or throttle the writing to storage whenever the value changes.
|
||||
filter: FilterOptions,
|
||||
}
|
||||
|
||||
impl<T> Default for UseSpecificStorageOptions<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
listen_to_storage_changes: true,
|
||||
write_defaults: true,
|
||||
merge_defaults: |stored_value, _default_value| stored_value.to_string(),
|
||||
on_error: Rc::new(|_| ()),
|
||||
filter: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UseSpecificStorageOptions<T> {
|
||||
pub fn into_storage_options(self, storage_type: StorageType) -> UseStorageOptions<T> {
|
||||
UseStorageOptions {
|
||||
storage_type,
|
||||
listen_to_storage_changes: self.listen_to_storage_changes,
|
||||
write_defaults: self.write_defaults,
|
||||
merge_defaults: self.merge_defaults,
|
||||
on_error: self.on_error,
|
||||
filter: self.filter,
|
||||
}
|
||||
}
|
||||
|
||||
filter_builder_methods!(
|
||||
/// the serializing and storing into storage
|
||||
filter
|
||||
);
|
||||
}
|
|
@ -1,22 +1,36 @@
|
|||
use crate::core::MaybeRwSignal;
|
||||
use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions};
|
||||
use crate::storage::{use_storage_with_options, StorageType};
|
||||
use leptos::*;
|
||||
use paste::paste;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::{use_storage, Codec, StorageType, UseStorageOptions};
|
||||
use leptos::signal_prelude::*;
|
||||
|
||||
use_specific_storage!(
|
||||
/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Please refer to [`use_storage`]
|
||||
///
|
||||
/// ## See also
|
||||
///
|
||||
/// * [`use_storage`]
|
||||
/// * [`use_session_storage`]
|
||||
// #[doc(cfg(feature = "storage"))]
|
||||
local
|
||||
/// [`use_local_storage`]
|
||||
);
|
||||
/// Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
|
||||
///
|
||||
/// LocalStorage stores data in the browser with no expiration time. Access is given to all pages from the same origin (e.g., all pages from "https://example.com" share the same origin). While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored.
|
||||
///
|
||||
/// This is contrast to [`use_session_storage`] which clears data when the page session ends and is not shared.
|
||||
///
|
||||
/// ## Usage
|
||||
/// See [`use_storage`] for more details on how to use.
|
||||
pub fn use_local_storage<T, C>(
|
||||
key: impl AsRef<str>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + Default + PartialEq,
|
||||
C: Codec<T> + Default,
|
||||
{
|
||||
use_storage_with_options(
|
||||
StorageType::Local,
|
||||
key,
|
||||
UseStorageOptions::<T, C>::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details.
|
||||
pub fn use_local_storage_with_options<T, C>(
|
||||
key: impl AsRef<str>,
|
||||
options: UseStorageOptions<T, C>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: Codec<T>,
|
||||
{
|
||||
use_storage_with_options(StorageType::Local, key, options)
|
||||
}
|
||||
|
|
|
@ -1,22 +1,36 @@
|
|||
use crate::core::MaybeRwSignal;
|
||||
use crate::storage::shared::{use_specific_storage, UseSpecificStorageOptions};
|
||||
use crate::storage::{use_storage_with_options, StorageType};
|
||||
use leptos::*;
|
||||
use paste::paste;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::{use_storage, Codec, StorageType, UseStorageOptions};
|
||||
use leptos::signal_prelude::*;
|
||||
|
||||
use_specific_storage!(
|
||||
/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Please refer to [`use_storage`]
|
||||
///
|
||||
/// ## See also
|
||||
///
|
||||
/// * [`use_storage`]
|
||||
/// * [`use_local_storage`]
|
||||
// #[doc(cfg(feature = "storage"))]
|
||||
session
|
||||
/// [`use_session_storage`]
|
||||
);
|
||||
/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
|
||||
///
|
||||
/// SessionStorages stores data in the browser that is deleted when the page session ends. A page session ends when the browser closes the tab. Data is not shared between pages. While data doesn't expire the user can view, modify and delete all data stored. Browsers allow 5MB of data to be stored.
|
||||
///
|
||||
/// Use [`use_local_storage`] to store data that is shared amongst all pages with the same origin and persists between page sessions.
|
||||
///
|
||||
/// ## Usage
|
||||
/// See [`use_storage`] for more details on how to use.
|
||||
pub fn use_session_storage<T, C>(
|
||||
key: impl AsRef<str>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + Default + PartialEq,
|
||||
C: Codec<T> + Default,
|
||||
{
|
||||
use_storage_with_options(
|
||||
StorageType::Session,
|
||||
key,
|
||||
UseStorageOptions::<T, C>::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details.
|
||||
pub fn use_session_storage_with_options<T, C>(
|
||||
key: impl AsRef<str>,
|
||||
options: UseStorageOptions<T, C>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: Codec<T>,
|
||||
{
|
||||
use_storage_with_options(StorageType::Session, key, options)
|
||||
}
|
||||
|
|
|
@ -1,26 +1,18 @@
|
|||
#![cfg_attr(feature = "ssr", allow(unused_variables, unused_imports, dead_code))]
|
||||
|
||||
use crate::core::MaybeRwSignal;
|
||||
use crate::utils::FilterOptions;
|
||||
use crate::{
|
||||
filter_builder_methods, use_event_listener, watch_pausable_with_options, DebounceOptions,
|
||||
ThrottleOptions, WatchOptions, WatchPausableReturn,
|
||||
core::{MaybeRwSignal, StorageType},
|
||||
use_event_listener, use_window,
|
||||
utils::FilterOptions,
|
||||
watch_with_options, WatchOptions,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use js_sys::Reflect;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Error;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
pub use crate::core::StorageType;
|
||||
const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
||||
|
||||
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).
|
||||
/// Reactive [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage).
|
||||
///
|
||||
/// ## Demo
|
||||
///
|
||||
|
@ -28,471 +20,383 @@ const CUSTOM_STORAGE_EVENT_NAME: &str = "leptos-use-storage";
|
|||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// It returns a triplet `(read_signal, write_signal, delete_from_storage_func)` of type `(ReadSignal<T>, WriteSignal<T>, Fn())`.
|
||||
/// Pass a [`StorageType`] to determine the kind of key-value browser storage to use. The specified key is where data is stored. All values are stored as UTF-16 strings which is then encoded and decoded via the given [`Codec`]. This value is synced with other calls using the same key on the smae page and across tabs for local storage. See [`UseStorageOptions`] to see how behaviour can be further customised.
|
||||
///
|
||||
/// Values are (de-)serialized to/from JSON using [`serde`](https://serde.rs/).
|
||||
/// See [`Codec`] for more details on how to handle versioning--dealing with data that can outlast your code.
|
||||
///
|
||||
/// Returns a triplet `(read_signal, write_signal, delete_from_storage_fn)`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_storage, use_storage_with_options, UseStorageOptions};
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions, StringCodec, JsonCodec, ProstCodec};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// #
|
||||
/// #[derive(Serialize, Deserialize, Clone)]
|
||||
/// pub struct MyState {
|
||||
/// pub hello: String,
|
||||
/// pub greeting: String,
|
||||
/// }
|
||||
///
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// // bind struct. Must be serializable.
|
||||
/// let (state, set_state, _) = use_storage(
|
||||
/// "my-state",
|
||||
/// MyState {
|
||||
/// hello: "hi".to_string(),
|
||||
/// greeting: "Hello".to_string()
|
||||
/// },
|
||||
/// ); // returns Signal<MyState>
|
||||
/// // Binds a struct:
|
||||
/// let (state, set_state, _) = use_local_storage::<MyState, JsonCodec>("my-state");
|
||||
///
|
||||
/// // bind bool.
|
||||
/// let (flag, set_flag, remove_flag) = use_storage("my-flag", true); // returns Signal<bool>
|
||||
/// // Binds a bool, stored as a string:
|
||||
/// let (flag, set_flag, remove_flag) = use_session_storage::<bool, StringCodec>("my-flag");
|
||||
///
|
||||
/// // bind number
|
||||
/// let (count, set_count, _) = use_storage("my-count", 0); // returns Signal<i32>
|
||||
/// // Binds a number, stored as a string:
|
||||
/// let (count, set_count, _) = use_session_storage::<i32, StringCodec>("my-count");
|
||||
/// // Binds a number, stored in JSON:
|
||||
/// let (count, set_count, _) = use_session_storage::<i32, JsonCodec>("my-count-kept-in-js");
|
||||
///
|
||||
/// // bind string with SessionStorage
|
||||
/// let (id, set_id, _) = use_storage_with_options(
|
||||
/// // Bind string with SessionStorage stored in ProtoBuf format:
|
||||
/// let (id, set_id, _) = use_storage_with_options::<String, ProstCodec>(
|
||||
/// StorageType::Session,
|
||||
/// "my-id",
|
||||
/// "some_string_id".to_string(),
|
||||
/// UseStorageOptions::default().storage_type(StorageType::Session),
|
||||
/// UseStorageOptions::default(),
|
||||
/// );
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## 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("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(
|
||||
/// "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)]
|
||||
/// // Data stored in JSON must implement Serialize, Deserialize:
|
||||
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
/// pub struct MyState {
|
||||
/// pub hello: String,
|
||||
/// pub greeting: String,
|
||||
/// }
|
||||
/// #
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// let (state, set_state, _) = use_storage_with_options(
|
||||
/// "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))
|
||||
///
|
||||
/// // Default can be used to implement intial or deleted values.
|
||||
/// // You can also use a signal via UseStorageOptions::default_value`
|
||||
/// impl Default for MyState {
|
||||
/// fn default() -> Self {
|
||||
/// Self {
|
||||
/// hello: "hi".to_string(),
|
||||
/// greeting: "Hello".to_string()
|
||||
/// }
|
||||
/// }),
|
||||
/// );
|
||||
/// #
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Filter Storage Write
|
||||
///
|
||||
/// You can specify `debounce` or `throttle` options for limiting writes to storage.
|
||||
///
|
||||
/// ## Server-Side Rendering
|
||||
///
|
||||
/// On the server this falls back to a `create_signal(default)` and an empty remove function.
|
||||
///
|
||||
/// ## See also
|
||||
///
|
||||
/// * [`use_local_storage`]
|
||||
/// * [`use_session_storage`]
|
||||
// #[doc(cfg(feature = "storage"))]
|
||||
pub fn use_storage<T, D>(key: &str, defaults: D) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
|
||||
D: Into<MaybeRwSignal<T>>,
|
||||
T: Clone,
|
||||
{
|
||||
use_storage_with_options(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>(
|
||||
key: &str,
|
||||
defaults: D,
|
||||
options: UseStorageOptions<T>,
|
||||
#[inline(always)]
|
||||
pub fn use_storage<T, C>(
|
||||
storage_type: StorageType,
|
||||
key: impl AsRef<str>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
for<'de> T: Serialize + Deserialize<'de> + Clone + 'static,
|
||||
D: Into<MaybeRwSignal<T>>,
|
||||
T: Clone,
|
||||
T: Clone + PartialEq,
|
||||
C: Codec<T>,
|
||||
{
|
||||
let defaults = defaults.into();
|
||||
use_storage_with_options(storage_type, key, UseStorageOptions::default())
|
||||
}
|
||||
|
||||
/// Version of [`use_storage`] that accepts [`UseStorageOptions`].
|
||||
pub fn use_storage_with_options<T, C>(
|
||||
storage_type: StorageType,
|
||||
key: impl AsRef<str>,
|
||||
options: UseStorageOptions<T, C>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: Codec<T>,
|
||||
{
|
||||
let UseStorageOptions {
|
||||
storage_type,
|
||||
listen_to_storage_changes,
|
||||
write_defaults,
|
||||
merge_defaults,
|
||||
codec,
|
||||
on_error,
|
||||
listen_to_storage_changes,
|
||||
initial_value,
|
||||
filter,
|
||||
} = options;
|
||||
|
||||
let (data, set_data) = defaults.into_signal();
|
||||
|
||||
let raw_init = data.get_untracked();
|
||||
let (data, set_data) = initial_value.into_signal();
|
||||
let default = data.get_untracked();
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
let remove: Rc<dyn Fn()> = Rc::new(|| {});
|
||||
let remove = move || {
|
||||
set_data.set(default.clone());
|
||||
};
|
||||
(data.into(), set_data, remove)
|
||||
} else {
|
||||
let storage = storage_type.into_storage();
|
||||
// Get storage API
|
||||
let storage = storage_type
|
||||
.into_storage()
|
||||
.map_err(UseStorageError::StorageNotAvailable)
|
||||
.and_then(|s| s.ok_or(UseStorageError::StorageReturnedNone));
|
||||
let storage = handle_error(&on_error, storage);
|
||||
|
||||
let remove: Rc<dyn Fn()> = match storage {
|
||||
Ok(Some(storage)) => {
|
||||
let write = {
|
||||
let on_error = on_error.clone();
|
||||
let storage = storage.clone();
|
||||
let key = key.to_string();
|
||||
|
||||
Rc::new(move |v: &T| {
|
||||
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(),
|
||||
);
|
||||
|
||||
// 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) => {
|
||||
on_error.clone()(UseStorageError::SerializationError(e));
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let read = {
|
||||
let storage = storage.clone();
|
||||
let on_error = on_error.clone();
|
||||
let key = key.to_string();
|
||||
let raw_init = raw_init.clone();
|
||||
|
||||
Rc::new(
|
||||
move |event_detail: Option<StorageEventDetail>| -> Option<T> {
|
||||
let serialized_init = match serde_json::to_string(&raw_init) {
|
||||
Ok(serialized) => Some(serialized),
|
||||
Err(e) => {
|
||||
on_error.clone()(UseStorageError::DefaultSerializationError(e));
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let raw_value = if let Some(event_detail) = event_detail {
|
||||
event_detail.new_value
|
||||
} else {
|
||||
match storage.get_item(&key) {
|
||||
Ok(raw_value) => match raw_value {
|
||||
Some(raw_value) => Some(merge_defaults(&raw_value, &raw_init)),
|
||||
None => serialized_init.clone(),
|
||||
},
|
||||
Err(e) => {
|
||||
on_error.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_error.clone()(UseStorageError::SerializationError(e));
|
||||
None
|
||||
}
|
||||
},
|
||||
None => {
|
||||
if let Some(serialized_init) = &serialized_init {
|
||||
if write_defaults {
|
||||
if let Err(e) = storage.set_item(&key, serialized_init) {
|
||||
on_error(UseStorageError::StorageAccessError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(raw_init.clone())
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let WatchPausableReturn {
|
||||
pause: pause_watch,
|
||||
resume: resume_watch,
|
||||
..
|
||||
} = watch_pausable_with_options(
|
||||
move || data.get(),
|
||||
move |data, _, _| Rc::clone(&write)(data),
|
||||
WatchOptions::default().filter(filter),
|
||||
);
|
||||
|
||||
let update = {
|
||||
let key = key.to_string();
|
||||
let storage = storage.clone();
|
||||
let raw_init = raw_init.clone();
|
||||
|
||||
Rc::new(move |event_detail: Option<StorageEventDetail>| {
|
||||
if let Some(event_detail) = &event_detail {
|
||||
if event_detail.storage_area != Some(storage.clone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
match &event_detail.key {
|
||||
None => {
|
||||
set_data.set(raw_init.clone());
|
||||
return;
|
||||
}
|
||||
Some(event_key) => {
|
||||
if event_key != &key {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pause_watch();
|
||||
|
||||
if let Some(value) = read(event_detail.clone()) {
|
||||
set_data.set(value);
|
||||
}
|
||||
|
||||
if event_detail.is_some() {
|
||||
// use timeout to avoid infinite loop
|
||||
let resume = resume_watch.clone();
|
||||
let _ = set_timeout_with_handle(resume, Duration::ZERO);
|
||||
} else {
|
||||
resume_watch();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let update_from_custom_event = {
|
||||
let update = Rc::clone(&update);
|
||||
|
||||
move |event: web_sys::CustomEvent| {
|
||||
let update = Rc::clone(&update);
|
||||
queue_microtask(move || update(Some(event.into())))
|
||||
}
|
||||
};
|
||||
|
||||
let update_from_storage_event = {
|
||||
let update = Rc::clone(&update);
|
||||
|
||||
move |event: web_sys::StorageEvent| update(Some(event.into()))
|
||||
};
|
||||
|
||||
if listen_to_storage_changes {
|
||||
let _ = use_event_listener(window(), ev::storage, update_from_storage_event);
|
||||
let _ = use_event_listener(
|
||||
window(),
|
||||
ev::Custom::new(CUSTOM_STORAGE_EVENT_NAME),
|
||||
update_from_custom_event,
|
||||
);
|
||||
}
|
||||
|
||||
update(None);
|
||||
|
||||
let k = key.to_string();
|
||||
|
||||
Rc::new(move || {
|
||||
let _ = storage.remove_item(&k);
|
||||
// Schedules a storage event microtask. Uses a queue to avoid re-entering the runtime
|
||||
let dispatch_storage_event = {
|
||||
let key = key.as_ref().to_owned();
|
||||
let on_error = on_error.to_owned();
|
||||
move || {
|
||||
let key = key.to_owned();
|
||||
let on_error = on_error.to_owned();
|
||||
queue_microtask(move || {
|
||||
// Note: we cannot construct a full StorageEvent so we _must_ rely on a custom event
|
||||
let mut custom = web_sys::CustomEventInit::new();
|
||||
custom.detail(&JsValue::from_str(&key));
|
||||
let result = window()
|
||||
.dispatch_event(
|
||||
&web_sys::CustomEvent::new_with_event_init_dict(
|
||||
INTERNAL_STORAGE_EVENT,
|
||||
&custom,
|
||||
)
|
||||
.expect("failed to create custom storage event"),
|
||||
)
|
||||
.map_err(UseStorageError::NotifyItemChangedFailed);
|
||||
let _ = handle_error(&on_error, result);
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
on_error(UseStorageError::NoStorage(e));
|
||||
Rc::new(move || {})
|
||||
}
|
||||
_ => {
|
||||
// do nothing
|
||||
Rc::new(move || {})
|
||||
};
|
||||
|
||||
// Fetches direct from browser storage and fills set_data if changed (memo)
|
||||
let fetch_from_storage = {
|
||||
let storage = storage.to_owned();
|
||||
let codec = codec.to_owned();
|
||||
let key = key.as_ref().to_owned();
|
||||
let on_error = on_error.to_owned();
|
||||
move || {
|
||||
let fetched = storage
|
||||
.to_owned()
|
||||
.and_then(|storage| {
|
||||
// Get directly from storage
|
||||
let result = storage
|
||||
.get_item(&key)
|
||||
.map_err(UseStorageError::GetItemFailed);
|
||||
handle_error(&on_error, result)
|
||||
})
|
||||
.unwrap_or_default() // Drop handled Err(())
|
||||
.map(|encoded| {
|
||||
// Decode item
|
||||
let result = codec
|
||||
.decode(encoded)
|
||||
.map_err(UseStorageError::ItemCodecError);
|
||||
handle_error(&on_error, result)
|
||||
})
|
||||
.transpose()
|
||||
.unwrap_or_default(); // Drop handled Err(())
|
||||
|
||||
match fetched {
|
||||
Some(value) => {
|
||||
// Replace data if changed
|
||||
if value != data.get_untracked() {
|
||||
set_data.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Revert to default
|
||||
None => set_data.set(default.clone()),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch initial value
|
||||
fetch_from_storage();
|
||||
|
||||
// Fires when storage needs to be fetched
|
||||
let notify = create_trigger();
|
||||
|
||||
// Refetch from storage. Keeps track of how many times we've been notified. Does not increment for calls to set_data
|
||||
let notify_id = create_memo::<usize>(move |prev| {
|
||||
notify.track();
|
||||
match prev {
|
||||
None => 1, // Avoid async fetch of initial value
|
||||
Some(prev) => {
|
||||
fetch_from_storage();
|
||||
prev + 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set item on internal (non-event) page changes to the data signal
|
||||
{
|
||||
let storage = storage.to_owned();
|
||||
let codec = codec.to_owned();
|
||||
let key = key.as_ref().to_owned();
|
||||
let on_error = on_error.to_owned();
|
||||
let dispatch_storage_event = dispatch_storage_event.to_owned();
|
||||
let _ = watch_with_options(
|
||||
move || (notify_id.get(), data.get()),
|
||||
move |(id, value), prev, _| {
|
||||
// Skip setting storage on changes from external events. The ID will change on external events.
|
||||
if prev.map(|(prev_id, _)| *prev_id != *id).unwrap_or_default() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(storage) = &storage {
|
||||
// Encode value
|
||||
let result = codec
|
||||
.encode(value)
|
||||
.map_err(UseStorageError::ItemCodecError)
|
||||
.and_then(|enc_value| {
|
||||
// Set storage -- sends a global event
|
||||
storage
|
||||
.set_item(&key, &enc_value)
|
||||
.map_err(UseStorageError::SetItemFailed)
|
||||
});
|
||||
let result = handle_error(&on_error, result);
|
||||
// Send internal storage event
|
||||
if result.is_ok() {
|
||||
dispatch_storage_event();
|
||||
}
|
||||
}
|
||||
},
|
||||
WatchOptions::default().filter(filter),
|
||||
);
|
||||
}
|
||||
|
||||
if listen_to_storage_changes {
|
||||
let check_key = key.as_ref().to_owned();
|
||||
// Listen to global storage events
|
||||
let _ = use_event_listener(use_window(), leptos::ev::storage, move |ev| {
|
||||
let ev_key = ev.key();
|
||||
// Key matches or all keys deleted (None)
|
||||
if ev_key == Some(check_key.clone()) || ev_key.is_none() {
|
||||
notify.notify()
|
||||
}
|
||||
});
|
||||
// Listen to internal storage events
|
||||
let check_key = key.as_ref().to_owned();
|
||||
let _ = use_event_listener(
|
||||
use_window(),
|
||||
ev::Custom::new(INTERNAL_STORAGE_EVENT),
|
||||
move |ev: web_sys::CustomEvent| {
|
||||
if Some(check_key.clone()) == ev.detail().as_string() {
|
||||
notify.notify()
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Remove from storage fn
|
||||
let remove = {
|
||||
let key = key.as_ref().to_owned();
|
||||
move || {
|
||||
let _ = storage.as_ref().map(|storage| {
|
||||
// Delete directly from storage
|
||||
let result = storage
|
||||
.remove_item(&key)
|
||||
.map_err(UseStorageError::RemoveItemFailed);
|
||||
let _ = handle_error(&on_error, result);
|
||||
notify.notify();
|
||||
dispatch_storage_event();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
(data, set_data, remove)
|
||||
}}
|
||||
|
||||
(data, set_data, move || 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>,
|
||||
/// Session handling errors returned by [`use_storage_with_options`].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UseStorageError<Err> {
|
||||
#[error("storage not available")]
|
||||
StorageNotAvailable(JsValue),
|
||||
#[error("storage not returned from window")]
|
||||
StorageReturnedNone,
|
||||
#[error("failed to get item")]
|
||||
GetItemFailed(JsValue),
|
||||
#[error("failed to set item")]
|
||||
SetItemFailed(JsValue),
|
||||
#[error("failed to delete item")]
|
||||
RemoveItemFailed(JsValue),
|
||||
#[error("failed to notify item changed")]
|
||||
NotifyItemChangedFailed(JsValue),
|
||||
#[error("failed to encode / decode item value")]
|
||||
ItemCodecError(Err),
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
/// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`].
|
||||
pub struct UseStorageOptions<T: 'static, C: Codec<T>> {
|
||||
// Translates to and from UTF-16 strings
|
||||
codec: C,
|
||||
// Callback for when an error occurs
|
||||
on_error: Rc<dyn Fn(UseStorageError<C::Error>)>,
|
||||
// Whether to continuously listen to changes from browser storage
|
||||
listen_to_storage_changes: bool,
|
||||
// Initial value to use when the storage key is not set
|
||||
initial_value: MaybeRwSignal<T>,
|
||||
// Debounce or throttle the writing to storage whenever the value changes
|
||||
filter: FilterOptions,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
/// A codec for encoding and decoding values to and from UTF-16 strings. These strings are intended to be stored in browser storage.
|
||||
///
|
||||
/// ## Versioning
|
||||
///
|
||||
/// Versioning is the process of handling long-term data that can outlive our code.
|
||||
///
|
||||
/// For example we could have a settings struct whose members change over time. We might eventually add timezone support and we might then remove support for a thousand separator on numbers. Each change results in a new possible version of the stored data. If we stored these settings in browser storage we would need to handle all possible versions of the data format that can occur. If we don't offer versioning then all settings could revert to the default every time we encounter an old format.
|
||||
///
|
||||
/// How best to handle versioning depends on the codec involved:
|
||||
///
|
||||
/// - The [`StringCodec`](super::StringCodec) can avoid versioning entirely by keeping to privimitive types. In our example above, we could have decomposed the settings struct into separate timezone and number separator fields. These would be encoded as strings and stored as two separate key-value fields in the browser rather than a single field. If a field is missing then the value intentionally would fallback to the default without interfering with the other field.
|
||||
///
|
||||
/// - The [`ProstCodec`](super::ProstCodec) uses [Protocol buffers](https://protobuf.dev/overview/) designed to solve the problem of long-term storage. It provides semantics for versioning that are not present in JSON or other formats.
|
||||
///
|
||||
/// - The [`JsonCodec`](super::JsonCodec) stores data as JSON. We can then rely on serde or by providing our own manual version handling. See the codec for more details.
|
||||
pub trait Codec<T>: Clone + 'static {
|
||||
/// The error type returned when encoding or decoding fails.
|
||||
type Error;
|
||||
/// Encodes a value to a UTF-16 string.
|
||||
fn encode(&self, val: &T) -> Result<String, Self::Error>;
|
||||
/// Decodes a UTF-16 string to a value. Should be able to decode any string encoded by [`encode`].
|
||||
fn decode(&self, str: String) -> Result<T, Self::Error>;
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling.
|
||||
fn handle_error<T, Err>(
|
||||
on_error: &Rc<dyn Fn(UseStorageError<Err>)>,
|
||||
result: Result<T, UseStorageError<Err>>,
|
||||
) -> Result<T, ()> {
|
||||
result.map_err(|err| (on_error)(err))
|
||||
}
|
||||
|
||||
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)`
|
||||
pub(crate) storage_type: StorageType,
|
||||
/// Listen to changes to this storage key from somewhere else. Defaults to true.
|
||||
pub(crate) listen_to_storage_changes: bool,
|
||||
/// If no value for the give key is found in the storage, write it. Defaults to true.
|
||||
pub(crate) 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.
|
||||
pub(crate) merge_defaults: fn(&str, &T) -> String,
|
||||
/// Optional callback whenever an error occurs. The callback takes an argument of type [`UseStorageError`].
|
||||
pub(crate) on_error: Rc<dyn Fn(UseStorageError)>,
|
||||
|
||||
/// Debounce or throttle the writing to storage whenever the value changes.
|
||||
pub(crate) filter: FilterOptions,
|
||||
}
|
||||
|
||||
impl<T> Default for UseStorageOptions<T> {
|
||||
impl<T: Default, C: Codec<T> + Default> Default for UseStorageOptions<T, C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
storage_type: Default::default(),
|
||||
codec: C::default(),
|
||||
on_error: Rc::new(|_err| ()),
|
||||
listen_to_storage_changes: true,
|
||||
write_defaults: true,
|
||||
merge_defaults: |stored_value, _default_value| stored_value.to_string(),
|
||||
on_error: Rc::new(|_| ()),
|
||||
filter: Default::default(),
|
||||
initial_value: MaybeRwSignal::default(),
|
||||
filter: FilterOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UseStorageOptions<T> {
|
||||
filter_builder_methods!(
|
||||
/// the serializing and storing into storage
|
||||
filter
|
||||
);
|
||||
impl<T: Default, C: Codec<T>> UseStorageOptions<T, C> {
|
||||
/// Sets the codec to use for encoding and decoding values to and from UTF-16 strings.
|
||||
pub fn codec(self, codec: impl Into<C>) -> Self {
|
||||
Self {
|
||||
codec: codec.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional callback whenever an error occurs.
|
||||
pub fn on_error(self, on_error: impl Fn(UseStorageError<C::Error>) + 'static) -> Self {
|
||||
Self {
|
||||
on_error: Rc::new(on_error),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen to changes to this storage key from browser and page events. Defaults to true.
|
||||
pub fn listen_to_storage_changes(self, listen_to_storage_changes: bool) -> Self {
|
||||
Self {
|
||||
listen_to_storage_changes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Initial value to use when the storage key is not set. Note that this value is read once on creation of the storage hook and not updated again. Accepts a signal and defaults to `T::default()`.
|
||||
pub fn initial_value(self, initial: impl Into<MaybeRwSignal<T>>) -> Self {
|
||||
Self {
|
||||
initial_value: initial.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Debounce or throttle the writing to storage whenever the value changes.
|
||||
pub fn filter(self, filter: impl Into<FilterOptions>) -> Self {
|
||||
Self {
|
||||
filter: filter.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
use crate::core::{ElementMaybeSignal, MaybeRwSignal};
|
||||
#[cfg(feature = "storage")]
|
||||
use crate::storage::{use_storage_with_options, UseStorageOptions};
|
||||
#[cfg(feature = "storage")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::storage::{use_storage, StringCodec, UseStorageOptions};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::core::StorageType;
|
||||
use crate::use_preferred_dark;
|
||||
use cfg_if::cfg_if;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::*;
|
||||
use std::marker::PhantomData;
|
||||
|
@ -16,9 +13,6 @@ use wasm_bindgen::JsCast;
|
|||
|
||||
/// Reactive color mode (dark / light / customs) with auto data persistence.
|
||||
///
|
||||
/// > Data persistence is only enabled when the crate feature **`storage`** is enabled. You
|
||||
/// can use the function without it but the mode won't be persisted.
|
||||
///
|
||||
/// ## Demo
|
||||
///
|
||||
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_color_mode)
|
||||
|
@ -58,7 +52,7 @@ use wasm_bindgen::JsCast;
|
|||
/// #
|
||||
/// mode.get(); // ColorMode::Dark or ColorMode::Light
|
||||
///
|
||||
/// set_mode.set(ColorMode::Dark); // change to dark mode and persist (with feature `storage`)
|
||||
/// set_mode.set(ColorMode::Dark); // change to dark mode and persist
|
||||
///
|
||||
/// set_mode.set(ColorMode::Auto); // change to auto mode
|
||||
/// #
|
||||
|
@ -255,49 +249,30 @@ pub enum ColorMode {
|
|||
Custom(String),
|
||||
}
|
||||
|
||||
cfg_if! { if #[cfg(feature = "storage")] {
|
||||
fn get_store_signal(
|
||||
initial_value: MaybeRwSignal<ColorMode>,
|
||||
storage_signal: Option<RwSignal<ColorMode>>,
|
||||
storage_key: &str,
|
||||
storage_enabled: bool,
|
||||
storage: StorageType,
|
||||
listen_to_storage_changes: bool,
|
||||
) -> (Signal<ColorMode>, WriteSignal<ColorMode>) {
|
||||
if let Some(storage_signal) = storage_signal {
|
||||
let (store, set_store) = storage_signal.split();
|
||||
(store.into(), set_store)
|
||||
} else if storage_enabled {
|
||||
let (store, set_store, _) = use_storage_with_options(
|
||||
storage_key,
|
||||
initial_value,
|
||||
UseStorageOptions::default()
|
||||
.listen_to_storage_changes(listen_to_storage_changes)
|
||||
.storage_type(storage),
|
||||
);
|
||||
|
||||
(store, set_store)
|
||||
} else {
|
||||
initial_value.into_signal()
|
||||
}
|
||||
fn get_store_signal(
|
||||
initial_value: MaybeRwSignal<ColorMode>,
|
||||
storage_signal: Option<RwSignal<ColorMode>>,
|
||||
storage_key: &str,
|
||||
storage_enabled: bool,
|
||||
storage: StorageType,
|
||||
listen_to_storage_changes: bool,
|
||||
) -> (Signal<ColorMode>, WriteSignal<ColorMode>) {
|
||||
if let Some(storage_signal) = storage_signal {
|
||||
let (store, set_store) = storage_signal.split();
|
||||
(store.into(), set_store)
|
||||
} else if storage_enabled {
|
||||
let (store, set_store, _) = use_storage_with_options::<ColorMode, StringCodec>(
|
||||
storage,
|
||||
storage_key,
|
||||
UseStorageOptions::default()
|
||||
.listen_to_storage_changes(listen_to_storage_changes)
|
||||
.initial_value(initial_value),
|
||||
);
|
||||
(store, set_store)
|
||||
} else {
|
||||
initial_value.into_signal()
|
||||
}
|
||||
} else {
|
||||
fn get_store_signal(
|
||||
initial_value: MaybeRwSignal<ColorMode>,
|
||||
storage_signal: Option<RwSignal<ColorMode>>,
|
||||
_storage_key: &str,
|
||||
_storage_enabled: bool,
|
||||
_storage: StorageType,
|
||||
_listen_to_storage_changes: bool,
|
||||
) -> (Signal<ColorMode>, WriteSignal<ColorMode>) {
|
||||
if let Some(storage_signal) = storage_signal {
|
||||
let (store, set_store) = storage_signal.split();
|
||||
(store.into(), set_store)
|
||||
} else {
|
||||
initial_value.into_signal()
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
impl Display for ColorMode {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
|
@ -330,6 +305,14 @@ impl From<String> for ColorMode {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromStr for ColorMode {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(ColorMode::from(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DefaultBuilder)]
|
||||
pub struct UseColorModeOptions<El, T>
|
||||
where
|
||||
|
@ -378,7 +361,7 @@ where
|
|||
|
||||
/// If the color mode should be persisted. If `true` this required the
|
||||
/// *create feature* **`storage`** to be enabled.
|
||||
/// Defaults to `true` and is forced to `false` if the feature **`storage`** is not enabled.
|
||||
/// Defaults to `true`.
|
||||
storage_enabled: bool,
|
||||
|
||||
/// Emit `auto` mode from state
|
||||
|
|
Loading…
Add table
Reference in a new issue