mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-02-02 10:54:15 -05:00
Merge branch 'main' into leptos-0.7
# Conflicts: # CHANGELOG.md # Cargo.toml # src/is_err.rs # src/is_none.rs # src/is_ok.rs # src/is_some.rs # src/storage/use_local_storage.rs # src/storage/use_session_storage.rs # src/storage/use_storage.rs # src/use_broadcast_channel.rs # src/use_cookie.rs # src/use_device_pixel_ratio.rs # src/use_event_source.rs # src/use_to_string.rs # src/use_websocket.rs # src/utils/codecs/string/from_to_string.rs # src/utils/codecs/string/json.rs # src/utils/codecs/string/prost.rs
This commit is contained in:
commit
ec6027c59e
56 changed files with 1585 additions and 831 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [Synphonyte]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: [Synphonyte] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -30,13 +30,13 @@ jobs:
|
|||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings
|
||||
run: cargo clippy --features docs,math --tests -- -D warnings
|
||||
- name: Run tests (general)
|
||||
run: cargo test --features math,docs,ssr,prost,serde
|
||||
run: cargo test --features math,docs,ssr
|
||||
- name: Run tests (axum)
|
||||
run: cargo test --features math,docs,ssr,prost,serde,axum --doc use_cookie::use_cookie
|
||||
run: cargo test --features math,docs,ssr,axum --doc use_cookie::use_cookie
|
||||
- name: Run tests (actix)
|
||||
run: cargo test --features math,docs,ssr,prost,serde,actix --doc use_cookie::use_cookie
|
||||
run: cargo test --features math,docs,ssr,actix --doc use_cookie::use_cookie
|
||||
|
||||
#### mdbook
|
||||
- name: Install mdbook I
|
||||
|
|
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
|
@ -23,10 +23,10 @@ jobs:
|
|||
- name: Check formatting
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --features prost,serde,docs,math --tests -- -D warnings
|
||||
run: cargo clippy --features docs,math --tests -- -D warnings
|
||||
- name: Run tests (general)
|
||||
run: cargo test --features math,docs,ssr,prost,serde
|
||||
run: cargo test --features math,docs,ssr
|
||||
- name: Run tests (axum)
|
||||
run: cargo test --features math,docs,ssr,prost,serde,axum --doc use_cookie::use_cookie
|
||||
run: cargo test --features math,docs,ssr,axum --doc use_cookie::use_cookie
|
||||
- name: Run tests (actix)
|
||||
run: cargo test --features math,docs,ssr,prost,serde,actix --doc use_cookie::use_cookie
|
||||
run: cargo test --features math,docs,ssr,actix --doc use_cookie::use_cookie
|
||||
|
|
1
.idea/leptos-use.iml
generated
1
.idea/leptos-use.iml
generated
|
@ -76,6 +76,7 @@
|
|||
<sourceFolder url="file://$MODULE_DIR$/examples/use_not/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples/use_or/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples/use_event_source/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples/sync_signal/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/examples/use_event_listener/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/docs/book/book" />
|
||||
|
|
81
CHANGELOG.md
81
CHANGELOG.md
|
@ -9,6 +9,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- Updated to Leptos 0.7
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### New Functions 🚀
|
||||
|
||||
- `use_user_media`
|
||||
|
||||
### New Features 🚀
|
||||
|
||||
- Codecs:
|
||||
- All codecs now live in their own crate `codee`
|
||||
- There are now binary codecs in addition to string codecs.
|
||||
- `FromToBytesCodec`
|
||||
- `WebpackSerdeCodec`
|
||||
- `BincodeSerdeCodec`
|
||||
- `ProstCodec` (see also the section "Breaking Changes 🛠" below)
|
||||
- Every binary codec can be used as a string codec with the `Base64` wrapper which encodes the binary data as a
|
||||
base64
|
||||
string.
|
||||
- This required feature `base64`
|
||||
- It can be wrapped for example like this: `Base64<WebpackSerdeCodec>`.
|
||||
- There is now an `OptionCodec` wrapper that allows to wrap any string codec that encodes `T` to encode `Option<T>`.
|
||||
- Use it like this: `OptionCodec<FromToStringCodec<f64>>`.
|
||||
|
||||
- `ElementMaybeSignal` is now implemented for `websys::HtmlElement` (thanks to @blorbb).
|
||||
- `UseStorageOptions` now has `delay_during_hydration` which has to be used when you conditionally show parts of
|
||||
the DOM controlled by a value from storage. This leads to hydration errors which can be fixed by setting this new
|
||||
option to `true`.
|
||||
- `cookie::SameSite` is now re-exported
|
||||
- Changing the signal returned by `use_cookie` now tries and changes the headers during SSR.
|
||||
- New book chapter about codecs
|
||||
- The macro `use_derive_signal!` is now exported (thanks to @mscofield0).
|
||||
|
||||
### Breaking Changes 🛠
|
||||
|
||||
- `UseStorageOptions` and `UseEventSourceOptions` no longer accept a `codec` value because this is already provided as a
|
||||
generic parameter to the respective function calls.
|
||||
- Codecs have been refactored. There are now two traits that codecs implement: `Encoder` and `Decoder`. The
|
||||
trait `StringCodec` is gone. The methods are now associated methods and their params now always take references.
|
||||
- `JsonCodec` has been renamed to `JsonSerdeCodec`.
|
||||
- The feature to enable this codec is now called `json_serde` instead of just `serde`.
|
||||
- `ProstCodec` now encodes as binary data. If you want to keep using it with string data you can wrap it like
|
||||
this: `Base64<ProstCodec>`. You have to enable both features `prost` and `base64` for this.
|
||||
- All of these structs, traits and features now live in their own crate called `codee`
|
||||
- `use_websocket`:
|
||||
- `UseWebsocketOptions` has been renamed to `UseWebSocketOptions` (uppercase S) to be consistent with the return
|
||||
type.
|
||||
- `UseWebSocketOptions::reconnect_limit` and `UseEventSourceOptions::reconnect_limit` is now `ReconnectLimit`
|
||||
instead
|
||||
of `u64`. Use `ReconnectLimit::Infinite` for infinite retries or `ReconnectLimit::Limited(...)` for limited
|
||||
retries.
|
||||
- `use_websocket` now uses codecs to send typed messages over the network.
|
||||
- When calling you have give type parameters for the message type and the
|
||||
codec: `use_websocket::<String, WebpackSerdeCodec>`
|
||||
- You can use binary or string codecs.
|
||||
- The `UseWebSocketReturn::send` closure now takes a `&T` which is encoded using the codec.
|
||||
- The `UseWebSocketReturn::message` signal now returns an `Option<T>` which is decoded using the codec.
|
||||
- `UseWebSocketReturn::send_bytes` and `UseWebSocketReturn::message_bytes` are gone.
|
||||
- `UseWebSocketOptions::on_message` and `UseWebSocketOptions::on_message_bytes` have been renamed
|
||||
to `on_message_raw` and `on_message_raw_bytes`.
|
||||
- The new `UseWebSocketOptions::on_message` takes a `&T`.
|
||||
- `UseWebSocketOptions::on_error` now takes a `UseWebSocketError` instead of a `web_sys::Event`.
|
||||
- `use_storage` now always saves the default value to storage if the key doesn't exist yet.
|
||||
|
||||
### Fixes 🍕
|
||||
|
||||
- Fixed auto-reconnect in `use_websocket`
|
||||
- Fixed typo in compiler error messages in `use_cookie` (thanks to @SleeplessOne1917).
|
||||
|
||||
## [0.10.10] - 2024-05-10
|
||||
|
||||
### Change 🔥
|
||||
|
||||
- Added compile-time warning when you use `ssr` feature with `wasm32`. You can enable `wasm_ssr` to remove the warning.
|
||||
|
||||
## [0.10.9] - 2024-04-27
|
||||
|
||||
### Fixes 🍕
|
||||
|
@ -117,7 +191,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- You can now convert `leptos::html::HtmlElement<T>` into `Element(s)MaybeSignal`. This should make functions a lot
|
||||
easier to use in directives.
|
||||
- There's now a chapter in the book especially for `Element(s)MaybeSignal`.
|
||||
- Throttled or debounced callbacks (in watch_* or *_fn) no longer are called after the containing scope was cleaned up.
|
||||
- Throttled or debounced callbacks (in watch\__ or _\_fn) no longer are called after the containing scope was cleaned
|
||||
up.
|
||||
- The document returned from `use_document` now supports the methods `query_selector` and `query_selector_all`.
|
||||
|
||||
## [0.9.0] - 2023-12-06
|
||||
|
@ -147,7 +222,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `use_scroll` now uses `try_get_untracked` in the debounced callback to avoid panics if the context has been destroyed
|
||||
while the callback was waiting to be called.
|
||||
- `use_idle` works properly now (no more idles too early).
|
||||
- `use_web_notification` doesn't panic on the server anymore.
|
||||
- `use_web_notification` doesn't panic on the server anymore.
|
||||
|
||||
## [0.8.2] - 2023-11-09
|
||||
|
||||
|
@ -221,7 +296,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- takes now a `&str` instead of a `String` as its `url` parameter.
|
||||
- same for the returned `send` method.
|
||||
- The `ready_state` return type is now renamed to `ConnectionReadyState` instead of `UseWebSocketReadyState`.
|
||||
- The returned signals `ready_state`, `message`, `message_bytes` have now the type
|
||||
- The returned signals `ready_state`, `message`, `message_bytes` have now the type
|
||||
`Signal<...>` instead of `ReadSignal<...>` to make them more consistent with other functions.
|
||||
- The options `reconnect_limit` and `reconnect_interval` now take a `u64` instead of `Option<u64>` to improve DX.
|
||||
- The option `manual` has been renamed to `immediate` to make it more consistent with other functions.
|
||||
|
|
19
Cargo.toml
19
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "leptos-use"
|
||||
version = "0.10.9"
|
||||
version = "0.10.10"
|
||||
edition = "2021"
|
||||
authors = ["Marc-Stefan Cassola"]
|
||||
categories = ["gui", "web-programming"]
|
||||
|
@ -15,8 +15,8 @@ homepage = "https://leptos-use.rs"
|
|||
[dependencies]
|
||||
actix-web = { version = "4", optional = true, default-features = false }
|
||||
async-trait = "0.1"
|
||||
base64 = { version = "0.21", optional = true }
|
||||
cfg-if = "1"
|
||||
codee = "0.1"
|
||||
cookie = { version = "0.18", features = ["percent-encode"] }
|
||||
default-struct-builder = "0.5"
|
||||
futures-util = "0.3"
|
||||
|
@ -32,11 +32,7 @@ leptos_actix = { version = "0.6", optional = true }
|
|||
leptos-spin = { version = "0.2", optional = true }
|
||||
num = { version = "0.4", optional = true }
|
||||
paste = "1"
|
||||
prost = { version = "0.12", optional = true }
|
||||
rmp-serde = { version = "1.1", optional = true }
|
||||
send_wrapper = "0.6.0"
|
||||
serde = { version = "1", optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
thiserror = "1"
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
@ -78,6 +74,7 @@ features = [
|
|||
"MediaDevices",
|
||||
"MediaQueryList",
|
||||
"MediaStream",
|
||||
"MediaStreamConstraints",
|
||||
"MediaStreamTrack",
|
||||
"MessageEvent",
|
||||
"MouseEvent",
|
||||
|
@ -136,17 +133,19 @@ features = [
|
|||
getrandom = { version = "0.2", features = ["js"] }
|
||||
leptos_meta = "0.7.0-alpha"
|
||||
rand = "0.8"
|
||||
codee = { version = "0.1", features = ["json_serde", "msgpack_serde", "base64", "prost"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
actix = ["dep:actix-web", "dep:leptos_actix", "dep:http0_2"]
|
||||
axum = ["dep:leptos_axum", "dep:http1"]
|
||||
docs = []
|
||||
math = ["num"]
|
||||
prost = ["base64", "dep:prost"]
|
||||
serde = ["dep:serde", "serde_json"]
|
||||
spin = ["dep:leptos-spin", "dep:http1"]
|
||||
ssr = []
|
||||
msgpack = ["dep:rmp-serde", "dep:serde"]
|
||||
wasm_ssr = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["math", "docs", "ssr", "prost", "serde"]
|
||||
features = ["math", "docs", "ssr"]
|
||||
rustdoc-args = ["--cfg=web_sys_unstable_apis"]
|
||||
rustc-args = ["--cfg=web_sys_unstable_apis"]
|
||||
|
|
17
build.rs
Normal file
17
build.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let ssr = env::var("CARGO_FEATURE_SSR").is_ok();
|
||||
let wasm_ssr = env::var("CARGO_FEATURE_WASM_SSR").is_ok();
|
||||
let wasm32 = env::var("CARGO_CFG_TARGET_ARCH").expect("should be present in the build script")
|
||||
== "wasm32";
|
||||
if ssr && wasm32 && !wasm_ssr {
|
||||
println!(
|
||||
"cargo::warning=You have enabled the `ssr` feature for a wasm32 target. \
|
||||
This is probably not what you want. Please check https://leptos-use.rs/server_side_rendering.html \
|
||||
for how to use the `ssr` feature correctly.\n \
|
||||
If you're building for wasm32 on the server you can enable the `wasm_ssr` feature to get rid of \
|
||||
this warning."
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
[Introduction](introduction.md)
|
||||
[Get Started](get_started.md)
|
||||
[Options](options.md)
|
||||
[Element Parameters](element_parameters.md)
|
||||
[Server-Side Rendering](server_side_rendering.md)
|
||||
[Encoding and Decoding Data](codecs.md)
|
||||
[Changelog](changelog.md)
|
||||
[Functions](functions.md)
|
||||
|
||||
|
@ -47,6 +49,7 @@
|
|||
- [use_preferred_contrast](browser/use_preferred_contrast.md)
|
||||
- [use_preferred_dark](browser/use_preferred_dark.md)
|
||||
- [use_service_worker](browser/use_service_worker.md)
|
||||
- [use_user_media](browser/use_user_media.md)
|
||||
- [use_web_notification](browser/use_web_notification.md)
|
||||
|
||||
# Sensors
|
||||
|
@ -65,6 +68,7 @@
|
|||
|
||||
- [use_event_source](network/use_event_source.md)
|
||||
- [use_websocket](network/use_websocket.md)
|
||||
|
||||
<!-- - [use_webtransport](network/use_webtransport.md) -->
|
||||
|
||||
# Animation
|
||||
|
@ -94,12 +98,14 @@
|
|||
- [use_sorted](iterable/use_sorted.md)
|
||||
|
||||
# Utilities
|
||||
|
||||
- [is_err](utilities/is_err.md)
|
||||
- [is_none](utilities/is_none.md)
|
||||
- [is_ok](utilities/is_ok.md)
|
||||
- [is_some](utilities/is_some.md)
|
||||
- [use_cycle_list](utilities/use_cycle_list.md)
|
||||
- [use_debounce_fn](utilities/use_debounce_fn.md)
|
||||
- [use_derive_signal!](utilities/use_derive_signal.md)
|
||||
- [use_supported](utilities/use_supported.md)
|
||||
- [use_throttle_fn](utilities/use_throttle_fn.md)
|
||||
- [use_to_string](utilities/use_to_string.md)
|
||||
|
|
3
docs/book/src/browser/use_user_media.md
Normal file
3
docs/book/src/browser/use_user_media.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# use_user_media
|
||||
|
||||
<!-- cmdrun python3 ../extract_doc_comment.py use_user_media -->
|
156
docs/book/src/codecs.md
Normal file
156
docs/book/src/codecs.md
Normal file
|
@ -0,0 +1,156 @@
|
|||
# Encoding and Decoding Data
|
||||
|
||||
Several functions encode and decode data for storing it and/or sending it over the network. To do this, codecs
|
||||
located at [`src/utils/codecs`](https://github.com/Synphonyte/leptos-use/tree/main/src/utils/codecs) are used. They
|
||||
implement the traits [`Encoder`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/mod.rs#L9) with the
|
||||
method `encode` and [`Decoder`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/mod.rs#L17) with the
|
||||
method `decode`.
|
||||
|
||||
There are two types of codecs: One that encodes as binary data (`Vec[u8]`) and another type that encodes as
|
||||
strings (`String`). There is also an adapter
|
||||
[`Base64`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/base64.rs) that can be used to
|
||||
wrap a binary codec and make it a string codec by representing the binary data as a base64 string.
|
||||
|
||||
## Available Codecs
|
||||
|
||||
### String Codecs
|
||||
|
||||
- [**`FromToStringCodec`
|
||||
**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/from_to_string.rs)
|
||||
- [**`JsonSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/json_serde.rs)**
|
||||
|
||||
### Binary Codecs
|
||||
|
||||
- [**`FromToBytesCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/from_to_bytes.rs)
|
||||
- [**`BincodeSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/bincode_serde.rs)
|
||||
- [**`MsgpackSerdeCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/msgpack_serde.rs)
|
||||
|
||||
### Adapters
|
||||
|
||||
- [**`Base64`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/base64.rs) —
|
||||
Wraps a binary codec and make it a string codec by representing the binary data as a base64 string.
|
||||
- [**`OptionCodec`**](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/option.rs) —
|
||||
Wraps a string codec that encodes `T` to create a codec that encodes `Option<T>`.
|
||||
|
||||
## Example
|
||||
|
||||
In this example, a codec is given to [`use_cookie`](browser/use_cookie.md) that stores data as a string in the JSON
|
||||
format. Since cookies can only store strings, we have to use string codecs here.
|
||||
|
||||
```rust,noplayground
|
||||
# use leptos::*;
|
||||
# use leptos_use::use_cookie;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
|
||||
# #[component]
|
||||
# pub fn App(cx: Scope) -> impl IntoView {
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct MyState {
|
||||
chicken_count: i32,
|
||||
egg_count: i32,
|
||||
}
|
||||
|
||||
let (cookie, set_cookie) = use_cookie::<MyState, JsonCodec>("my-state-cookie");
|
||||
# view! {}
|
||||
# }
|
||||
```
|
||||
|
||||
## Custom Codecs
|
||||
|
||||
If you don't find a suitable codecs for your needs, you can implement your own; it's straightforward! If you want to
|
||||
create a string codec, you can look
|
||||
at [`JsonSerdeCodec`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/json_serde.rs).
|
||||
In case it's a binary codec, have a look
|
||||
at [`BincodeSerdeCodec`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/bincode_serde.rs).
|
||||
|
||||
## 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 thousands separator for 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 `FromToStringCodec` can avoid versioning entirely by keeping
|
||||
to primitive 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 fall back to the default without interfering with the other
|
||||
field.
|
||||
|
||||
- The `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 codecs that use serde under the hood can rely on serde or by
|
||||
providing their own manual version handling. See the next sections for more details.
|
||||
|
||||
### Rely on `serde`
|
||||
|
||||
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).
|
||||
|
||||
### Manual Version Handling
|
||||
|
||||
We look at the example of the `JsonSerdeCodec` in this section.
|
||||
|
||||
To implement version handling, we parse the JSON generically then transform the
|
||||
resulting `JsValue` before decoding it into our struct again.
|
||||
|
||||
Let's look at an example.
|
||||
|
||||
```rust,noplayground
|
||||
# use leptos::*;
|
||||
# use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
|
||||
# use serde::{Deserialize, Serialize};
|
||||
# use serde_json::json;
|
||||
# use leptos_use::utils::{Encoder, Decoder};
|
||||
#
|
||||
# pub fn Demo() -> impl IntoView {
|
||||
#[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||
pub struct MyState {
|
||||
pub hello: String,
|
||||
// This field was added in a later version
|
||||
pub greeting: String,
|
||||
}
|
||||
|
||||
pub struct MyStateCodec;
|
||||
|
||||
impl Encoder<MyState> for MyStateCodec {
|
||||
type Error = serde_json::Error;
|
||||
type Encoded = String;
|
||||
|
||||
fn encode(val: &MyState) -> Result<Self::Encoded, Self::Error> {
|
||||
serde_json::to_string(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder<MyState> for MyStateCodec {
|
||||
type Error = serde_json::Error;
|
||||
type Encoded = str;
|
||||
|
||||
fn decode(stored_value: &Self::Encoded) -> 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then use it like the following just as any other codec.
|
||||
let (get, set, remove) = use_local_storage::<MyState, MyStateCodec>("my-struct-key");
|
||||
# view! { }
|
||||
# }
|
||||
```
|
|
@ -64,7 +64,7 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.demo-container button ~ button {
|
||||
.demo-container button + button {
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
|
||||
|
|
22
docs/book/src/options.md
Normal file
22
docs/book/src/options.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Options
|
||||
|
||||
Most functions in Leptos-Use come with a version `..._with_options`. For example `use_css_var` has a
|
||||
version `use_css_var_with_options`. As the name suggests, you can provide additional options to those versions of the
|
||||
functions.
|
||||
|
||||
These options are defined as structs with the corresponding PascalCase name. For our example `use_css_var_with_options`
|
||||
the name of the struct is `UseCssVarOptions`. Every option struct implements `Default` and the builder pattern to
|
||||
make it easy to change only the values needed. This can look like the following example.
|
||||
|
||||
```rust
|
||||
let (color, set_color) = use_css_var_with_options(
|
||||
"--color",
|
||||
UseCssVarOptions::default()
|
||||
.target(el)
|
||||
.initial_value("#eee"),
|
||||
);
|
||||
```
|
||||
|
||||
Here only the values `target` and `initial_value` are changed and everything else is left to default.
|
||||
|
||||
TODO : automatic conversion like Fn and Option
|
|
@ -54,6 +54,13 @@ By adding `"leptos-use/ssr"` to the `ssr` feature of your project, it will only
|
|||
be enabled when your project is built with `ssr`, and you will get the server
|
||||
functions server-side, and the client functions client-side.
|
||||
|
||||
## WASM on the server
|
||||
|
||||
If you enable `ssr` in your project on a `wasm32` target architecture, you will get
|
||||
a compile-time warning in the console because it is a common mistake that users enable `ssr` globally.
|
||||
If you're using `wasm32` on the server however you can safely disable this warning by
|
||||
enabling the `wasm_ssr` feature together with `ssr`.
|
||||
|
||||
## Functions with Target Elements
|
||||
|
||||
A lot of functions like `use_resize_observer` and `use_element_size` are only useful when a target HTML/SVG element is
|
||||
|
|
5
docs/book/src/utilities/use_derive_signal.md
Normal file
5
docs/book/src/utilities/use_derive_signal.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# use_derive_signal!
|
||||
|
||||
Macro to easily create helper functions that derive a signal using a piece of code.
|
||||
|
||||
See [`is_ok`](is_ok.md) or [`use_to_string`](use_to_string.md) as examples.
|
|
@ -55,6 +55,7 @@ members = [
|
|||
"use_throttle_fn",
|
||||
"use_timeout_fn",
|
||||
"use_timestamp",
|
||||
"use_user_media",
|
||||
"use_web_notification",
|
||||
"use_websocket",
|
||||
"use_webtransport",
|
||||
|
|
|
@ -3,7 +3,7 @@ use leptos::ev::{keypress, KeyboardEvent};
|
|||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_use::storage::use_local_storage;
|
||||
use leptos_use::storage::{use_local_storage, use_local_storage_with_options, UseStorageOptions};
|
||||
use leptos_use::utils::FromToStringCodec;
|
||||
use leptos_use::{
|
||||
use_color_mode_with_options, use_cookie_with_options, use_debounce_fn, use_event_listener,
|
||||
|
@ -40,7 +40,10 @@ pub fn App() -> impl IntoView {
|
|||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let (count, set_count, _) = use_local_storage::<i32, FromToStringCodec>("count-state");
|
||||
let (count, set_count, _) = use_local_storage_with_options::<i32, FromToStringCodec>(
|
||||
"count-state",
|
||||
UseStorageOptions::default().delay_during_hydration(true),
|
||||
);
|
||||
let on_click = move |_| set_count.update(|count| *count += 1);
|
||||
|
||||
let nf = use_intl_number_format(
|
||||
|
@ -96,6 +99,10 @@ fn HomePage() -> impl IntoView {
|
|||
<p>Dark preferred: {is_dark_preferred}</p>
|
||||
<LocalStorageTest/>
|
||||
<p>Test cookie: {move || test_cookie().unwrap_or("<Expired>".to_string())}</p>
|
||||
|
||||
<Show when={move || count() > 0 }>
|
||||
<div>Greater than 0 </div>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,12 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
codee = { path = "../../../codee", features = ["json_serde"] }
|
||||
leptos = { version = "0.6", features = ["nightly", "csr"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos-use = { path = "../..", features = ["docs", "prost", "serde"] }
|
||||
leptos-use = { path = "../..", features = ["docs"] }
|
||||
web-sys = "0.3"
|
||||
serde = "1.0.163"
|
||||
|
||||
|
|
16
examples/use_user_media/Cargo.toml
Normal file
16
examples/use_user_media/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "use_user_media"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { version = "0.6", features = ["nightly", "csr"] }
|
||||
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"
|
23
examples/use_user_media/README.md
Normal file
23
examples/use_user_media/README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
A simple example for `use_user_media`.
|
||||
|
||||
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
|
||||
```
|
2
examples/use_user_media/Trunk.toml
Normal file
2
examples/use_user_media/Trunk.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[build]
|
||||
public_url = "/demo/"
|
7
examples/use_user_media/index.html
Normal file
7
examples/use_user_media/index.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="css" href="style/output.css">
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
3
examples/use_user_media/input.css
Normal file
3
examples/use_user_media/input.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
2
examples/use_user_media/rust-toolchain.toml
Normal file
2
examples/use_user_media/rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
57
examples/use_user_media/src/main.rs
Normal file
57
examples/use_user_media/src/main.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use leptos::*;
|
||||
use leptos_use::docs::demo_or_body;
|
||||
use leptos_use::{use_user_media, UseUserMediaReturn};
|
||||
|
||||
#[component]
|
||||
fn Demo() -> impl IntoView {
|
||||
let video_ref = create_node_ref::<leptos::html::Video>();
|
||||
|
||||
let UseUserMediaReturn {
|
||||
stream,
|
||||
enabled,
|
||||
set_enabled,
|
||||
..
|
||||
} = use_user_media();
|
||||
|
||||
create_effect(move |_| {
|
||||
match stream.get() {
|
||||
Some(Ok(s)) => {
|
||||
video_ref.get().map(|v| v.set_src_object(Some(&s)));
|
||||
return;
|
||||
}
|
||||
Some(Err(e)) => logging::error!("Failed to get media stream: {:?}", e),
|
||||
None => logging::log!("No stream yet"),
|
||||
}
|
||||
|
||||
video_ref.get().map(|v| v.set_src_object(None));
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="flex flex-col gap-4 text-center">
|
||||
<div>
|
||||
<button on:click=move |_| set_enabled(
|
||||
!enabled(),
|
||||
)>{move || if enabled() { "Stop" } else { "Start" }} video</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<video
|
||||
node_ref=video_ref
|
||||
controls=false
|
||||
autoplay=true
|
||||
muted=true
|
||||
class="h-96 w-auto"
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
mount_to(demo_or_body(), || {
|
||||
view! { <Demo/> }
|
||||
})
|
||||
}
|
350
examples/use_user_media/style/output.css
Normal file
350
examples/use_user_media/style/output.css
Normal file
|
@ -0,0 +1,350 @@
|
|||
[type='text'],input:where(:not([type])),[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, input:where(:not([type])):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;
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
::-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],[size]:where(select:not([size="1"])) {
|
||||
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");
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
[type='checkbox']:checked {
|
||||
-webkit-appearance: auto;
|
||||
-moz-appearance: auto;
|
||||
appearance: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
[type='radio']:checked {
|
||||
-webkit-appearance: auto;
|
||||
-moz-appearance: auto;
|
||||
appearance: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
[type='checkbox']:indeterminate {
|
||||
-webkit-appearance: auto;
|
||||
-moz-appearance: auto;
|
||||
appearance: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[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: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
::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: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.h-96 {
|
||||
height: 24rem;
|
||||
}
|
||||
|
||||
.w-auto {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
}
|
15
examples/use_user_media/tailwind.config.js
Normal file
15
examples/use_user_media/tailwind.config.js
Normal 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'),
|
||||
],
|
||||
}
|
|
@ -5,10 +5,12 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
leptos = { version = "0.6", features = ["nightly", "csr"] }
|
||||
codee = { path = "../../../codee", features = ["msgpack_serde"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos-use = { path = "../..", features = ["docs"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
web-sys = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
use leptos::prelude::*;
|
||||
use leptos_use::docs::demo_or_body;
|
||||
use leptos_use::{
|
||||
core::ConnectionReadyState, use_websocket, use_websocket_with_options, UseWebSocketOptions,
|
||||
UseWebsocketReturn,
|
||||
core::ConnectionReadyState, use_websocket, use_websocket_with_options, UseWebSocketError,
|
||||
UseWebSocketOptions, UseWebSocketReturn,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use codee::{binary::MsgpackSerdeCodec, string::FromToStringCodec};
|
||||
use web_sys::{CloseEvent, Event};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct Apple {
|
||||
name: String,
|
||||
worm_count: u32,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Demo() -> impl IntoView {
|
||||
let (history, set_history) = signal(vec![]);
|
||||
|
@ -18,27 +26,22 @@ fn Demo() -> impl IntoView {
|
|||
// use_websocket
|
||||
// ----------------------------
|
||||
|
||||
let UseWebsocketReturn {
|
||||
let UseWebSocketReturn {
|
||||
ready_state,
|
||||
message,
|
||||
message_bytes,
|
||||
send,
|
||||
send_bytes,
|
||||
open,
|
||||
close,
|
||||
..
|
||||
} = use_websocket("wss://echo.websocket.events/");
|
||||
} = use_websocket::<Apple, MsgpackSerdeCodec>("wss://echo.websocket.events/");
|
||||
|
||||
let send_message = move |_| {
|
||||
let m = "Hello, world!";
|
||||
send(m);
|
||||
set_history.update(|history: &mut Vec<_>| history.push(format! {"[send]: {:?}", m}));
|
||||
};
|
||||
|
||||
let send_byte_message = move |_| {
|
||||
let m = b"Hello, world!\r\n".to_vec();
|
||||
send_bytes(m.clone());
|
||||
set_history.update(|history: &mut Vec<_>| history.push(format! {"[send_bytes]: {:?}", m}));
|
||||
let m = Apple {
|
||||
name: "More worm than apple".to_string(),
|
||||
worm_count: 10,
|
||||
};
|
||||
send(&m);
|
||||
set_history.update(|history: &mut Vec<_>| history.push(format!("[send]: {:?}", m)));
|
||||
};
|
||||
|
||||
let status = move || ready_state().to_string();
|
||||
|
@ -53,15 +56,11 @@ fn Demo() -> impl IntoView {
|
|||
};
|
||||
|
||||
create_effect(move |_| {
|
||||
if let Some(m) = message.get() {
|
||||
update_history(&set_history, format! {"[message]: {:?}", m});
|
||||
};
|
||||
});
|
||||
|
||||
create_effect(move |_| {
|
||||
if let Some(m) = message_bytes.get() {
|
||||
update_history(&set_history, format! {"[message_bytes]: {:?}", m});
|
||||
};
|
||||
message.with(move |message| {
|
||||
if let Some(m) = message {
|
||||
update_history(&set_history, format!("[message]: {:?}", m));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// ----------------------------
|
||||
|
@ -72,49 +71,44 @@ fn Demo() -> impl IntoView {
|
|||
|
||||
let on_open_callback = move |e: Event| {
|
||||
set_history2.update(|history: &mut Vec<_>| {
|
||||
history.push(format! {"[onopen]: event {:?}", e.type_()})
|
||||
history.push(format!("[onopen]: event {:?}", e.type_()))
|
||||
});
|
||||
};
|
||||
|
||||
let on_close_callback = move |e: CloseEvent| {
|
||||
set_history2.update(|history: &mut Vec<_>| {
|
||||
history.push(format! {"[onclose]: event {:?}", e.type_()})
|
||||
history.push(format!("[onclose]: event {:?}", e.type_()))
|
||||
});
|
||||
};
|
||||
|
||||
let on_error_callback = move |e: Event| {
|
||||
let on_error_callback = move |e: UseWebSocketError<_, _>| {
|
||||
set_history2.update(|history: &mut Vec<_>| {
|
||||
history.push(format! {"[onerror]: event {:?}", e.type_()})
|
||||
history.push(match e {
|
||||
UseWebSocketError::Event(e) => format!("[onerror]: event {:?}", e.type_()),
|
||||
_ => format!("[onerror]: {:?}", e),
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
let on_message_callback = move |m: String| {
|
||||
set_history2.update(|history: &mut Vec<_>| history.push(format! {"[onmessage]: {:?}", m}));
|
||||
let on_message_callback = move |m: &String| {
|
||||
set_history2.update(|history: &mut Vec<_>| history.push(format!("[onmessage]: {:?}", m)));
|
||||
};
|
||||
|
||||
let on_message_bytes_callback = move |m: Vec<u8>| {
|
||||
set_history2
|
||||
.update(|history: &mut Vec<_>| history.push(format! {"[onmessage_bytes]: {:?}", m}));
|
||||
};
|
||||
|
||||
let UseWebsocketReturn {
|
||||
let UseWebSocketReturn {
|
||||
ready_state: ready_state2,
|
||||
send: send2,
|
||||
send_bytes: send_bytes2,
|
||||
open: open2,
|
||||
close: close2,
|
||||
message: message2,
|
||||
message_bytes: message_bytes2,
|
||||
..
|
||||
} = use_websocket_with_options(
|
||||
} = use_websocket_with_options::<String, FromToStringCodec>(
|
||||
"wss://echo.websocket.events/",
|
||||
UseWebSocketOptions::default()
|
||||
.immediate(false)
|
||||
.on_open(on_open_callback.clone())
|
||||
.on_close(on_close_callback.clone())
|
||||
.on_error(on_error_callback.clone())
|
||||
.on_message(on_message_callback.clone())
|
||||
.on_message_bytes(on_message_bytes_callback.clone()),
|
||||
.on_message(on_message_callback.clone()),
|
||||
);
|
||||
|
||||
let open_connection2 = move |_| {
|
||||
|
@ -125,28 +119,16 @@ fn Demo() -> impl IntoView {
|
|||
};
|
||||
|
||||
let send_message2 = move |_| {
|
||||
let message = "Hello, use_leptos!";
|
||||
send2(message);
|
||||
update_history(&set_history2, format! {"[send]: {:?}", message});
|
||||
};
|
||||
|
||||
let send_byte_message2 = move |_| {
|
||||
let m = b"Hello, world!\r\n".to_vec();
|
||||
send_bytes2(m.clone());
|
||||
update_history(&set_history2, format! {"[send_bytes]: {:?}", m});
|
||||
let message = "Hello, use_leptos!".to_string();
|
||||
send2(&message);
|
||||
update_history(&set_history2, format!("[send]: {:?}", message));
|
||||
};
|
||||
|
||||
let status2 = move || ready_state2.get().to_string();
|
||||
|
||||
create_effect(move |_| {
|
||||
if let Some(m) = message2.get() {
|
||||
update_history(&set_history2, format! {"[message]: {:?}", m});
|
||||
};
|
||||
});
|
||||
|
||||
create_effect(move |_| {
|
||||
if let Some(m) = message_bytes2.get() {
|
||||
update_history(&set_history2, format! {"[message_bytes]: {:?}", m});
|
||||
update_history(&set_history2, format!("[message]: {:?}", m));
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -161,9 +143,7 @@ fn Demo() -> impl IntoView {
|
|||
<button on:click=send_message disabled=move || !connected()>
|
||||
"Send"
|
||||
</button>
|
||||
<button on:click=send_byte_message disabled=move || !connected()>
|
||||
"Send bytes"
|
||||
</button>
|
||||
|
||||
<button on:click=open_connection disabled=connected>
|
||||
"Open"
|
||||
</button>
|
||||
|
@ -200,9 +180,7 @@ fn Demo() -> impl IntoView {
|
|||
<button on:click=send_message2 disabled=move || !connected2()>
|
||||
"Send"
|
||||
</button>
|
||||
<button on:click=send_byte_message2 disabled=move || !connected2()>
|
||||
"Send Bytes"
|
||||
</button>
|
||||
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-2xl mr-2">"History"</h3>
|
||||
<button
|
||||
|
|
|
@ -7,7 +7,7 @@ use std::marker::PhantomData;
|
|||
use std::ops::Deref;
|
||||
|
||||
/// Used as an argument type to make it easily possible to pass either
|
||||
/// * a `web_sys` element that implements `E` (for example `EventTarget` or `Element`),
|
||||
/// * a `web_sys` element that implements `E` (for example `EventTarget`, `Element` or `HtmlElement`),
|
||||
/// * an `Option<T>` where `T` is the web_sys element,
|
||||
/// * a `Signal<T>` where `T` is the web_sys element,
|
||||
/// * a `Signal<Option<T>>` where `T` is the web_sys element,
|
||||
|
@ -251,6 +251,7 @@ macro_rules! impl_from_node_ref {
|
|||
|
||||
impl_from_node_ref!(web_sys::EventTarget);
|
||||
impl_from_node_ref!(web_sys::Element);
|
||||
impl_from_node_ref!(web_sys::HtmlElement);
|
||||
|
||||
// From leptos::html::HTMLElement ///////////////////////////////////////////////
|
||||
|
||||
|
@ -270,6 +271,7 @@ macro_rules! impl_from_html_element {
|
|||
|
||||
impl_from_html_element!(web_sys::EventTarget);
|
||||
impl_from_html_element!(web_sys::Element);
|
||||
impl_from_html_element!(web_sys::HtmlElement);
|
||||
|
||||
// From Signal<leptos::html::HTMLElement> /////////////////////////////////////////
|
||||
|
||||
|
@ -330,3 +332,11 @@ impl_from_signal_html_element!(Signal<Option<HtmlElement<HtmlEl>>>, web_sys::Ele
|
|||
impl_from_signal_html_element!(ReadSignal<Option<HtmlElement<HtmlEl>>>, web_sys::Element);
|
||||
impl_from_signal_html_element!(RwSignal<Option<HtmlElement<HtmlEl>>>, web_sys::Element);
|
||||
impl_from_signal_html_element!(Memo<Option<HtmlElement<HtmlEl>>>, web_sys::Element);
|
||||
|
||||
impl_from_signal_html_element!(Signal<Option<HtmlElement<HtmlEl>>>, web_sys::HtmlElement);
|
||||
impl_from_signal_html_element!(
|
||||
ReadSignal<Option<HtmlElement<HtmlEl>>>,
|
||||
web_sys::HtmlElement
|
||||
);
|
||||
impl_from_signal_html_element!(RwSignal<Option<HtmlElement<HtmlEl>>>, web_sys::HtmlElement);
|
||||
impl_from_signal_html_element!(Memo<Option<HtmlElement<HtmlEl>>>, web_sys::HtmlElement);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
use crate::utils::use_derive_signal;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use_derive_signal!(
|
||||
crate::use_derive_signal!(
|
||||
/// Reactive `Result::is_err()`.
|
||||
///
|
||||
/// ## Usage
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use crate::utils::use_derive_signal;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use_derive_signal!(
|
||||
crate::use_derive_signal!(
|
||||
/// Reactive `Option::is_none()`.
|
||||
///
|
||||
/// ## Usage
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use crate::utils::use_derive_signal;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use_derive_signal!(
|
||||
crate::use_derive_signal!(
|
||||
/// Reactive `Result::is_ok()`.
|
||||
///
|
||||
/// ## Usage
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use crate::utils::use_derive_signal;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use_derive_signal!(
|
||||
crate::use_derive_signal!(
|
||||
/// Reactive `Option::is_some()`.
|
||||
///
|
||||
/// ## Usage
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(unexpected_cfgs)]
|
||||
// #![feature(doc_cfg)]
|
||||
//! Collection of essential Leptos utilities inspired by SolidJS USE / VueUse
|
||||
|
||||
|
@ -24,6 +25,7 @@ mod is_none;
|
|||
mod is_ok;
|
||||
mod is_some;
|
||||
mod on_click_outside;
|
||||
mod use_user_media;
|
||||
mod signal_debounced;
|
||||
mod signal_throttled;
|
||||
mod sync_signal;
|
||||
|
@ -89,6 +91,7 @@ pub use is_none::*;
|
|||
pub use is_ok::*;
|
||||
pub use is_some::*;
|
||||
pub use on_click_outside::*;
|
||||
pub use use_user_media::*;
|
||||
pub use signal_debounced::*;
|
||||
pub use signal_throttled::*;
|
||||
pub use sync_signal::*;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::{use_storage_with_options, StorageType, UseStorageOptions};
|
||||
use crate::utils::StringCodec;
|
||||
use codee::{Decoder, Encoder};
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
|
||||
|
@ -16,23 +16,23 @@ pub fn use_local_storage<T, C>(
|
|||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + Default + PartialEq + Send + Sync,
|
||||
C: StringCodec<T> + Default,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
{
|
||||
use_storage_with_options(
|
||||
use_storage_with_options::<T, C>(
|
||||
StorageType::Local,
|
||||
key,
|
||||
UseStorageOptions::<T, C>::default(),
|
||||
UseStorageOptions::<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>::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>,
|
||||
options: UseStorageOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq + Send + Sync,
|
||||
C: StringCodec<T> + Default,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
{
|
||||
use_storage_with_options(StorageType::Local, key, options)
|
||||
use_storage_with_options::<T, C>(StorageType::Local, key, options)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use super::{use_storage_with_options, StorageType, UseStorageOptions};
|
||||
use crate::utils::StringCodec;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use codee::{Decoder, Encoder};
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
|
||||
|
@ -16,23 +15,23 @@ pub fn use_session_storage<T, C>(
|
|||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + Default + PartialEq + Send + Sync,
|
||||
C: StringCodec<T> + Default,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
{
|
||||
use_storage_with_options(
|
||||
use_storage_with_options::<T, C>(
|
||||
StorageType::Session,
|
||||
key,
|
||||
UseStorageOptions::<T, C>::default(),
|
||||
UseStorageOptions::<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>::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>,
|
||||
options: UseStorageOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq + Send + Sync,
|
||||
C: StringCodec<T> + Default,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
{
|
||||
use_storage_with_options(StorageType::Session, key, options)
|
||||
use_storage_with_options::<T, C>(StorageType::Session, key, options)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::{
|
||||
core::{MaybeRwSignal, StorageType},
|
||||
utils::{FilterOptions, StringCodec},
|
||||
utils::FilterOptions,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use codee::{CodecError, Decoder, Encoder};
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
@ -25,12 +26,13 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// 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.
|
||||
/// See [`UseStorageOptions`] to see how behavior can be further customised.
|
||||
///
|
||||
/// See [`StringCodec`] for more details on how to handle versioning — dealing with data that can outlast your code.
|
||||
/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
|
||||
/// binary codec wrapped in [`Base64`].
|
||||
///
|
||||
/// > To use the [`JsonCodec`], you will need to add the `"serde"` feature to your project's `Cargo.toml`.
|
||||
/// > To use [`ProstCodec`], add the feature `"prost"`.
|
||||
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
|
||||
/// available and what feature flags they require.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
|
@ -38,11 +40,13 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::utils::{FromToStringCodec, JsonCodec, ProstCodec};
|
||||
/// # use codee::string::{FromToStringCodec, JsonSerdeCodec, Base64};
|
||||
/// # use codee::binary::ProstCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// // Binds a struct:
|
||||
/// let (state, set_state, _) = use_local_storage::<MyState, JsonCodec>("my-state");
|
||||
/// let (state, set_state, _) = use_local_storage::<MyState, JsonSerdeCodec>("my-state");
|
||||
///
|
||||
/// // Binds a bool, stored as a string:
|
||||
/// let (flag, set_flag, remove_flag) = use_session_storage::<bool, FromToStringCodec>("my-flag");
|
||||
|
@ -50,10 +54,10 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// // Binds a number, stored as a string:
|
||||
/// let (count, set_count, _) = use_session_storage::<i32, FromToStringCodec>("my-count");
|
||||
/// // Binds a number, stored in JSON:
|
||||
/// let (count, set_count, _) = use_session_storage::<i32, JsonCodec>("my-count-kept-in-js");
|
||||
/// let (count, set_count, _) = use_session_storage::<i32, JsonSerdeCodec>("my-count-kept-in-js");
|
||||
///
|
||||
/// // Bind string with SessionStorage stored in ProtoBuf format:
|
||||
/// let (id, set_id, _) = use_storage::<String, ProstCodec>(
|
||||
/// let (id, set_id, _) = use_storage::<String, Base64<ProstCodec>>(
|
||||
/// StorageType::Session,
|
||||
/// "my-id",
|
||||
/// );
|
||||
|
@ -80,13 +84,67 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Create Your Own Custom Codec
|
||||
///
|
||||
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
|
||||
///
|
||||
/// ## Server-Side Rendering
|
||||
///
|
||||
/// On the server the returned signals will just read/manipulate the `initial_value` without persistence.
|
||||
///
|
||||
/// ### Hydration bugs and `use_cookie`
|
||||
///
|
||||
/// If you use a value from storage to control conditional rendering you might run into issues with
|
||||
/// hydration.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::use_session_storage;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Example() -> impl IntoView {
|
||||
/// let (flag, set_flag, _) = use_session_storage::<bool, FromToStringCodec>("my-flag");
|
||||
///
|
||||
/// view! {
|
||||
/// <Show when=move || flag.get()>
|
||||
/// <div>Some conditional content</div>
|
||||
/// </Show>
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can see hydration warnings in the browser console and the conditional parts of
|
||||
/// the app might never show up when rendered on the server and then hydrated in the browser. The
|
||||
/// reason for this is that the server has no access to storage and therefore will always use
|
||||
/// `initial_value` as described above. So on the server your app is always rendered as if
|
||||
/// the value from storage was `initial_value`. Then in the browser the actual stored value is used
|
||||
/// which might be different, hence during hydration the DOM looks different from the one rendered
|
||||
/// on the server which produces the hydration warnings.
|
||||
///
|
||||
/// The recommended way to avoid this is to use `use_cookie` instead because values stored in cookies
|
||||
/// are available on the server as well as in the browser.
|
||||
///
|
||||
/// If you still want to use storage instead of cookies you can use the `delay_during_hydration`
|
||||
/// option that will use the `initial_value` during hydration just as on the server and delay loading
|
||||
/// the value from storage by an animation frame. This gets rid of the hydration warnings and makes
|
||||
/// the app correctly render things. Some flickering might be unavoidable though.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{use_local_storage_with_options, UseStorageOptions};
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Example() -> impl IntoView {
|
||||
/// let (flag, set_flag, _) = use_local_storage_with_options::<bool, FromToStringCodec>(
|
||||
/// "my-flag",
|
||||
/// UseStorageOptions::default().delay_during_hydration(true),
|
||||
/// );
|
||||
///
|
||||
/// view! {
|
||||
/// <Show when=move || flag.get()>
|
||||
/// <div>Some conditional content</div>
|
||||
/// </Show>
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn use_storage<T, C>(
|
||||
storage_type: StorageType,
|
||||
|
@ -94,7 +152,7 @@ pub fn use_storage<T, C>(
|
|||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Default + Clone + PartialEq + Send + Sync,
|
||||
C: StringCodec<T> + Default,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
{
|
||||
use_storage_with_options::<T, C>(storage_type, key, UseStorageOptions::default())
|
||||
}
|
||||
|
@ -103,39 +161,42 @@ where
|
|||
pub fn use_storage_with_options<T, C>(
|
||||
storage_type: StorageType,
|
||||
key: impl AsRef<str>,
|
||||
options: UseStorageOptions<T, C>,
|
||||
options: UseStorageOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq + Send + Sync,
|
||||
C: StringCodec<T> + Default,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
{
|
||||
let UseStorageOptions {
|
||||
codec,
|
||||
on_error,
|
||||
listen_to_storage_changes,
|
||||
initial_value,
|
||||
filter,
|
||||
delay_during_hydration,
|
||||
} = options;
|
||||
|
||||
let (data, set_data) = initial_value.into_signal();
|
||||
let default = data.get_untracked();
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
let _ = codec;
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let _ = on_error;
|
||||
let _ = listen_to_storage_changes;
|
||||
let _ = filter;
|
||||
let _ = delay_during_hydration;
|
||||
let _ = storage_type;
|
||||
let _ = key;
|
||||
let _ = INTERNAL_STORAGE_EVENT;
|
||||
|
||||
|
||||
let remove = move || {
|
||||
set_data.set(default.clone());
|
||||
};
|
||||
|
||||
(data.into(), set_data, remove)
|
||||
} else {
|
||||
(data, set_data, remove)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use crate::{use_event_listener, use_window, watch_with_options, WatchOptions};
|
||||
|
||||
// Get storage API
|
||||
|
@ -174,9 +235,9 @@ where
|
|||
// 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()
|
||||
|
@ -188,11 +249,11 @@ where
|
|||
handle_error(&on_error, result)
|
||||
})
|
||||
.unwrap_or_default() // Drop handled Err(())
|
||||
.as_ref()
|
||||
.map(|encoded| {
|
||||
// Decode item
|
||||
let result = codec
|
||||
.decode(encoded)
|
||||
.map_err(UseStorageError::ItemCodecError);
|
||||
let result = C::decode(encoded)
|
||||
.map_err(|e| UseStorageError::ItemCodecError(CodecError::Decode(e)));
|
||||
handle_error(&on_error, result)
|
||||
})
|
||||
.transpose()
|
||||
|
@ -212,9 +273,6 @@ where
|
|||
}
|
||||
};
|
||||
|
||||
// Fetch initial value
|
||||
fetch_from_storage();
|
||||
|
||||
// Fires when storage needs to be fetched
|
||||
let notify = Trigger::new();
|
||||
|
||||
|
@ -233,7 +291,6 @@ where
|
|||
// 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();
|
||||
|
@ -247,9 +304,8 @@ where
|
|||
|
||||
if let Ok(storage) = &storage {
|
||||
// Encode value
|
||||
let result = codec
|
||||
.encode(value)
|
||||
.map_err(UseStorageError::ItemCodecError)
|
||||
let result = C::encode(value)
|
||||
.map_err(|e| UseStorageError::ItemCodecError(CodecError::Encode(e)))
|
||||
.and_then(|enc_value| {
|
||||
// Set storage -- sends a global event
|
||||
storage
|
||||
|
@ -267,6 +323,13 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
// Fetch initial value
|
||||
if delay_during_hydration && leptos::leptos_dom::HydrationCtx::is_hydrating() {
|
||||
request_animation_frame(fetch_from_storage.clone());
|
||||
} else {
|
||||
fetch_from_storage();
|
||||
}
|
||||
|
||||
if listen_to_storage_changes {
|
||||
let check_key = key.as_ref().to_owned();
|
||||
// Listen to global storage events
|
||||
|
@ -307,12 +370,12 @@ where
|
|||
};
|
||||
|
||||
(data, set_data, remove)
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
/// Session handling errors returned by [`use_storage_with_options`].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UseStorageError<Err> {
|
||||
pub enum UseStorageError<E, D> {
|
||||
#[error("storage not available")]
|
||||
StorageNotAvailable(JsValue),
|
||||
#[error("storage not returned from window")]
|
||||
|
@ -326,69 +389,62 @@ pub enum UseStorageError<Err> {
|
|||
#[error("failed to notify item changed")]
|
||||
NotifyItemChangedFailed(JsValue),
|
||||
#[error("failed to encode / decode item value")]
|
||||
ItemCodecError(Err),
|
||||
ItemCodecError(CodecError<E, D>),
|
||||
}
|
||||
|
||||
/// 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: StringCodec<T>> {
|
||||
// Translates to and from UTF-16 strings
|
||||
codec: C,
|
||||
#[derive(DefaultBuilder)]
|
||||
pub struct UseStorageOptions<T, E, D>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
// Callback for when an error occurs
|
||||
on_error: Arc<dyn Fn(UseStorageError<C::Error>)>,
|
||||
#[builder(skip)]
|
||||
on_error: Arc<dyn Fn(UseStorageError<E, D>)>,
|
||||
// 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
|
||||
#[builder(skip)]
|
||||
initial_value: MaybeRwSignal<T>,
|
||||
// Debounce or throttle the writing to storage whenever the value changes
|
||||
#[builder(into)]
|
||||
filter: FilterOptions,
|
||||
/// Delays the reading of the value from storage by one animation frame during hydration.
|
||||
/// This ensures that during hydration the value is the initial value just like it is on the server
|
||||
/// which helps prevent hydration errors. Defaults to `false`.
|
||||
delay_during_hydration: bool,
|
||||
}
|
||||
|
||||
/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling.
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn handle_error<T, Err>(
|
||||
on_error: &Arc<dyn Fn(UseStorageError<Err>)>,
|
||||
result: Result<T, UseStorageError<Err>>,
|
||||
fn handle_error<T, E, D>(
|
||||
on_error: &Arc<dyn Fn(UseStorageError<E, D>)>,
|
||||
result: Result<T, UseStorageError<E, D>>,
|
||||
) -> Result<T, ()> {
|
||||
result.map_err(|err| (on_error)(err))
|
||||
}
|
||||
|
||||
impl<T: Default, C: StringCodec<T> + Default> Default for UseStorageOptions<T, C> {
|
||||
impl<T: Default, E, D> Default for UseStorageOptions<T, E, D> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
codec: C::default(),
|
||||
on_error: Arc::new(|_err| ()),
|
||||
listen_to_storage_changes: true,
|
||||
initial_value: MaybeRwSignal::default(),
|
||||
filter: FilterOptions::default(),
|
||||
delay_during_hydration: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default, C: StringCodec<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
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default, E, D> UseStorageOptions<T, E, D> {
|
||||
/// Optional callback whenever an error occurs.
|
||||
pub fn on_error(self, on_error: impl Fn(UseStorageError<C::Error>) + 'static) -> Self {
|
||||
pub fn on_error(self, on_error: impl Fn(UseStorageError<E, D>) + 'static) -> Self {
|
||||
Self {
|
||||
on_error: Arc::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 {
|
||||
|
@ -396,12 +452,4 @@ impl<T: Default, C: StringCodec<T>> UseStorageOptions<T, C> {
|
|||
..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,9 +1,8 @@
|
|||
use crate::utils::StringCodec;
|
||||
use crate::{
|
||||
js, use_event_listener, use_event_listener_with_options, use_supported, UseEventListenerOptions,
|
||||
};
|
||||
use codee::{CodecError, Decoder, Encoder};
|
||||
use leptos::ev::messageerror;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
@ -25,7 +24,7 @@ use wasm_bindgen::JsValue;
|
|||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::{use_broadcast_channel, UseBroadcastChannelReturn};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
|
@ -46,13 +45,17 @@ use wasm_bindgen::JsValue;
|
|||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Just like with [`use_storage`] you can use different codecs for encoding and decoding.
|
||||
/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
|
||||
/// binary codec wrapped in [`Base64`].
|
||||
///
|
||||
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
|
||||
/// available and what feature flags they require.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::use_broadcast_channel;
|
||||
/// # use leptos_use::utils::JsonCodec;
|
||||
/// # use codee::string::JsonSerdeCodec;
|
||||
/// #
|
||||
/// // Data sent in JSON must implement Serialize, Deserialize:
|
||||
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
|
@ -63,35 +66,35 @@ use wasm_bindgen::JsValue;
|
|||
///
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// use_broadcast_channel::<MyState, JsonCodec>("everyting-is-awesome");
|
||||
/// use_broadcast_channel::<MyState, JsonSerdeCodec>("everyting-is-awesome");
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Create Your Own Custom Codec
|
||||
///
|
||||
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
|
||||
pub fn use_broadcast_channel<T, C>(
|
||||
name: &str,
|
||||
) -> UseBroadcastChannelReturn<T, impl Fn(&T) + Clone, impl Fn() + Clone, C::Error>
|
||||
) -> UseBroadcastChannelReturn<
|
||||
T,
|
||||
impl Fn(&T) + Clone,
|
||||
impl Fn() + Clone,
|
||||
<C as Encoder<T>>::Error,
|
||||
<C as Decoder<T>>::Error,
|
||||
>
|
||||
where
|
||||
C: StringCodec<T> + Default + Clone,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
{
|
||||
let is_supported = use_supported(|| js!("BroadcastChannel" in &window()));
|
||||
|
||||
let (is_closed, set_closed) = signal(false);
|
||||
let (channel, set_channel) = signal(None::<web_sys::BroadcastChannel>);
|
||||
let (message, set_message) = signal(None::<T>);
|
||||
let (error, set_error) = signal(None::<UseBroadcastChannelError<C::Error>>);
|
||||
|
||||
let codec = C::default();
|
||||
let (error, set_error) = signal(
|
||||
None::<UseBroadcastChannelError<<C as Encoder<T>>::Error, <C as Decoder<T>>::Error>>,
|
||||
);
|
||||
|
||||
let post = {
|
||||
let codec = codec.clone();
|
||||
|
||||
move |data: &T| {
|
||||
if let Some(channel) = channel.get_untracked() {
|
||||
match codec.encode(data) {
|
||||
match C::encode(data) {
|
||||
Ok(msg) => {
|
||||
channel
|
||||
.post_message(&msg.into())
|
||||
|
@ -101,7 +104,9 @@ where
|
|||
.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
set_error.set(Some(UseBroadcastChannelError::Encode(err)));
|
||||
set_error.set(Some(UseBroadcastChannelError::Codec(CodecError::Encode(
|
||||
err,
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,11 +132,13 @@ where
|
|||
leptos::ev::message,
|
||||
move |event| {
|
||||
if let Some(data) = event.data().as_string() {
|
||||
match codec.decode(data) {
|
||||
match C::decode(&data) {
|
||||
Ok(msg) => {
|
||||
set_message.set(Some(msg));
|
||||
}
|
||||
Err(err) => set_error.set(Some(UseBroadcastChannelError::Decode(err))),
|
||||
Err(err) => set_error.set(Some(UseBroadcastChannelError::Codec(
|
||||
CodecError::Decode(err),
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
set_error.set(Some(UseBroadcastChannelError::ValueNotString));
|
||||
|
@ -169,12 +176,13 @@ where
|
|||
}
|
||||
|
||||
/// Return type of [`use_broadcast_channel`].
|
||||
pub struct UseBroadcastChannelReturn<T, PFn, CFn, Err>
|
||||
pub struct UseBroadcastChannelReturn<T, PFn, CFn, E, D>
|
||||
where
|
||||
T: 'static,
|
||||
PFn: Fn(&T) + Clone,
|
||||
CFn: Fn() + Clone,
|
||||
Err: 'static,
|
||||
E: 'static,
|
||||
D: 'static,
|
||||
{
|
||||
/// `true` if this browser supports `BroadcastChannel`s.
|
||||
pub is_supported: Signal<bool>,
|
||||
|
@ -192,22 +200,20 @@ where
|
|||
pub close: CFn,
|
||||
|
||||
/// Latest error as reported by the `messageerror` event.
|
||||
pub error: Signal<Option<UseBroadcastChannelError<Err>>>,
|
||||
pub error: Signal<Option<UseBroadcastChannelError<E, D>>>,
|
||||
|
||||
/// Wether the channel is closed
|
||||
pub is_closed: Signal<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum UseBroadcastChannelError<Err> {
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UseBroadcastChannelError<E, D> {
|
||||
#[error("failed to post message")]
|
||||
PostMessage(JsValue),
|
||||
#[error("channel message error")]
|
||||
MessageEvent(web_sys::MessageEvent),
|
||||
#[error("failed to encode value")]
|
||||
Encode(Err),
|
||||
#[error("failed to decode value")]
|
||||
Decode(Err),
|
||||
#[error("failed to (de)encode value")]
|
||||
Codec(CodecError<E, D>),
|
||||
#[error("received value is not a string")]
|
||||
ValueNotString,
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ use crate::core::url;
|
|||
use crate::core::StorageType;
|
||||
use crate::core::{ElementMaybeSignal, MaybeRwSignal};
|
||||
use crate::storage::{use_storage_with_options, UseStorageOptions};
|
||||
use crate::utils::FromToStringCodec;
|
||||
use crate::{sync_signal_with_options, use_cookie, use_preferred_dark, SyncSignalOptions};
|
||||
use codee::string::FromToStringCodec;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
|
@ -121,7 +121,6 @@ use wasm_bindgen::JsCast;
|
|||
///
|
||||
/// ## See also
|
||||
///
|
||||
/// * [`use_dark`]
|
||||
/// * [`use_preferred_dark`]
|
||||
/// * [`use_storage`]
|
||||
/// * [`use_cookie`]
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use crate::core::now;
|
||||
use crate::utils::StringCodec;
|
||||
use codee::{CodecError, Decoder, Encoder};
|
||||
use cookie::time::{Duration, OffsetDateTime};
|
||||
use cookie::{Cookie, CookieJar, SameSite};
|
||||
pub use cookie::SameSite;
|
||||
use cookie::{Cookie, CookieJar};
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// SSR-friendly and reactive cookie access.
|
||||
///
|
||||
/// You can use this function multiple times in your for the same cookie and they're signals will synchronize
|
||||
/// You can use this function multiple times for the same cookie and their signals will synchronize
|
||||
/// (even across windows/tabs). But there is no way to listen to changes to `document.cookie` directly so in case
|
||||
/// something outside of this function changes the cookie, the signal will **not** be updated.
|
||||
///
|
||||
|
@ -30,7 +30,7 @@ use std::rc::Rc;
|
|||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::use_cookie;
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// # use rand::prelude::*;
|
||||
///
|
||||
/// #
|
||||
|
@ -56,7 +56,11 @@ use std::rc::Rc;
|
|||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// See [`StringCodec`] for details on how to handle versioning — dealing with data that can outlast your code.
|
||||
/// Values are (en)decoded via the given codec. You can use any of the string codecs or a
|
||||
/// binary codec wrapped in [`Base64`].
|
||||
///
|
||||
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
|
||||
/// available and what feature flags they require.
|
||||
///
|
||||
/// ## Cookie attributes
|
||||
///
|
||||
|
@ -66,7 +70,7 @@ use std::rc::Rc;
|
|||
/// # use cookie::SameSite;
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
|
@ -86,7 +90,9 @@ use std::rc::Rc;
|
|||
/// This works equally well on the server or the client.
|
||||
/// On the server this function reads the cookie from the HTTP request header and writes it back into
|
||||
/// the HTTP response header according to options (if provided).
|
||||
/// The returned `WriteSignal` will not affect the cookie headers on the server.
|
||||
/// The returned `WriteSignal` may not affect the cookie headers on the server! It will try and write
|
||||
/// the headers buy if this happens after the headers have already been streamed to the client then
|
||||
/// this will have no effect.
|
||||
///
|
||||
/// > If you're using `axum` you have to enable the `"axum"` feature in your Cargo.toml.
|
||||
/// > In case it's `actix-web` enable the feature `"actix"`, for `spin` enable `"spin"`.
|
||||
|
@ -101,7 +107,7 @@ use std::rc::Rc;
|
|||
/// # use leptos::prelude::*;
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::{use_cookie_with_options, UseCookieOptions};
|
||||
/// # use leptos_use::utils::JsonCodec;
|
||||
/// # use codee::string::JsonSerdeCodec;
|
||||
/// #
|
||||
/// # #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
/// # pub struct Auth {
|
||||
|
@ -111,7 +117,7 @@ use std::rc::Rc;
|
|||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// use_cookie_with_options::<Auth, JsonCodec>(
|
||||
/// use_cookie_with_options::<Auth, JsonSerdeCodec>(
|
||||
/// "auth",
|
||||
/// UseCookieOptions::default()
|
||||
/// .ssr_cookies_header_getter(|| {
|
||||
|
@ -136,7 +142,7 @@ use std::rc::Rc;
|
|||
/// All you need to do is to implement the [`StringCodec`] trait together with `Default` and `Clone`.
|
||||
pub fn use_cookie<T, C>(cookie_name: &str) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
|
||||
where
|
||||
C: StringCodec<T> + Default + Clone,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
T: Clone,
|
||||
{
|
||||
use_cookie_with_options::<T, C>(cookie_name, UseCookieOptions::default())
|
||||
|
@ -145,10 +151,10 @@ where
|
|||
/// Version of [`use_cookie`] that takes [`UseCookieOptions`].
|
||||
pub fn use_cookie_with_options<T, C>(
|
||||
cookie_name: &str,
|
||||
options: UseCookieOptions<T, C::Error>,
|
||||
options: UseCookieOptions<T, <C as Encoder<T>>::Error, <C as Decoder<T>>::Error>,
|
||||
) -> (Signal<Option<T>>, WriteSignal<Option<T>>)
|
||||
where
|
||||
C: StringCodec<T> + Default + Clone,
|
||||
C: Encoder<T, Encoded = String> + Decoder<T, Encoded = str>,
|
||||
T: Clone,
|
||||
{
|
||||
let UseCookieOptions {
|
||||
|
@ -181,7 +187,6 @@ where
|
|||
let (cookie, set_cookie) = signal(None::<T>);
|
||||
|
||||
let jar = StoredValue::new(CookieJar::new());
|
||||
let codec = C::default();
|
||||
|
||||
if !has_expired {
|
||||
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
|
||||
|
@ -193,9 +198,8 @@ where
|
|||
set_cookie.set(
|
||||
jar.get(cookie_name)
|
||||
.and_then(|c| {
|
||||
codec
|
||||
.decode(c.value().to_string())
|
||||
.map_err(|err| on_error(err))
|
||||
C::decode(c.value())
|
||||
.map_err(|err| on_error(CodecError::Decode(err)))
|
||||
.ok()
|
||||
})
|
||||
.or(default_value),
|
||||
|
@ -216,16 +220,16 @@ where
|
|||
use crate::{
|
||||
use_broadcast_channel, watch_pausable, UseBroadcastChannelReturn, WatchPausableReturn,
|
||||
};
|
||||
use codee::string::{FromToStringCodec, OptionCodec};
|
||||
|
||||
let UseBroadcastChannelReturn { message, post, .. } =
|
||||
use_broadcast_channel::<Option<String>, OptionStringCodec>(&format!(
|
||||
use_broadcast_channel::<Option<String>, OptionCodec<FromToStringCodec>>(&format!(
|
||||
"leptos-use:cookies:{cookie_name}"
|
||||
));
|
||||
|
||||
let on_cookie_change = {
|
||||
let cookie_name = cookie_name.to_owned();
|
||||
let ssr_cookies_header_getter = Rc::clone(&ssr_cookies_header_getter);
|
||||
let codec = codec.clone();
|
||||
let on_error = Rc::clone(&on_error);
|
||||
let domain = domain.clone();
|
||||
let path = path.clone();
|
||||
|
@ -236,9 +240,11 @@ where
|
|||
}
|
||||
|
||||
let value = cookie.with_untracked(|cookie| {
|
||||
cookie
|
||||
.as_ref()
|
||||
.and_then(|cookie| codec.encode(cookie).map_err(|err| on_error(err)).ok())
|
||||
cookie.as_ref().and_then(|cookie| {
|
||||
C::encode(cookie)
|
||||
.map_err(|err| on_error(CodecError::Encode(err)))
|
||||
.ok()
|
||||
})
|
||||
});
|
||||
|
||||
if value
|
||||
|
@ -290,7 +296,7 @@ where
|
|||
pause();
|
||||
|
||||
if let Some(message) = message {
|
||||
match codec.decode(message.clone()) {
|
||||
match C::decode(&message) {
|
||||
Ok(value) => {
|
||||
let ssr_cookies_header_getter =
|
||||
Rc::clone(&ssr_cookies_header_getter);
|
||||
|
@ -314,7 +320,7 @@ where
|
|||
set_cookie.set(Some(value));
|
||||
}
|
||||
Err(err) => {
|
||||
on_error(err);
|
||||
on_error(CodecError::Decode(err));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -357,27 +363,31 @@ where
|
|||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
if !readonly {
|
||||
let value = cookie
|
||||
.with_untracked(|cookie| {
|
||||
cookie
|
||||
.as_ref()
|
||||
.map(|cookie| codec.encode(&cookie).map_err(|err| on_error(err)).ok())
|
||||
})
|
||||
.flatten();
|
||||
jar.update_value(|jar| {
|
||||
write_server_cookie(
|
||||
cookie_name,
|
||||
value,
|
||||
jar,
|
||||
max_age,
|
||||
expires,
|
||||
domain,
|
||||
path,
|
||||
same_site,
|
||||
secure,
|
||||
http_only,
|
||||
ssr_set_cookie,
|
||||
)
|
||||
create_isomorphic_effect(move |_| {
|
||||
let value = cookie
|
||||
.with(|cookie| {
|
||||
cookie.as_ref().map(|cookie| {
|
||||
C::encode(cookie)
|
||||
.map_err(|err| on_error(CodecError::Encode(err)))
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
jar.update_value(|jar| {
|
||||
write_server_cookie(
|
||||
cookie_name,
|
||||
value,
|
||||
jar,
|
||||
max_age,
|
||||
expires,
|
||||
domain,
|
||||
path,
|
||||
same_site,
|
||||
secure,
|
||||
http_only,
|
||||
ssr_set_cookie,
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -387,7 +397,7 @@ where
|
|||
|
||||
/// Options for [`use_cookie_with_options`].
|
||||
#[derive(DefaultBuilder)]
|
||||
pub struct UseCookieOptions<T, Err> {
|
||||
pub struct UseCookieOptions<T, E, D> {
|
||||
/// [`Max-Age` of the cookie](https://tools.ietf.org/html/rfc6265#section-5.2.2) in milliseconds. The returned signal will turn to `None` after the max age is reached.
|
||||
/// Default: `None`
|
||||
///
|
||||
|
@ -464,10 +474,10 @@ pub struct UseCookieOptions<T, Err> {
|
|||
ssr_set_cookie: Rc<dyn Fn(&Cookie)>,
|
||||
|
||||
/// Callback for encoding/decoding errors. Defaults to logging the error to the console.
|
||||
on_error: Rc<dyn Fn(Err)>,
|
||||
on_error: Rc<dyn Fn(CodecError<E, D>)>,
|
||||
}
|
||||
|
||||
impl<T, Err> Default for UseCookieOptions<T, Err> {
|
||||
impl<T, E, D> Default for UseCookieOptions<T, E, D> {
|
||||
#[allow(dead_code)]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
@ -484,13 +494,13 @@ impl<T, Err> Default for UseCookieOptions<T, Err> {
|
|||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
#[cfg(all(feature = "actix", feature = "axum"))]
|
||||
compile_error!("You cannot enable only one of features \"actix\" and \"axum\" at the same time");
|
||||
compile_error!("You can only enable one of features \"actix\" and \"axum\" at the same time");
|
||||
|
||||
#[cfg(all(feature = "actix", feature = "spin"))]
|
||||
compile_error!("You cannot enable only one of features \"actix\" and \"spin\" at the same time");
|
||||
compile_error!("You can only enable one of features \"actix\" and \"spin\" at the same time");
|
||||
|
||||
#[cfg(all(feature = "axum", feature = "spin"))]
|
||||
compile_error!("You cannot enable only one of features \"axum\" and \"spin\" at the same time");
|
||||
compile_error!("You can only enable one of features \"axum\" and \"spin\" at the same time");
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
const COOKIE: http0_2::HeaderName = http0_2::header::COOKIE;
|
||||
|
@ -854,8 +864,7 @@ fn write_server_cookie(
|
|||
if let Some(value) = value {
|
||||
let cookie: Cookie = build_cookie_from_options(
|
||||
name, max_age, expires, http_only, secure, &path, same_site, &domain, &value,
|
||||
)
|
||||
.into();
|
||||
);
|
||||
|
||||
jar.add(cookie.into_owned());
|
||||
} else {
|
||||
|
@ -879,21 +888,3 @@ fn load_and_parse_cookie_jar(
|
|||
jar
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct OptionStringCodec;
|
||||
|
||||
impl StringCodec<Option<String>> for OptionStringCodec {
|
||||
type Error = ();
|
||||
|
||||
fn encode(&self, val: &Option<String>) -> Result<String, Self::Error> {
|
||||
match val {
|
||||
Some(val) => Ok(format!("~<|Some|>~{val}")),
|
||||
None => Ok("~<|None|>~".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(&self, str: String) -> Result<Option<String>, Self::Error> {
|
||||
Ok(str.strip_prefix("~<|Some|>~").map(|v| v.to_owned()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Reactive [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)
|
||||
///
|
||||
|
@ -30,11 +30,10 @@ use leptos::prelude::wrappers::read::Signal;
|
|||
/// On the server this function returns a Signal that is always `1.0`.
|
||||
pub fn use_device_pixel_ratio() -> Signal<f64> {
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use leptos::prelude::*;
|
||||
let pixel_ratio = Signal::derive(|| 1.0);
|
||||
Signal::derive(|| 1.0)
|
||||
} else {
|
||||
use crate::{use_event_listener_with_options, UseEventListenerOptions};
|
||||
use leptos::prelude::*;
|
||||
|
||||
use leptos::ev::change;
|
||||
|
||||
let initial_pixel_ratio = window().device_pixel_ratio();
|
||||
|
@ -57,6 +56,7 @@ pub fn use_device_pixel_ratio() -> Signal<f64> {
|
|||
.once(true),
|
||||
);
|
||||
});
|
||||
|
||||
pixel_ratio.into()
|
||||
}}
|
||||
pixel_ratio.into()
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use crate::core::ConnectionReadyState;
|
||||
use crate::utils::StringCodec;
|
||||
use crate::{js, use_event_listener};
|
||||
use crate::{js, use_event_listener, ReconnectLimit};
|
||||
use codee::Decoder;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::prelude::diagnostics::SpecialNonReactiveZone;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::cell::Cell;
|
||||
|
@ -21,14 +20,17 @@ use thiserror::Error;
|
|||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Values are decoded via the given [`Codec`].
|
||||
/// Values are decoded via the given decoder. You can use any of the string codecs or a
|
||||
/// binary codec wrapped in [`Base64`].
|
||||
///
|
||||
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
|
||||
/// available and what feature flags they require.
|
||||
///
|
||||
/// > To use the [`JsonCodec`], you will need to add the `"serde"` feature to your project's `Cargo.toml`.
|
||||
/// > To use [`ProstCodec`], add the feature `"prost"`.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::{use_event_source, UseEventSourceReturn, utils::JsonCodec};
|
||||
/// # use leptos_use::{use_event_source, UseEventSourceReturn};
|
||||
/// # use codee::string::JsonSerdeCodec;
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// #
|
||||
/// #[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
|
@ -41,7 +43,7 @@ use thiserror::Error;
|
|||
/// # fn Demo() -> impl IntoView {
|
||||
/// let UseEventSourceReturn {
|
||||
/// ready_state, data, error, close, ..
|
||||
/// } = use_event_source::<EventSourceData, JsonCodec>("https://event-source-url");
|
||||
/// } = use_event_source::<EventSourceData, JsonSerdeCodec>("https://event-source-url");
|
||||
/// #
|
||||
/// # view! { }
|
||||
/// # }
|
||||
|
@ -57,7 +59,8 @@ use thiserror::Error;
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec};
|
||||
/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions};
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
|
@ -88,7 +91,8 @@ use thiserror::Error;
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, utils::FromToStringCodec};
|
||||
/// # use leptos_use::{use_event_source_with_options, UseEventSourceReturn, UseEventSourceOptions, ReconnectLimit};
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
|
@ -97,7 +101,7 @@ use thiserror::Error;
|
|||
/// } = use_event_source_with_options::<bool, FromToStringCodec>(
|
||||
/// "https://event-source-url",
|
||||
/// UseEventSourceOptions::default()
|
||||
/// .reconnect_limit(5) // at most 5 attempts
|
||||
/// .reconnect_limit(ReconnectLimit::Limited(5)) // at most 5 attempts
|
||||
/// .reconnect_interval(2000) // wait for 2 seconds between attempts
|
||||
/// );
|
||||
/// #
|
||||
|
@ -116,24 +120,23 @@ pub fn use_event_source<T, C>(
|
|||
) -> UseEventSourceReturn<T, C::Error, impl Fn() + Clone + 'static, impl Fn() + Clone + 'static>
|
||||
where
|
||||
T: Clone + PartialEq + Send + Sync + 'static,
|
||||
C: StringCodec<T> + Default,
|
||||
C: Decoder<T, Encoded = str>,
|
||||
C::Error: Send + Sync,
|
||||
{
|
||||
use_event_source_with_options(url, UseEventSourceOptions::<T, C>::default())
|
||||
use_event_source_with_options::<T, C>(url, UseEventSourceOptions::<T>::default())
|
||||
}
|
||||
|
||||
/// Version of [`use_event_source`] that takes a `UseEventSourceOptions`. See [`use_event_source`] for how to use.
|
||||
pub fn use_event_source_with_options<T, C>(
|
||||
url: &str,
|
||||
options: UseEventSourceOptions<T, C>,
|
||||
options: UseEventSourceOptions<T>,
|
||||
) -> UseEventSourceReturn<T, C::Error, impl Fn() + Clone + 'static, impl Fn() + Clone + 'static>
|
||||
where
|
||||
T: Clone + PartialEq + Send + Sync + 'static,
|
||||
C: StringCodec<T> + Default,
|
||||
C: Decoder<T, Encoded = str>,
|
||||
C::Error: Send + Sync,
|
||||
{
|
||||
let UseEventSourceOptions {
|
||||
codec,
|
||||
reconnect_limit,
|
||||
reconnect_interval,
|
||||
on_failed,
|
||||
|
@ -156,7 +159,7 @@ where
|
|||
|
||||
let set_data_from_string = move |data_string: Option<String>| {
|
||||
if let Some(data_string) = data_string {
|
||||
match codec.decode(data_string) {
|
||||
match C::decode(&data_string) {
|
||||
Ok(data) => set_data.set(Some(data)),
|
||||
Err(err) => set_error.set(Some(UseEventSourceError::Deserialize(err))),
|
||||
}
|
||||
|
@ -218,12 +221,15 @@ where
|
|||
|
||||
// only reconnect if EventSource isn't reconnecting by itself
|
||||
// this is the case when the connection is closed (readyState is 2)
|
||||
if es.ready_state() == 2 && !explicitly_closed.get() && reconnect_limit > 0 {
|
||||
if es.ready_state() == 2
|
||||
&& !explicitly_closed.get()
|
||||
&& matches!(reconnect_limit, ReconnectLimit::Limited(_))
|
||||
{
|
||||
es.close();
|
||||
|
||||
retried.set(retried.get() + 1);
|
||||
|
||||
if retried.get() < reconnect_limit {
|
||||
if reconnect_limit.is_exceeded_by(retried.get()) {
|
||||
set_timeout(
|
||||
move || {
|
||||
if let Some(init) = init.get_value() {
|
||||
|
@ -244,19 +250,13 @@ where
|
|||
es.set_onerror(Some(on_error.as_ref().unchecked_ref()));
|
||||
on_error.forget();
|
||||
|
||||
let on_message = Closure::wrap(Box::new({
|
||||
let set_data_from_string = set_data_from_string.clone();
|
||||
|
||||
move |e: web_sys::MessageEvent| {
|
||||
set_data_from_string(e.data().as_string());
|
||||
}
|
||||
let on_message = Closure::wrap(Box::new(move |e: web_sys::MessageEvent| {
|
||||
set_data_from_string(e.data().as_string());
|
||||
}) as Box<dyn FnMut(web_sys::MessageEvent)>);
|
||||
es.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
|
||||
on_message.forget();
|
||||
|
||||
for event_name in named_events.clone() {
|
||||
let set_data_from_string = set_data_from_string.clone();
|
||||
|
||||
let _ = use_event_listener(
|
||||
es.clone(),
|
||||
leptos::ev::Custom::<ev::Event>::new(event_name),
|
||||
|
@ -314,16 +314,13 @@ where
|
|||
|
||||
/// Options for [`use_event_source_with_options`].
|
||||
#[derive(DefaultBuilder)]
|
||||
pub struct UseEventSourceOptions<T, C>
|
||||
pub struct UseEventSourceOptions<T>
|
||||
where
|
||||
T: 'static,
|
||||
C: StringCodec<T>,
|
||||
{
|
||||
/// Decodes from the received String to a value of type `T`.
|
||||
codec: C,
|
||||
|
||||
/// Retry times. Defaults to 3.
|
||||
reconnect_limit: u64,
|
||||
/// Retry times. Defaults to `ReconnectLimit::Limited(3)`. Use `ReconnectLimit::Infinite` for
|
||||
/// infinite retries.
|
||||
reconnect_limit: ReconnectLimit,
|
||||
|
||||
/// Retry interval in ms. Defaults to 3000.
|
||||
reconnect_interval: u64,
|
||||
|
@ -346,11 +343,10 @@ where
|
|||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T, C: StringCodec<T> + Default> Default for UseEventSourceOptions<T, C> {
|
||||
impl<T> Default for UseEventSourceOptions<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
codec: C::default(),
|
||||
reconnect_limit: 3,
|
||||
reconnect_limit: ReconnectLimit::default(),
|
||||
reconnect_interval: 3000,
|
||||
on_failed: Arc::new(|| {}),
|
||||
immediate: true,
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use crate::utils::use_derive_signal;
|
||||
use leptos::prelude::wrappers::read::Signal;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use_derive_signal!(
|
||||
crate::use_derive_signal!(
|
||||
/// Reactive `ToString::to_string()`.
|
||||
///
|
||||
/// ## Usage
|
||||
|
|
204
src/use_user_media.rs
Normal file
204
src/use_user_media.rs
Normal file
|
@ -0,0 +1,204 @@
|
|||
use crate::core::MaybeRwSignal;
|
||||
use cfg_if::cfg_if;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
||||
/// Reactive [`mediaDevices.getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) streaming.
|
||||
///
|
||||
/// ## Demo
|
||||
///
|
||||
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_user_media)
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::{use_user_media, UseUserMediaReturn};
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// let video_ref = create_node_ref::<leptos::html::Video>();
|
||||
///
|
||||
/// let UseUserMediaReturn { stream, start, .. } = use_user_media();
|
||||
///
|
||||
/// start();
|
||||
///
|
||||
/// create_effect(move |_|
|
||||
/// video_ref.get().map(|v| {
|
||||
/// match stream.get() {
|
||||
/// Some(Ok(s)) => v.set_src_object(Some(&s)),
|
||||
/// Some(Err(e)) => logging::error!("Failed to get media stream: {:?}", e),
|
||||
/// None => logging::log!("No stream yet"),
|
||||
/// }
|
||||
/// })
|
||||
/// );
|
||||
///
|
||||
/// view! { <video node_ref=video_ref controls=false autoplay=true muted=true></video> }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Server-Side Rendering
|
||||
///
|
||||
/// On the server calls to `start` or any other way to enable the stream will be ignored
|
||||
/// and the stream will always be `None`.
|
||||
pub fn use_user_media() -> UseUserMediaReturn<impl Fn() + Clone, impl Fn() + Clone> {
|
||||
use_user_media_with_options(UseUserMediaOptions::default())
|
||||
}
|
||||
|
||||
/// Version of [`use_user_media`] that takes a `UseUserMediaOptions`. See [`use_user_media`] for how to use.
|
||||
pub fn use_user_media_with_options(
|
||||
options: UseUserMediaOptions,
|
||||
) -> UseUserMediaReturn<impl Fn() + Clone, impl Fn() + Clone> {
|
||||
let UseUserMediaOptions {
|
||||
enabled,
|
||||
video,
|
||||
audio,
|
||||
..
|
||||
} = options;
|
||||
|
||||
let (enabled, set_enabled) = enabled.into_signal();
|
||||
|
||||
let (stream, set_stream) = create_signal(None::<Result<web_sys::MediaStream, JsValue>>);
|
||||
|
||||
let _start = move || async move {
|
||||
cfg_if! { if #[cfg(not(feature = "ssr"))] {
|
||||
if stream.get_untracked().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = create_media(video, audio).await;
|
||||
|
||||
set_stream.update(|s| *s = Some(stream));
|
||||
} else {
|
||||
let _ = video;
|
||||
let _ = audio;
|
||||
}}
|
||||
};
|
||||
|
||||
let _stop = move || {
|
||||
if let Some(Ok(stream)) = stream.get_untracked() {
|
||||
for track in stream.get_tracks() {
|
||||
track.unchecked_ref::<web_sys::MediaStreamTrack>().stop();
|
||||
}
|
||||
}
|
||||
|
||||
set_stream.set(None);
|
||||
};
|
||||
|
||||
let start = move || {
|
||||
cfg_if! { if #[cfg(not(feature = "ssr"))] {
|
||||
spawn_local(async move {
|
||||
_start().await;
|
||||
stream.with_untracked(move |stream| {
|
||||
if let Some(Ok(_)) = stream {
|
||||
set_enabled.set(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}}
|
||||
};
|
||||
|
||||
let stop = move || {
|
||||
_stop();
|
||||
set_enabled.set(false);
|
||||
};
|
||||
|
||||
let _ = watch(
|
||||
move || enabled.get(),
|
||||
move |enabled, _, _| {
|
||||
if *enabled {
|
||||
spawn_local(async move {
|
||||
_start().await;
|
||||
});
|
||||
} else {
|
||||
_stop();
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
UseUserMediaReturn {
|
||||
stream: stream.into(),
|
||||
start,
|
||||
stop,
|
||||
enabled,
|
||||
set_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
async fn create_media(video: bool, audio: bool) -> Result<web_sys::MediaStream, JsValue> {
|
||||
use crate::js_fut;
|
||||
use crate::use_window::use_window;
|
||||
|
||||
let media = use_window()
|
||||
.navigator()
|
||||
.ok_or_else(|| JsValue::from_str("Failed to access window.navigator"))
|
||||
.and_then(|n| n.media_devices())?;
|
||||
|
||||
let mut constraints = web_sys::MediaStreamConstraints::new();
|
||||
if video {
|
||||
constraints.video(&JsValue::from(true));
|
||||
}
|
||||
if audio {
|
||||
constraints.audio(&JsValue::from(true));
|
||||
}
|
||||
|
||||
let promise = media.get_user_media_with_constraints(&constraints)?;
|
||||
let res = js_fut!(promise).await?;
|
||||
|
||||
Ok::<_, JsValue>(web_sys::MediaStream::unchecked_from_js(res))
|
||||
}
|
||||
|
||||
/// Options for [`use_user_media_with_options`].
|
||||
/// Either or both constraints must be specified.
|
||||
/// If the browser cannot find all media tracks with the specified types that meet the constraints given,
|
||||
/// then the returned promise is rejected with `NotFoundError`
|
||||
#[derive(DefaultBuilder, Clone, Copy, Debug)]
|
||||
pub struct UseUserMediaOptions {
|
||||
/// If the stream is enabled. Defaults to `false`.
|
||||
enabled: MaybeRwSignal<bool>,
|
||||
/// Constraint parameter describing video media type requested
|
||||
/// The default value is `false`.
|
||||
video: bool,
|
||||
/// Constraint parameter describing audio media type requested
|
||||
/// The default value is `false`.
|
||||
audio: bool,
|
||||
}
|
||||
|
||||
impl Default for UseUserMediaOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false.into(),
|
||||
video: true,
|
||||
audio: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return type of [`use_user_media`].
|
||||
#[derive(Clone)]
|
||||
pub struct UseUserMediaReturn<StartFn, StopFn>
|
||||
where
|
||||
StartFn: Fn() + Clone,
|
||||
StopFn: Fn() + Clone,
|
||||
{
|
||||
/// The current [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) if it exists.
|
||||
/// Initially this is `None` until `start` resolved successfully.
|
||||
/// In case the stream couldn't be started, for example because the user didn't grant permission,
|
||||
/// this has the value `Some(Err(...))`.
|
||||
pub stream: Signal<Option<Result<web_sys::MediaStream, JsValue>>>,
|
||||
|
||||
/// Starts the screen streaming. Triggers the ask for permission if not already granted.
|
||||
pub start: StartFn,
|
||||
|
||||
/// Stops the screen streaming
|
||||
pub stop: StopFn,
|
||||
|
||||
/// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
|
||||
/// has resolved successfully and thus the stream is enabled.
|
||||
pub enabled: Signal<bool>,
|
||||
|
||||
/// A value of `true` is the same as calling `start()` whereas `false` is the same as calling `stop()`.
|
||||
pub set_enabled: WriteSignal<bool>,
|
||||
}
|
|
@ -5,8 +5,12 @@ use leptos::{leptos_dom::helpers::TimeoutHandle, prelude::*};
|
|||
use send_wrapper::SendWrapper;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::core::ConnectionReadyState;
|
||||
use codee::{
|
||||
CodecError, Decoder, Encoder, HybridCoderError, HybridDecoder, HybridEncoder, IsBinary,
|
||||
};
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use js_sys::Array;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
@ -20,30 +24,30 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Values are (en)decoded via the given codec. You can use any of the codecs, string or binary.
|
||||
///
|
||||
/// > Please check [the codec chapter](https://leptos-use.rs/codecs.html) to see what codecs are
|
||||
/// available and what feature flags they require.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::{use_websocket, UseWebsocketReturn};
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// # use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
/// # use leptos_use::core::ConnectionReadyState;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// let UseWebsocketReturn {
|
||||
/// let UseWebSocketReturn {
|
||||
/// ready_state,
|
||||
/// message,
|
||||
/// message_bytes,
|
||||
/// send,
|
||||
/// send_bytes,
|
||||
/// open,
|
||||
/// close,
|
||||
/// ..
|
||||
/// } = use_websocket("wss://echo.websocket.events/");
|
||||
/// } = use_websocket::<String, FromToStringCodec>("wss://echo.websocket.events/");
|
||||
///
|
||||
/// let send_message = move |_| {
|
||||
/// send("Hello, world!");
|
||||
/// };
|
||||
///
|
||||
/// let send_byte_message = move |_| {
|
||||
/// send_bytes(b"Hello, world!\r\n".to_vec());
|
||||
/// send(&"Hello, world!".to_string());
|
||||
/// };
|
||||
///
|
||||
/// let status = move || ready_state.get().to_string();
|
||||
|
@ -63,17 +67,49 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
/// <p>"status: " {status}</p>
|
||||
///
|
||||
/// <button on:click=send_message disabled=move || !connected()>"Send"</button>
|
||||
/// <button on:click=send_byte_message disabled=move || !connected()>"Send bytes"</button>
|
||||
/// <button on:click=open_connection disabled=connected>"Open"</button>
|
||||
/// <button on:click=close_connection disabled=move || !connected()>"Close"</button>
|
||||
///
|
||||
/// <p>"Receive message: " {move || format!("{:?}", message.get())}</p>
|
||||
/// <p>"Receive byte message: " {move || format!("{:?}", message_bytes.get())}</p>
|
||||
/// </div>
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Here is another example using `msgpack` for encoding and decoding. This means that only binary
|
||||
/// messages can be sent or received. For this to work you have to enable the **`msgpack_serde` feature** flag.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use codee::binary::MsgpackSerdeCodec;
|
||||
/// # use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// #[derive(Serialize, Deserialize)]
|
||||
/// struct SomeData {
|
||||
/// name: String,
|
||||
/// count: i32,
|
||||
/// }
|
||||
///
|
||||
/// let UseWebSocketReturn {
|
||||
/// message,
|
||||
/// send,
|
||||
/// ..
|
||||
/// } = use_websocket::<SomeData, MsgpackSerdeCodec>("wss://some.websocket.server/");
|
||||
///
|
||||
/// let send_data = move || {
|
||||
/// send(&SomeData {
|
||||
/// name: "John Doe".to_string(),
|
||||
/// count: 42,
|
||||
/// });
|
||||
/// };
|
||||
/// #
|
||||
/// # view! {}
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Relative Paths
|
||||
///
|
||||
/// If the provided `url` is relative, it will be resolved relative to the current page.
|
||||
|
@ -105,11 +141,11 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
/// #[derive(Clone)]
|
||||
/// pub struct WebsocketContext {
|
||||
/// pub message: Signal<Option<String>>,
|
||||
/// send: Arc<dyn Fn(&str)>, // use Arc to make it easily cloneable
|
||||
/// send: Arc<dyn Fn(&String)>, // use Arc to make it easily cloneable
|
||||
/// }
|
||||
///
|
||||
/// impl WebsocketContext {
|
||||
/// pub fn new(message: Signal<Option<String>>, send: Arc<dyn Fn(&str)>) -> Self {
|
||||
/// pub fn new(message: Signal<Option<String>>, send: Arc<dyn Fn(&String)>) -> Self {
|
||||
/// Self {
|
||||
/// message,
|
||||
/// send,
|
||||
|
@ -119,7 +155,7 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
/// // create a method to avoid having to use parantheses around the field
|
||||
/// #[inline(always)]
|
||||
/// pub fn send(&self, message: &str) {
|
||||
/// (self.send)(message)
|
||||
/// (self.send)(&message.to_string())
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
@ -128,16 +164,17 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::{use_websocket, UseWebsocketReturn};
|
||||
/// # use codee::string::FromToStringCodec;
|
||||
/// # use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
/// # use std::sync::Arc;
|
||||
/// # #[derive(Clone)]
|
||||
/// # pub struct WebsocketContext {
|
||||
/// # pub message: Signal<Option<String>>,
|
||||
/// # send: Arc<dyn Fn(&str)>,
|
||||
/// # send: Arc<dyn Fn(&String)>,
|
||||
/// # }
|
||||
/// #
|
||||
/// # impl WebsocketContext {
|
||||
/// # pub fn new(message: Signal<Option<String>>, send: Arc<dyn Fn(&str)>) -> Self {
|
||||
/// # pub fn new(message: Signal<Option<String>>, send: Arc<dyn Fn(&String)>) -> Self {
|
||||
/// # Self {
|
||||
/// # message,
|
||||
/// # send,
|
||||
|
@ -147,11 +184,11 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
///
|
||||
/// # #[component]
|
||||
/// # fn Demo() -> impl IntoView {
|
||||
/// let UseWebsocketReturn {
|
||||
/// let UseWebSocketReturn {
|
||||
/// message,
|
||||
/// send,
|
||||
/// ..
|
||||
/// } = use_websocket("ws:://some.websocket.io");
|
||||
/// } = use_websocket::<String, FromToStringCodec>("ws:://some.websocket.io");
|
||||
///
|
||||
/// provide_context(WebsocketContext::new(message, Arc::new(send.clone())));
|
||||
/// #
|
||||
|
@ -163,18 +200,18 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::{use_websocket, UseWebsocketReturn};
|
||||
/// # use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
/// # use std::sync::Arc;
|
||||
/// # #[derive(Clone)]
|
||||
/// # pub struct WebsocketContext {
|
||||
/// # pub message: Signal<Option<String>>,
|
||||
/// # send: Arc<dyn Fn(&str)>,
|
||||
/// # send: Arc<dyn Fn(&String)>,
|
||||
/// # }
|
||||
/// #
|
||||
/// # impl WebsocketContext {
|
||||
/// # #[inline(always)]
|
||||
/// # pub fn send(&self, message: &str) {
|
||||
/// # (self.send)(message)
|
||||
/// # (self.send)(&message.to_string())
|
||||
/// # }
|
||||
/// # }
|
||||
///
|
||||
|
@ -191,32 +228,52 @@ use web_sys::{BinaryType, CloseEvent, Event, MessageEvent, WebSocket};
|
|||
/// ## Server-Side Rendering
|
||||
///
|
||||
/// On the server the returned functions amount to no-ops.
|
||||
pub fn use_websocket(
|
||||
pub fn use_websocket<T, C>(
|
||||
url: &str,
|
||||
) -> UseWebsocketReturn<
|
||||
) -> UseWebSocketReturn<
|
||||
T,
|
||||
impl Fn() + Clone + 'static,
|
||||
impl Fn() + Clone + 'static,
|
||||
impl Fn(&str) + Clone + 'static,
|
||||
impl Fn(Vec<u8>) + Clone + 'static,
|
||||
> {
|
||||
use_websocket_with_options(url, UseWebSocketOptions::default())
|
||||
impl Fn(&T) + Clone + 'static,
|
||||
>
|
||||
where
|
||||
T: 'static,
|
||||
C: Encoder<T> + Decoder<T>,
|
||||
C: IsBinary<T, <C as Decoder<T>>::Encoded>,
|
||||
C: HybridDecoder<T, <C as Decoder<T>>::Encoded, Error = <C as Decoder<T>>::Error>,
|
||||
C: HybridEncoder<T, <C as Encoder<T>>::Encoded, Error = <C as Encoder<T>>::Error>,
|
||||
{
|
||||
use_websocket_with_options::<T, C>(url, UseWebSocketOptions::default())
|
||||
}
|
||||
|
||||
/// Version of [`use_websocket`] that takes `UseWebSocketOptions`. See [`use_websocket`] for how to use.
|
||||
pub fn use_websocket_with_options(
|
||||
pub fn use_websocket_with_options<T, C>(
|
||||
url: &str,
|
||||
options: UseWebSocketOptions,
|
||||
) -> UseWebsocketReturn<
|
||||
options: UseWebSocketOptions<
|
||||
T,
|
||||
HybridCoderError<<C as Encoder<T>>::Error>,
|
||||
HybridCoderError<<C as Decoder<T>>::Error>,
|
||||
>,
|
||||
) -> UseWebSocketReturn<
|
||||
T,
|
||||
impl Fn() + Clone + 'static,
|
||||
impl Fn() + Clone + 'static,
|
||||
impl Fn(&str) + Clone + 'static,
|
||||
impl Fn(Vec<u8>) + Clone,
|
||||
> {
|
||||
impl Fn(&T) + Clone + 'static,
|
||||
>
|
||||
where
|
||||
T: 'static,
|
||||
C: Encoder<T> + Decoder<T>,
|
||||
C: IsBinary<T, <C as Decoder<T>>::Encoded>,
|
||||
C: HybridDecoder<T, <C as Decoder<T>>::Encoded, Error = <C as Decoder<T>>::Error>,
|
||||
C: HybridEncoder<T, <C as Encoder<T>>::Encoded, Error = <C as Encoder<T>>::Error>,
|
||||
{
|
||||
let url = normalize_url(url);
|
||||
|
||||
let UseWebSocketOptions {
|
||||
on_open,
|
||||
on_message,
|
||||
on_message_bytes,
|
||||
on_message_raw,
|
||||
on_message_raw_bytes,
|
||||
on_error,
|
||||
on_close,
|
||||
reconnect_limit,
|
||||
|
@ -227,12 +284,12 @@ pub fn use_websocket_with_options(
|
|||
|
||||
let (ready_state, set_ready_state) = signal(ConnectionReadyState::Closed);
|
||||
let (message, set_message) = signal(None);
|
||||
let (message_bytes, set_message_bytes) = signal(None);
|
||||
let ws_ref: StoredValue<Option<SendWrapper<WebSocket>>> = StoredValue::new(None);
|
||||
|
||||
let reconnect_timer_ref: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
|
||||
|
||||
let reconnect_times_ref: StoredValue<u64> = StoredValue::new(0);
|
||||
let manually_closed_ref: StoredValue<bool> = StoredValue::new(false);
|
||||
|
||||
let unmounted = Arc::new(AtomicBool::new(false));
|
||||
|
||||
|
@ -243,10 +300,10 @@ pub fn use_websocket_with_options(
|
|||
let reconnect_ref: StoredValue<Option<Arc<dyn Fn() + Send + Sync>>> =
|
||||
StoredValue::new(None);
|
||||
reconnect_ref.set_value({
|
||||
let ws = ws_ref.get_value();
|
||||
Some(Arc::new(move || {
|
||||
if reconnect_times_ref.get_value() < reconnect_limit
|
||||
&& ws.clone().map_or(false, |ws: SendWrapper<WebSocket>| {
|
||||
if !manually_closed_ref.get_value()
|
||||
&& !reconnect_limit.is_exceeded_by(reconnect_times_ref.get_value())
|
||||
&& ws_ref.get_value().map_or(false, |ws: SendWrapper<WebSocket>| {
|
||||
ws.ready_state() != WebSocket::OPEN
|
||||
})
|
||||
{
|
||||
|
@ -267,13 +324,13 @@ pub fn use_websocket_with_options(
|
|||
});
|
||||
|
||||
connect_ref.set_value({
|
||||
let ws = ws_ref.get_value();
|
||||
let unmounted = Arc::clone(&unmounted);
|
||||
let on_error = Arc::clone(&on_error);
|
||||
|
||||
Some(Arc::new(move || {
|
||||
reconnect_timer_ref.set_value(None);
|
||||
|
||||
if let Some(web_socket) = &ws {
|
||||
if let Some(web_socket) = ws_ref.get_value() {
|
||||
let _ = web_socket.close();
|
||||
}
|
||||
|
||||
|
@ -323,7 +380,9 @@ pub fn use_websocket_with_options(
|
|||
{
|
||||
let unmounted = Arc::clone(&unmounted);
|
||||
let on_message = Arc::clone(&on_message);
|
||||
let on_message_bytes = Arc::clone(&on_message_bytes);
|
||||
let on_message_raw = Arc::clone(&on_message_raw);
|
||||
let on_message_raw_bytes = Arc::clone(&on_message_raw_bytes);
|
||||
let on_error = Arc::clone(&on_error);
|
||||
|
||||
let onmessage_closure = Closure::wrap(Box::new(move |e: MessageEvent| {
|
||||
if unmounted.get() {
|
||||
|
@ -345,12 +404,27 @@ pub fn use_websocket_with_options(
|
|||
#[cfg(debug_assertions)]
|
||||
let zone = diagnostics::SpecialNonReactiveZone::enter();
|
||||
|
||||
on_message(txt.clone());
|
||||
on_message_raw(&txt);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
SpecialNonReactiveZone::exit(prev);
|
||||
|
||||
match C::decode_str(&txt) {
|
||||
Ok(val) => {
|
||||
#[cfg(debug_assertions)]
|
||||
let prev = SpecialNonReactiveZone::enter();
|
||||
|
||||
on_message(&val);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
drop(zone);
|
||||
|
||||
set_message.set(Some(txt));
|
||||
set_message.set(Some(val));
|
||||
}
|
||||
Err(err) => {
|
||||
on_error(CodecError::Decode(err).into());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -361,12 +435,27 @@ pub fn use_websocket_with_options(
|
|||
#[cfg(debug_assertions)]
|
||||
let zone = diagnostics::SpecialNonReactiveZone::enter();
|
||||
|
||||
on_message_bytes(array.clone());
|
||||
on_message_raw_bytes(&array);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
drop(zone);
|
||||
|
||||
set_message_bytes.set(Some(array));
|
||||
match C::decode_bin(array.as_slice()) {
|
||||
Ok(val) => {
|
||||
#[cfg(debug_assertions)]
|
||||
let prev = SpecialNonReactiveZone::enter();
|
||||
|
||||
on_message(&val);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
SpecialNonReactiveZone::exit(prev);
|
||||
|
||||
set_message.set(Some(val));
|
||||
}
|
||||
Err(err) => {
|
||||
on_error(CodecError::Decode(err).into());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
|
@ -392,7 +481,7 @@ pub fn use_websocket_with_options(
|
|||
#[cfg(debug_assertions)]
|
||||
let zone = diagnostics::SpecialNonReactiveZone::enter();
|
||||
|
||||
on_error(e);
|
||||
on_error(UseWebSocketError::Event(e));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
drop(zone);
|
||||
|
@ -439,7 +528,7 @@ pub fn use_websocket_with_options(
|
|||
}
|
||||
|
||||
// Send text (String)
|
||||
let send = {
|
||||
let send_str = {
|
||||
Box::new(move |data: &str| {
|
||||
if ready_state.get_untracked() == ConnectionReadyState::Open {
|
||||
if let Some(web_socket) = ws_ref.get_value() {
|
||||
|
@ -450,10 +539,28 @@ pub fn use_websocket_with_options(
|
|||
};
|
||||
|
||||
// Send bytes
|
||||
let send_bytes = move |data: Vec<u8>| {
|
||||
let send_bytes = move |data: &[u8]| {
|
||||
if ready_state.get_untracked() == ConnectionReadyState::Open {
|
||||
if let Some(web_socket) = ws_ref.get_value() {
|
||||
let _ = web_socket.send_with_u8_array(&data);
|
||||
let _ = web_socket.send_with_u8_array(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let send = {
|
||||
let on_error = Rc::clone(&on_error);
|
||||
|
||||
move |value: &T| {
|
||||
if C::is_binary() {
|
||||
match C::encode_bin(value) {
|
||||
Ok(val) => send_bytes(&val),
|
||||
Err(err) => on_error(CodecError::Encode(err).into()),
|
||||
}
|
||||
} else {
|
||||
match C::encode_str(value) {
|
||||
Ok(val) => send_str(&val),
|
||||
Err(err) => on_error(CodecError::Encode(err).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -471,7 +578,7 @@ pub fn use_websocket_with_options(
|
|||
reconnect_timer_ref.set_value(None);
|
||||
|
||||
move || {
|
||||
reconnect_times_ref.set_value(reconnect_limit);
|
||||
manually_closed_ref.set_value(true);
|
||||
if let Some(web_socket) = ws_ref.get_value() {
|
||||
let _ = web_socket.close();
|
||||
}
|
||||
|
@ -491,33 +598,61 @@ pub fn use_websocket_with_options(
|
|||
close();
|
||||
});
|
||||
|
||||
UseWebsocketReturn {
|
||||
UseWebSocketReturn {
|
||||
ready_state: ready_state.into(),
|
||||
message: message.into(),
|
||||
message_bytes: message_bytes.into(),
|
||||
ws: ws_ref.get_value(),
|
||||
open,
|
||||
close,
|
||||
send,
|
||||
send_bytes,
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ReconnectLimit {
|
||||
Infinite,
|
||||
Limited(u64),
|
||||
}
|
||||
|
||||
impl Default for ReconnectLimit {
|
||||
fn default() -> Self {
|
||||
ReconnectLimit::Limited(3)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReconnectLimit {
|
||||
pub fn is_exceeded_by(self, times: u64) -> bool {
|
||||
match self {
|
||||
ReconnectLimit::Infinite => false,
|
||||
ReconnectLimit::Limited(limit) => times >= limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ArcFnBytes = Arc<dyn Fn(&[u8])>;
|
||||
|
||||
/// Options for [`use_websocket_with_options`].
|
||||
#[derive(DefaultBuilder)]
|
||||
pub struct UseWebSocketOptions {
|
||||
pub struct UseWebSocketOptions<T, E, D>
|
||||
where
|
||||
T: ?Sized,
|
||||
{
|
||||
/// `WebSocket` connect callback.
|
||||
on_open: Arc<dyn Fn(Event) + Send + Sync>,
|
||||
/// `WebSocket` message callback for typed message decoded by codec.
|
||||
#[builder(skip)]
|
||||
on_message: Arc<dyn Fn(&T)>,
|
||||
/// `WebSocket` message callback for text.
|
||||
on_message: Arc<dyn Fn(String) + Send + Sync>,
|
||||
on_message_raw: Arc<dyn Fn(&str) + Send + Sync>,
|
||||
/// `WebSocket` message callback for binary.
|
||||
on_message_bytes: Arc<dyn Fn(Vec<u8>) + Send + Sync>,
|
||||
on_message_raw_bytes: ArcFnBytes + Send + Sync,
|
||||
/// `WebSocket` error callback.
|
||||
on_error: Arc<dyn Fn(Event) + Send + Sync>,
|
||||
#[builder(skip)]
|
||||
on_error: Arc<dyn Fn(UseWebSocketError<E, D>) + Send + Sync>,
|
||||
/// `WebSocket` close callback.
|
||||
on_close: Arc<dyn Fn(CloseEvent) + Send + Sync>,
|
||||
/// Retry times. Defaults to 3.
|
||||
reconnect_limit: u64,
|
||||
/// Retry times. Defaults to `ReconnectLimit::Limited(3)`. Use `ReconnectLimit::Infinite` for
|
||||
/// infinite retries.
|
||||
reconnect_limit: ReconnectLimit,
|
||||
/// Retry interval in ms. Defaults to 3000.
|
||||
reconnect_interval: u64,
|
||||
/// If `true` the `WebSocket` connection will immediately be opened when calling this function.
|
||||
|
@ -528,15 +663,40 @@ pub struct UseWebSocketOptions {
|
|||
protocols: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for UseWebSocketOptions {
|
||||
impl<T: ?Sized, E, D> UseWebSocketOptions<T, E, D> {
|
||||
/// `WebSocket` error callback.
|
||||
pub fn on_error<F>(self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(UseWebSocketError<E, D>) + 'static,
|
||||
{
|
||||
Self {
|
||||
on_error: Rc::new(handler),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// `WebSocket` message callback for typed message decoded by codec.
|
||||
pub fn on_message<F>(self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&T) + 'static,
|
||||
{
|
||||
Self {
|
||||
on_message: Rc::new(handler),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, E, D> Default for UseWebSocketOptions<T, E, D> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
on_open: Arc::new(|_| {}),
|
||||
on_message: Arc::new(|_| {}),
|
||||
on_message_bytes: Arc::new(|_| {}),
|
||||
on_message_raw: Arc::new(|_| {}),
|
||||
on_message_raw_bytes: Arc::new(|_| {}),
|
||||
on_error: Arc::new(|_| {}),
|
||||
on_close: Arc::new(|_| {}),
|
||||
reconnect_limit: 3,
|
||||
reconnect_limit: ReconnectLimit::default(),
|
||||
reconnect_interval: 3000,
|
||||
immediate: true,
|
||||
protocols: Default::default(),
|
||||
|
@ -546,29 +706,33 @@ impl Default for UseWebSocketOptions {
|
|||
|
||||
/// Return type of [`use_websocket`].
|
||||
#[derive(Clone)]
|
||||
pub struct UseWebsocketReturn<OpenFn, CloseFn, SendFn, SendBytesFn>
|
||||
pub struct UseWebSocketReturn<T, OpenFn, CloseFn, SendFn>
|
||||
where
|
||||
T: 'static,
|
||||
OpenFn: Fn() + Clone + 'static,
|
||||
CloseFn: Fn() + Clone + 'static,
|
||||
SendFn: Fn(&str) + Clone + 'static,
|
||||
SendBytesFn: Fn(Vec<u8>) + Clone + 'static,
|
||||
SendFn: Fn(&T) + Clone + 'static,
|
||||
{
|
||||
/// The current state of the `WebSocket` connection.
|
||||
pub ready_state: Signal<ConnectionReadyState>,
|
||||
/// Latest text message received from `WebSocket`.
|
||||
pub message: Signal<Option<String>>,
|
||||
/// Latest binary message received from `WebSocket`.
|
||||
pub message_bytes: Signal<Option<Vec<u8>>>,
|
||||
/// Latest message received from `WebSocket`.
|
||||
pub message: Signal<Option<T>>,
|
||||
/// The `WebSocket` instance.
|
||||
pub ws: Option<SendWrapper<WebSocket>>,
|
||||
/// Opens the `WebSocket` connection
|
||||
pub open: OpenFn,
|
||||
/// Closes the `WebSocket` connection
|
||||
pub close: CloseFn,
|
||||
/// Sends `text` (string) based data
|
||||
/// Sends data through the socket
|
||||
pub send: SendFn,
|
||||
/// Sends binary data
|
||||
pub send_bytes: SendBytesFn,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum UseWebSocketError<E, D> {
|
||||
#[error("WebSocket error event")]
|
||||
Event(Event),
|
||||
#[error("WebSocket codec error: {0}")]
|
||||
Codec(#[from] CodecError<E, D>),
|
||||
}
|
||||
|
||||
fn normalize_url(url: &str) -> String {
|
||||
|
|
|
@ -21,7 +21,7 @@ use web_sys::WebTransportBidirectionalStream;
|
|||
#[cfg(feature = "bincode")]
|
||||
use bincode::serde::{decode_from_slice as from_slice, encode_to_vec as to_vec};
|
||||
|
||||
///
|
||||
/// This still under development and will not arrive before Leptos 0.7.
|
||||
///
|
||||
/// ## Demo
|
||||
///
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
use super::BinCodec;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Copy, Clone, Default, PartialEq)]
|
||||
pub struct FromToBytesCodec;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FromToBytesCodecError {
|
||||
#[error("failed to convert byte slice to byte array")]
|
||||
InvalidByteSlice(#[from] std::array::TryFromSliceError),
|
||||
|
||||
#[error("failed to convert byte array to string")]
|
||||
InvalidString(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
macro_rules! impl_bin_codec_for_number {
|
||||
($num:ty) => {
|
||||
impl BinCodec<$num> for FromToBytesCodec {
|
||||
type Error = FromToBytesCodecError;
|
||||
|
||||
fn encode(&self, val: &$num) -> Result<Vec<u8>, Self::Error> {
|
||||
Ok(val.to_be_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn decode(&self, val: &[u8]) -> Result<$num, Self::Error> {
|
||||
Ok(<$num>::from_be_bytes(val.try_into()?))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_bin_codec_for_number!(i8);
|
||||
impl_bin_codec_for_number!(u8);
|
||||
|
||||
impl_bin_codec_for_number!(i16);
|
||||
impl_bin_codec_for_number!(u16);
|
||||
|
||||
impl_bin_codec_for_number!(i32);
|
||||
impl_bin_codec_for_number!(u32);
|
||||
|
||||
impl_bin_codec_for_number!(i64);
|
||||
impl_bin_codec_for_number!(u64);
|
||||
|
||||
impl_bin_codec_for_number!(i128);
|
||||
impl_bin_codec_for_number!(u128);
|
||||
|
||||
impl_bin_codec_for_number!(isize);
|
||||
impl_bin_codec_for_number!(usize);
|
||||
|
||||
impl_bin_codec_for_number!(f32);
|
||||
impl_bin_codec_for_number!(f64);
|
||||
|
||||
impl BinCodec<bool> for FromToBytesCodec {
|
||||
type Error = FromToBytesCodecError;
|
||||
|
||||
fn encode(&self, val: &bool) -> Result<Vec<u8>, Self::Error> {
|
||||
let codec = FromToBytesCodec;
|
||||
let num: u8 = if *val { 1 } else { 0 };
|
||||
codec.encode(&num)
|
||||
}
|
||||
|
||||
fn decode(&self, val: &[u8]) -> Result<bool, Self::Error> {
|
||||
let codec = FromToBytesCodec;
|
||||
let num: u8 = codec.decode(val)?;
|
||||
Ok(num != 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl BinCodec<String> for FromToBytesCodec {
|
||||
type Error = FromToBytesCodecError;
|
||||
|
||||
fn encode(&self, val: &String) -> Result<Vec<u8>, Self::Error> {
|
||||
Ok(val.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn decode(&self, val: &[u8]) -> Result<String, Self::Error> {
|
||||
Ok(String::from_utf8(val.to_vec())?)
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
mod from_to_bytes;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use from_to_bytes::*;
|
||||
|
||||
/// A codec for encoding and decoding values to and from strings.
|
||||
/// These strings are intended to be sent over the network.
|
||||
pub trait BinCodec<T>: Clone + 'static {
|
||||
/// The error type returned when encoding or decoding fails.
|
||||
type Error;
|
||||
/// Encodes a value to a string.
|
||||
fn encode(&self, val: &T) -> Result<Vec<u8>, Self::Error>;
|
||||
/// Decodes a string to a value. Should be able to decode any string encoded by [`encode`].
|
||||
fn decode(&self, val: &[u8]) -> Result<T, Self::Error>;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
mod bin;
|
||||
mod string;
|
||||
|
||||
pub use string::*;
|
|
@ -1,45 +0,0 @@
|
|||
use super::StringCodec;
|
||||
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::prelude::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// #
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// let (get, set, remove) = use_local_storage::<i32, FromToStringCodec>("my-key");
|
||||
/// # view! { }
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Copy, Clone, Default, PartialEq)]
|
||||
pub struct FromToStringCodec;
|
||||
|
||||
impl<T: FromStr + ToString> StringCodec<T> for FromToStringCodec {
|
||||
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 = FromToStringCodec;
|
||||
assert_eq!(codec.encode(&s), Ok(s.clone()));
|
||||
assert_eq!(codec.decode(s.clone()), Ok(s));
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
use super::StringCodec;
|
||||
|
||||
/// A codec for storing JSON messages that relies on [`serde_json`] to parse.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::utils::JsonCodec;
|
||||
/// #
|
||||
/// # 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::prelude::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::utils::StringCodec;
|
||||
/// #
|
||||
/// # 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 StringCodec<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::prelude::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
|
||||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use serde_json::json;
|
||||
/// # use leptos_use::utils::StringCodec;
|
||||
/// #
|
||||
/// # 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 StringCodec<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(Copy, Clone, Default, PartialEq)]
|
||||
pub struct JsonCodec;
|
||||
|
||||
impl<T: serde::Serialize + serde::de::DeserializeOwned> StringCodec<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);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
mod from_to_string;
|
||||
#[cfg(feature = "serde_json")]
|
||||
mod json;
|
||||
#[cfg(feature = "prost")]
|
||||
mod prost;
|
||||
|
||||
pub use from_to_string::*;
|
||||
#[cfg(feature = "serde_json")]
|
||||
pub use json::*;
|
||||
#[cfg(feature = "prost")]
|
||||
pub use prost::*;
|
||||
|
||||
/// A codec for encoding and decoding values to and from strings.
|
||||
/// These strings are intended to be stored in browser storage or sent over the network.
|
||||
///
|
||||
/// ## 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 StringCodec<T>: Clone + 'static {
|
||||
/// The error type returned when encoding or decoding fails.
|
||||
type Error;
|
||||
/// Encodes a value to a string.
|
||||
fn encode(&self, val: &T) -> Result<String, Self::Error>;
|
||||
/// Decodes a string to a value. Should be able to decode any string encoded by [`encode`].
|
||||
fn decode(&self, str: String) -> Result<T, Self::Error>;
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
use super::StringCodec;
|
||||
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::prelude::*;
|
||||
/// # use leptos_use::storage::{StorageType, use_local_storage, use_session_storage, use_storage, UseStorageOptions};
|
||||
/// # use leptos_use::utils::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(Copy, 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> StringCodec<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));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
mod codecs;
|
||||
mod filters;
|
||||
mod is;
|
||||
mod js;
|
||||
|
@ -7,10 +6,8 @@ mod pausable;
|
|||
mod signal_filtered;
|
||||
mod use_derive_signal;
|
||||
|
||||
pub use codecs::*;
|
||||
pub use filters::*;
|
||||
pub use is::*;
|
||||
pub(crate) use js_value_from_to_string::*;
|
||||
pub use pausable::*;
|
||||
pub(crate) use signal_filtered::*;
|
||||
pub(crate) use use_derive_signal::*;
|
||||
|
|
|
@ -8,6 +8,7 @@ macro_rules! signal_filtered {
|
|||
) => {
|
||||
paste! {
|
||||
$(#[$outer])*
|
||||
#[track_caller]
|
||||
pub fn [<signal_ $filter_name d>]<S, T>(
|
||||
value: S,
|
||||
ms: impl Into<MaybeSignal<f64>> + 'static,
|
||||
|
@ -27,6 +28,7 @@ macro_rules! signal_filtered {
|
|||
/// See
|
||||
#[$simple_func_doc]
|
||||
/// for how to use.
|
||||
#[track_caller]
|
||||
pub fn [<signal_ $filter_name d_with_options>]<S, T>(
|
||||
value: S,
|
||||
ms: impl Into<MaybeSignal<f64>> + 'static,
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
/// Macro to easily create helper functions that derive a signal using a piece of code.
|
||||
///
|
||||
/// See [`is_ok`] or [`use_to_string`] as examples.
|
||||
#[macro_export]
|
||||
macro_rules! use_derive_signal {
|
||||
(
|
||||
$(#[$outer:meta])*
|
||||
|
@ -14,5 +18,3 @@ macro_rules! use_derive_signal {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use use_derive_signal;
|
||||
|
|
Loading…
Add table
Reference in a new issue