Merge branch 'main' into mouse-coord-type

This commit is contained in:
Marc-Stefan Cassola 2024-08-14 03:26:14 +01:00 committed by GitHub
commit ee7934fd7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 146 additions and 137 deletions

View file

@ -11,7 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `use_clipboard` doesn't need the unstable flags anymore. - `use_clipboard` doesn't need the unstable flags anymore.
- `use_locale` now uses `unic_langid::LanguageIdentifier` and proper locale matching (thanks to @mondeja). - `use_locale` now uses `unic_langid::LanguageIdentifier` and proper locale matching (thanks to @mondeja).
- Removed `UseMouseEventExtractorDefault` and reworked `UseMouseCoordType` (thanks to @carloskiki) - Removed `UseMouseEventExtractorDefault` and reworked `UseMouseCoordType` (thanks to @carloskiki)
- `use_preferred_dark` and `use_color_mode` now try to read the `Sec-CH-Prefers-Color-Scheme` header in SSR.
### Fixes 🍕
- Fixed the codec chapter in the book to refer to crate `codee`.
## [0.11.4] - 2024-08-12 ## [0.11.4] - 2024-08-12

View file

@ -34,11 +34,11 @@ num = { version = "0.4", optional = true }
paste = "1" paste = "1"
thiserror = "1" thiserror = "1"
unic-langid = "0.9" unic-langid = "0.9"
wasm-bindgen = ">=0.2.93" wasm-bindgen = "=0.2.93"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
[dependencies.web-sys] [dependencies.web-sys]
version = ">=0.3.70" version = "=0.3.70"
features = [ features = [
"AddEventListenerOptions", "AddEventListenerOptions",
"BinaryType", "BinaryType",

View file

@ -1,9 +1,9 @@
# Encoding and Decoding Data # Encoding and Decoding Data
Several functions encode and decode data for storing it and/or sending it over the network. To do this, codecs 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 from the crate [`codee`](https://docs.rs/codee/latest/codee/) are used. They
implement the traits [`Encoder`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/mod.rs#L9) with the implement the traits [`Encoder`](https://docs.rs/codee/latest/codee/trait.Encoder.html) with the
method `encode` and [`Decoder`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/mod.rs#L17) with the method `encode` and [`Decoder`](https://docs.rs/codee/latest/codee/trait.Decoder.html) with the
method `decode`. method `decode`.
There are two types of codecs: One that encodes as binary data (`Vec[u8]`) and another type that encodes as There are two types of codecs: One that encodes as binary data (`Vec[u8]`) and another type that encodes as
@ -11,26 +11,8 @@ 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 [`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. wrap a binary codec and make it a string codec by representing the binary data as a base64 string.
## Available Codecs Please check the documentation of [`codee`](https://docs.rs/codee/latest/codee/) for more details and a list of all
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 ## Example
@ -41,6 +23,7 @@ format. Since cookies can only store strings, we have to use string codecs here.
# use leptos::*; # use leptos::*;
# use leptos_use::use_cookie; # use leptos_use::use_cookie;
# use serde::{Deserialize, Serialize}; # use serde::{Deserialize, Serialize};
# use codee::string::JsonCodec;
# #[component] # #[component]
# pub fn App(cx: Scope) -> impl IntoView { # pub fn App(cx: Scope) -> impl IntoView {
@ -57,100 +40,13 @@ let (cookie, set_cookie) = use_cookie::<MyState, JsonCodec>("my-state-cookie");
## Custom Codecs ## 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 If you don't find a suitable codec for your needs, you can implement your own; it's straightforward!
create a string codec, you can look If you want to create a string codec, you can look at
at [`JsonSerdeCodec`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/string/json_serde.rs). [`JsonSerdeCodec`](https://docs.rs/codee/latest/src/codee/string/json_serde.rs.html).
In case it's a binary codec, have a look In case it's a binary codec, have a look at
at [`BincodeSerdeCodec`](https://github.com/Synphonyte/leptos-use/blob/main/src/utils/codecs/binary/bincode_serde.rs). [`BincodeSerdeCodec`](https://docs.rs/codee/latest/src/codee/binary/bincode_serde.rs.html).
## Versioning ## Versioning
Versioning is the process of handling long-term data that can outlive our code. For a discussion on how to implement versioning please refer to the
[relevant section in the docs for `codee`](https://docs.rs/codee/latest/codee/index.html#versioning).
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! { }
# }
```

View file

@ -21,8 +21,9 @@ log = "0.4"
simple_logger = "4" simple_logger = "4"
tokio = { version = "1", features = ["full"], optional = true } tokio = { version = "1", features = ["full"], optional = true }
tower = { version = "0.4", optional = true } tower = { version = "0.4", optional = true }
tower-default-headers = { git = "https://github.com/banool/tower-default-headers-rs" }
tower-http = { version = "0.5", features = ["fs"], optional = true } tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "0.2.92" wasm-bindgen = "=0.2.93"
thiserror = "1.0.38" thiserror = "1.0.38"
tracing = { version = "0.1.37", optional = true } tracing = { version = "0.1.37", optional = true }
http = "1" http = "1"

View file

@ -2,11 +2,13 @@
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
use axum::{routing::post, Router}; use axum::{routing::post, Router};
use http::{HeaderMap, HeaderName, HeaderValue};
use leptos::logging::log; use leptos::logging::log;
use leptos::*; use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes}; use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos_use_ssr::app::*; use leptos_use_ssr::app::*;
use leptos_use_ssr::fileserv::file_and_error_handler; use leptos_use_ssr::fileserv::file_and_error_handler;
use tower_default_headers::DefaultHeadersLayer;
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging"); simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
@ -20,12 +22,19 @@ async fn main() {
let addr = leptos_options.site_addr; let addr = leptos_options.site_addr;
let routes = generate_route_list(|| view! { <App/> }); let routes = generate_route_list(|| view! { <App/> });
let mut default_headers = HeaderMap::new();
let color_header = HeaderValue::from_static("Sec-CH-Prefers-Color-Scheme");
default_headers.insert(HeaderName::from_static("accept-ch"), color_header.clone());
default_headers.insert(HeaderName::from_static("vary"), color_header.clone());
default_headers.insert(HeaderName::from_static("critical-ch"), color_header);
// build our application with a route // build our application with a route
let app = Router::new() let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(&leptos_options, routes, || view! { <App/> }) .leptos_routes(&leptos_options, routes, || view! { <App/> })
.fallback(file_and_error_handler) .fallback(file_and_error_handler)
.with_state(leptos_options); .with_state(leptos_options)
.layer(DefaultHeadersLayer::new(default_headers));
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
log!("listening on http://{}", &addr); log!("listening on http://{}", &addr);

View file

@ -2,7 +2,11 @@ use crate::core::url;
use crate::core::StorageType; use crate::core::StorageType;
use crate::core::{ElementMaybeSignal, MaybeRwSignal}; use crate::core::{ElementMaybeSignal, MaybeRwSignal};
use crate::storage::{use_storage_with_options, UseStorageOptions}; use crate::storage::{use_storage_with_options, UseStorageOptions};
use crate::{sync_signal_with_options, use_cookie, use_preferred_dark, SyncSignalOptions}; use crate::utils::get_header;
use crate::{
sync_signal_with_options, use_cookie, use_preferred_dark_with_options,
SyncSignalOptions, UsePreferredDarkOptions,
};
use codee::string::FromToStringCodec; use codee::string::FromToStringCodec;
use default_struct_builder::DefaultBuilder; use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
@ -83,7 +87,7 @@ use wasm_bindgen::JsCast;
/// # } /// # }
/// ``` /// ```
/// ///
/// ### Cookies /// ### Cookie
/// ///
/// To persist color mode in a cookie, use `use_cookie_with_options` and specify `.cookie_enabled(true)`. /// To persist color mode in a cookie, use `use_cookie_with_options` and specify `.cookie_enabled(true)`.
/// ///
@ -112,9 +116,24 @@ use wasm_bindgen::JsCast;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// On the server this will by default return `ColorMode::Light`. Persistence with storage is disabled. /// On the server this will try to read the
/// [`Sec-CH-Prefers-Color-Scheme` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme)
/// to determine the color mode. If the header is not present it will return `ColorMode::Light`.
/// Please have a look at the linked documentation above for that header to see browser support
/// as well as potential server requirements.
/// ///
/// If `cookie_enabled` is set to `true`, cookies will be used and if present this value will be used /// > 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"`.
///
/// ### Bring your own header
///
/// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking,
/// you can provide your own way of reading the color scheme header value using the option
/// [`crate::UseColorModeOptions::ssr_color_header_getter`].
///
/// ### Cookie
///
/// If `cookie_enabled` is set to `true`, a cookie will be used and if present this value will be used
/// on the server as well as on the client. Please note that you have to add the `axum` or `actix` /// on the server as well as on the client. Please note that you have to add the `axum` or `actix`
/// feature as described in [`fn@crate::use_cookie`]. /// feature as described in [`fn@crate::use_cookie`].
/// ///
@ -151,6 +170,7 @@ where
emit_auto, emit_auto,
transition_enabled, transition_enabled,
listen_to_storage_changes, listen_to_storage_changes,
ssr_color_header_getter,
_marker, _marker,
} = options; } = options;
@ -162,7 +182,9 @@ where
]) ])
.collect(); .collect();
let preferred_dark = use_preferred_dark(); let preferred_dark = use_preferred_dark_with_options(UsePreferredDarkOptions {
ssr_color_header_getter,
});
let system = Signal::derive(move || { let system = Signal::derive(move || {
if preferred_dark.get() { if preferred_dark.get() {
@ -471,6 +493,14 @@ where
/// Defaults to true. /// Defaults to true.
listen_to_storage_changes: bool, listen_to_storage_changes: bool,
/// Getter function to return the string value of the
/// [`Sec-CH-Prefers-Color-Scheme`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme)
/// header.
/// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default
/// implementation provided.
#[allow(dead_code)]
ssr_color_header_getter: Rc<dyn Fn() -> Option<String>>,
#[builder(skip)] #[builder(skip)]
_marker: PhantomData<T>, _marker: PhantomData<T>,
} }
@ -496,6 +526,13 @@ impl Default for UseColorModeOptions<&'static str, web_sys::Element> {
emit_auto: false, emit_auto: false,
transition_enabled: false, transition_enabled: false,
listen_to_storage_changes: true, listen_to_storage_changes: true,
ssr_color_header_getter: Rc::new(move || {
get_header!(
HeaderName::from_static("sec-ch-prefers-color-scheme"),
use_locale,
ssr_color_header_getter
)
}),
_marker: PhantomData, _marker: PhantomData,
} }
} }

View file

@ -59,12 +59,13 @@ where
.collect::<Vec<_>>(); .collect::<Vec<_>>();
const EMPTY_ERR_MSG: &str = "Empty supported list. You have to provide at least one locale in the `supported` parameter"; const EMPTY_ERR_MSG: &str = "Empty supported list. You have to provide at least one locale in the `supported` parameter";
assert!(!supported.is_empty(), "{}", EMPTY_ERR_MSG); assert!(!supported.is_empty(), "{}", EMPTY_ERR_MSG);
Signal::derive(move || { Signal::derive(move || {
let supported = supported.clone(); let supported = supported.clone();
client_locales.with(|clienht_locales| { client_locales.with(|client_locales| {
let mut first_supported = None; let mut first_supported = None;
for s in supported { for s in supported {
@ -72,7 +73,7 @@ where
first_supported = Some(s.clone()); first_supported = Some(s.clone());
} }
for client_locale in clienht_locales { for client_locale in client_locales {
let client_locale: LanguageIdentifier = client_locale let client_locale: LanguageIdentifier = client_locale
.parse() .parse()
.expect("Client should provide a list of valid unicode locales"); .expect("Client should provide a list of valid unicode locales");

View file

@ -39,7 +39,7 @@ use std::rc::Rc;
/// ### Bring your own header /// ### Bring your own header
/// ///
/// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking, /// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking,
/// you can provide your own way of reading and writing the cookie header value using the option /// you can provide your own way of reading the language header value using the option
/// [`crate::UseLocalesOptions::ssr_lang_header_getter`]. /// [`crate::UseLocalesOptions::ssr_lang_header_getter`].
pub fn use_locales() -> Signal<Vec<String>> { pub fn use_locales() -> Signal<Vec<String>> {
use_locales_with_options(UseLocalesOptions::default()) use_locales_with_options(UseLocalesOptions::default())

View file

@ -1,5 +1,7 @@
use crate::use_media_query; use crate::utils::get_header;
use default_struct_builder::DefaultBuilder;
use leptos::*; use leptos::*;
use std::rc::Rc;
/// Reactive [dark theme preference](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme). /// Reactive [dark theme preference](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).
/// ///
@ -20,12 +22,66 @@ use leptos::*;
/// ///
/// ## Server-Side Rendering /// ## Server-Side Rendering
/// ///
/// On the server this functions returns a Signal that is always `false`. /// On the server this will try to read the
/// [`Sec-CH-Prefers-Color-Scheme` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme)
/// to determine the color mode. If the header is not present it will return `ColorMode::Light`.
/// Please have a look at the linked documentation above for that header to see browser support
/// as well as potential server requirements.
///
/// > 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"`.
///
/// ### Bring your own header
///
/// In case you're neither using Axum, Actix nor Spin, or the default implementation is not to your liking,
/// you can provide your own way of reading the color scheme header value using the option
/// [`crate::UsePreferredDarkOptions::ssr_color_header_getter`].
/// ///
/// ## See also /// ## See also
/// ///
/// * [`fn@crate::use_media_query`] /// * [`fn@crate::use_media_query`]
/// * [`fn@crate::use_preferred_contrast`] /// * [`fn@crate::use_preferred_contrast`]
pub fn use_preferred_dark() -> Signal<bool> { pub fn use_preferred_dark() -> Signal<bool> {
use_media_query("(prefers-color-scheme: dark)") use_preferred_dark_with_options(Default::default())
}
/// Version of [`fn@crate::use_preferred_dark`] that accepts a `UsePreferredDarkOptions`.
pub fn use_preferred_dark_with_options(options: UsePreferredDarkOptions) -> Signal<bool> {
#[cfg(not(feature = "ssr"))]
{
let _ = options;
crate::use_media_query("(prefers-color-scheme: dark)")
}
#[cfg(feature = "ssr")]
{
Signal::derive(move || (options.ssr_color_header_getter)() == Some("dark".to_string()))
}
}
/// Options for [`fn@crate::use_preferred_dark_with_options`].
#[derive(DefaultBuilder)]
pub struct UsePreferredDarkOptions {
/// Getter function to return the string value of the
/// [`Sec-CH-Prefers-Color-Scheme`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme)
/// header.
/// When you use one of the features `"axum"`, `"actix"` or `"spin"` there's a valid default
/// implementation provided.
#[allow(dead_code)]
pub(crate) ssr_color_header_getter: Rc<dyn Fn() -> Option<String>>,
}
impl Default for UsePreferredDarkOptions {
fn default() -> Self {
Self {
ssr_color_header_getter: Rc::new(move || {
get_header!(
HeaderName::from_static("sec-ch-prefers-color-scheme"),
use_locale,
ssr_color_header_getter
)
}),
}
}
} }

View file

@ -1,6 +1,6 @@
macro_rules! get_header { macro_rules! get_header {
( (
$header_name:ident, $header_name:expr,
$function_name:ident, $function_name:ident,
$option_name:ident $option_name:ident
$(,)? $(,)?
@ -19,14 +19,19 @@ macro_rules! get_header {
); );
return None; return None;
} }
#[cfg(feature = "actix")] #[cfg(feature = "actix")]
const $header_name: http0_2::HeaderName = http0_2::header::$header_name; #[allow(unused_imports)]
use http0_2::{HeaderName, header::*};
#[cfg(any(feature = "axum", feature = "spin"))] #[cfg(any(feature = "axum", feature = "spin"))]
const $header_name: http1::HeaderName = http1::header::$header_name; #[allow(unused_imports)]
use http1::{HeaderName, header::*};
#[cfg(any(feature = "axum", feature = "actix", feature = "spin"))] #[cfg(any(feature = "axum", feature = "actix", feature = "spin"))]
crate::utils::header($header_name) {
let header_name = $header_name;
crate::utils::header(header_name)
}
} else { } else {
None None
} }