work related to 0.7 blog port
This commit is contained in:
parent
c29081b12a
commit
b41fde3ff9
|
@ -25,7 +25,7 @@ use futures::Stream;
|
||||||
pub use hydrate::*;
|
pub use hydrate::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
pub use ssr::*;
|
pub use ssr::*;
|
||||||
use std::{fmt::Debug, future::Future, pin::Pin, sync::OnceLock};
|
use std::{fmt::Debug, future::Future, pin::Pin};
|
||||||
|
|
||||||
/// Type alias for a boxed [`Future`].
|
/// Type alias for a boxed [`Future`].
|
||||||
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;
|
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;
|
||||||
|
|
|
@ -98,7 +98,8 @@ impl SharedContext for SsrSharedContext {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, data)| async move {
|
.map(|(id, data)| async move {
|
||||||
let data = data.await;
|
let data = data.await;
|
||||||
format!("__RESOLVED_RESOURCES[{}] = {data:?};", id.0)
|
let data = data.replace('<', "\\u003c");
|
||||||
|
format!("__RESOLVED_RESOURCES[{}] = {:?};", id.0, data)
|
||||||
})
|
})
|
||||||
.collect::<FuturesUnordered<_>>();
|
.collect::<FuturesUnordered<_>>();
|
||||||
|
|
||||||
|
|
|
@ -9,34 +9,33 @@ description = "Axum integrations for the Leptos web framework."
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
any_spawner = { workspace = true, features = ["tokio"] }
|
||||||
|
hydration_context = { workspace = true }
|
||||||
axum = { version = "0.7", default-features = false, features = [
|
axum = { version = "0.7", default-features = false, features = [
|
||||||
"matched-path",
|
"matched-path",
|
||||||
] }
|
] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
http-body-util = "0.1"
|
http-body-util = "0.1"
|
||||||
leptos = { workspace = true, features = ["ssr"] }
|
leptos = { workspace = true, features = ["nonce", "hydration"] }
|
||||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||||
leptos_macro = { workspace = true, features = ["axum"] }
|
leptos_macro = { workspace = true, features = ["axum"] }
|
||||||
leptos_meta = { workspace = true }
|
leptos_meta = { workspace = true }
|
||||||
leptos_router = { workspace = true, features = ["ssr"] }
|
routing = { workspace = true }
|
||||||
leptos_integration_utils = { workspace = true }
|
#leptos_integration_utils = { workspace = true }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", default-features = false }
|
tokio = { version = "1", default-features = false }
|
||||||
tokio-util = { version = "0.7", features = ["rt"] }
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
once_cell = "1.18"
|
once_cell = "1.18"
|
||||||
cfg-if = "1.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
tokio = { version = "1", features = ["net"] }
|
tokio = { version = "1", features = ["net"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
nonce = ["leptos/nonce"]
|
|
||||||
wasm = []
|
wasm = []
|
||||||
default = ["tokio/fs", "tokio/sync"]
|
default = ["tokio/fs", "tokio/sync"]
|
||||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
#experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -10,7 +10,7 @@ rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
leptos = { workspace = true, features = ["ssr"] }
|
leptos = { workspace = true }
|
||||||
leptos_hot_reload = { workspace = true }
|
leptos_hot_reload = { workspace = true }
|
||||||
leptos_meta = { workspace = true }
|
leptos_meta = { workspace = true }
|
||||||
leptos_config = { workspace = true }
|
leptos_config = { workspace = true }
|
||||||
|
|
|
@ -7,20 +7,23 @@ license = "MIT"
|
||||||
repository = "https://github.com/leptos-rs/leptos"
|
repository = "https://github.com/leptos-rs/leptos"
|
||||||
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
|
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
|
||||||
readme = "../README.md"
|
readme = "../README.md"
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
|
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
|
base64 = { version = "0.22", optional = true }
|
||||||
cfg-if = "1"
|
cfg-if = "1"
|
||||||
|
hydration_context = { workspace = true, optional = true }
|
||||||
leptos_dom = { workspace = true }
|
leptos_dom = { workspace = true }
|
||||||
leptos_macro = { workspace = true }
|
leptos_macro = { workspace = true }
|
||||||
leptos_reactive = { workspace = true }
|
leptos_reactive = { workspace = true }
|
||||||
leptos_server = { workspace = true }
|
leptos_server = { workspace = true }
|
||||||
leptos_config = { workspace = true }
|
leptos_config = { workspace = true }
|
||||||
leptos-spin-macro = { git = "https://github.com/fermyon/leptos-spin", optional = true }
|
leptos-spin-macro = { version = "0.1", optional = true }
|
||||||
oco = { workspace = true }
|
|
||||||
paste = "1"
|
paste = "1"
|
||||||
|
rand = { version = "0.8", optional = true }
|
||||||
reactive_graph = { workspace = true, features = ["serde"] }
|
reactive_graph = { workspace = true, features = ["serde"] }
|
||||||
tachys = { workspace = true, features = ["oco", "reactive_graph"] }
|
tachys = { workspace = true, features = ["reactive_graph"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
typed-builder = "0.18"
|
typed-builder = "0.18"
|
||||||
typed-builder-macro = "0.18"
|
typed-builder-macro = "0.18"
|
||||||
|
@ -37,7 +40,7 @@ web-sys = { version = "0.3.63", features = [
|
||||||
"ShadowRootInit",
|
"ShadowRootInit",
|
||||||
"ShadowRootMode",
|
"ShadowRootMode",
|
||||||
] }
|
] }
|
||||||
wasm-bindgen = { version = "0.2" }
|
wasm-bindgen = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serde"]
|
default = ["serde"]
|
||||||
|
@ -94,6 +97,8 @@ denylist = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"rkyv", # was causing clippy issues on nightly
|
"rkyv", # was causing clippy issues on nightly
|
||||||
"trace-component-props",
|
"trace-component-props",
|
||||||
|
"spin",
|
||||||
|
"experimental-islands",
|
||||||
]
|
]
|
||||||
skip_feature_sets = [
|
skip_feature_sets = [
|
||||||
[
|
[
|
||||||
|
|
|
@ -156,6 +156,8 @@ pub mod children;
|
||||||
pub mod component;
|
pub mod component;
|
||||||
mod for_loop;
|
mod for_loop;
|
||||||
mod hydration_scripts;
|
mod hydration_scripts;
|
||||||
|
#[cfg(feature = "nonce")]
|
||||||
|
pub mod nonce;
|
||||||
mod show;
|
mod show;
|
||||||
pub mod text_prop;
|
pub mod text_prop;
|
||||||
pub use for_loop::*;
|
pub use for_loop::*;
|
||||||
|
@ -165,7 +167,7 @@ pub use reactive_graph::{
|
||||||
self,
|
self,
|
||||||
signal::{arc_signal, create_signal, signal},
|
signal::{arc_signal, create_signal, signal},
|
||||||
};
|
};
|
||||||
pub use server_fn::error;
|
pub use server_fn::{self, error};
|
||||||
pub use show::*;
|
pub use show::*;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use typed_builder;
|
pub use typed_builder;
|
||||||
|
@ -175,12 +177,28 @@ mod into_view;
|
||||||
pub use into_view::IntoView;
|
pub use into_view::IntoView;
|
||||||
pub use leptos_dom;
|
pub use leptos_dom;
|
||||||
pub use tachys;
|
pub use tachys;
|
||||||
pub mod logging;
|
pub mod mount;
|
||||||
mod mount;
|
|
||||||
pub use any_spawner::Executor;
|
pub use any_spawner::Executor;
|
||||||
pub use mount::*;
|
pub use leptos_config as config;
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
pub use mount::hydrate_body;
|
||||||
|
pub use mount::mount_to_body;
|
||||||
pub use oco;
|
pub use oco;
|
||||||
|
|
||||||
|
pub mod context {
|
||||||
|
pub use reactive_graph::owner::{provide_context, use_context};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub mod server {
|
||||||
|
pub use leptos_server::{ArcResource, Resource};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||||
|
pub mod logging {
|
||||||
|
pub use leptos_dom::{debug_warn, error, log, warn};
|
||||||
|
}
|
||||||
|
|
||||||
/*mod additional_attributes;
|
/*mod additional_attributes;
|
||||||
pub use additional_attributes::*;
|
pub use additional_attributes::*;
|
||||||
mod await_;
|
mod await_;
|
||||||
|
|
|
@ -4,11 +4,59 @@ use reactive_graph::owner::Owner;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use tachys::{
|
use tachys::{
|
||||||
dom::body,
|
dom::body,
|
||||||
|
hydration::Cursor,
|
||||||
renderer::{dom::Dom, Renderer},
|
renderer::{dom::Dom, Renderer},
|
||||||
view::{Mountable, Render},
|
view::{Mountable, PositionState, Render, RenderHtml},
|
||||||
};
|
};
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::HtmlElement;
|
use web_sys::HtmlElement;
|
||||||
|
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
/// Hydrates the app described by the provided function, starting at `<body>`.
|
||||||
|
pub fn hydrate_body<F, N>(f: F)
|
||||||
|
where
|
||||||
|
F: FnOnce() -> N + 'static,
|
||||||
|
N: IntoView,
|
||||||
|
{
|
||||||
|
let owner = hydrate_from(body(), f);
|
||||||
|
owner.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
/// Runs the provided closure and mounts the result to the provided element.
|
||||||
|
pub fn hydrate_from<F, N>(
|
||||||
|
parent: HtmlElement,
|
||||||
|
f: F,
|
||||||
|
) -> UnmountHandle<N::State, Dom>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> N + 'static,
|
||||||
|
N: IntoView,
|
||||||
|
{
|
||||||
|
use hydration_context::HydrateSharedContext;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
// use wasm-bindgen-futures to drive the reactive system
|
||||||
|
Executor::init_wasm_bindgen();
|
||||||
|
|
||||||
|
// create a new reactive owner and use it as the root node to run the app
|
||||||
|
let owner = Owner::new_root(Arc::new(HydrateSharedContext::new()));
|
||||||
|
let mountable = owner.with(move || {
|
||||||
|
let view = f().into_view();
|
||||||
|
view.hydrate::<true>(
|
||||||
|
&Cursor::new(parent.unchecked_into()),
|
||||||
|
&PositionState::default(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// returns a handle that owns the owner
|
||||||
|
// when this is dropped, it will clean up the reactive system and unmount the view
|
||||||
|
UnmountHandle {
|
||||||
|
owner,
|
||||||
|
mountable,
|
||||||
|
rndr: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs the provided closure and mounts the result to the `<body>`.
|
/// Runs the provided closure and mounts the result to the `<body>`.
|
||||||
pub fn mount_to_body<F, N>(f: F)
|
pub fn mount_to_body<F, N>(f: F)
|
||||||
where
|
where
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
use crate::context::{provide_context, use_context};
|
||||||
|
use base64::{
|
||||||
|
alphabet,
|
||||||
|
engine::{self, general_purpose},
|
||||||
|
Engine,
|
||||||
|
};
|
||||||
|
use rand::{thread_rng, RngCore};
|
||||||
|
use std::{fmt::Display, ops::Deref, sync::Arc};
|
||||||
|
|
||||||
|
/// A cryptographic nonce ("number used once") which can be
|
||||||
|
/// used by Content Security Policy to determine whether or not a given
|
||||||
|
/// resource will be allowed to load.
|
||||||
|
///
|
||||||
|
/// When the `nonce` feature is enabled on one of the server integrations,
|
||||||
|
/// a nonce is generated during server rendering and added to all inline
|
||||||
|
/// scripts used for HTML streaming and resource loading.
|
||||||
|
///
|
||||||
|
/// The nonce being used during the current server response can be
|
||||||
|
/// accessed using [`use_nonce`].
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// #[component]
|
||||||
|
/// pub fn App() -> impl IntoView {
|
||||||
|
/// provide_meta_context;
|
||||||
|
///
|
||||||
|
/// view! {
|
||||||
|
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||||
|
/// <Meta
|
||||||
|
/// http_equiv="Content-Security-Policy"
|
||||||
|
/// content=move || {
|
||||||
|
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||||
|
/// use_nonce()
|
||||||
|
/// .map(|nonce| {
|
||||||
|
/// format!(
|
||||||
|
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||||
|
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||||
|
/// )
|
||||||
|
/// })
|
||||||
|
/// .unwrap_or_default()
|
||||||
|
/// }
|
||||||
|
/// />
|
||||||
|
/// // manually insert nonce during SSR on inline script
|
||||||
|
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
|
||||||
|
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||||
|
/// <Style>"body { color: blue; }"</Style>
|
||||||
|
/// <p>"Test"</p>
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Nonce(pub(crate) Arc<str>);
|
||||||
|
|
||||||
|
impl Deref for Nonce {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Nonce {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO implement Attribute
|
||||||
|
|
||||||
|
/// Accesses the nonce that has been generated during the current
|
||||||
|
/// server response. This can be added to inline `<script>` and
|
||||||
|
/// `<style>` tags for compatibility with a Content Security Policy.
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// #[component]
|
||||||
|
/// pub fn App() -> impl IntoView {
|
||||||
|
/// provide_meta_context;
|
||||||
|
///
|
||||||
|
/// view! {
|
||||||
|
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||||
|
/// <Meta
|
||||||
|
/// http_equiv="Content-Security-Policy"
|
||||||
|
/// content=move || {
|
||||||
|
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||||
|
/// use_nonce()
|
||||||
|
/// .map(|nonce| {
|
||||||
|
/// format!(
|
||||||
|
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||||
|
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||||
|
/// )
|
||||||
|
/// })
|
||||||
|
/// .unwrap_or_default()
|
||||||
|
/// }
|
||||||
|
/// />
|
||||||
|
/// // manually insert nonce during SSR on inline script
|
||||||
|
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
|
||||||
|
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||||
|
/// <Style>"body { color: blue; }"</Style>
|
||||||
|
/// <p>"Test"</p>
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn use_nonce() -> Option<Nonce> {
|
||||||
|
use_context::<Nonce>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a nonce and provides it via context.
|
||||||
|
pub fn provide_nonce() {
|
||||||
|
provide_context(Nonce::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
const NONCE_ENGINE: engine::GeneralPurpose =
|
||||||
|
engine::GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD);
|
||||||
|
|
||||||
|
impl Nonce {
|
||||||
|
/// Generates a new nonce from 16 bytes (128 bits) of random data.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut thread_rng = thread_rng();
|
||||||
|
let mut bytes = [0; 16];
|
||||||
|
thread_rng.fill_bytes(&mut bytes);
|
||||||
|
Nonce(NONCE_ENGINE.encode(bytes).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Nonce {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,10 @@ use web_sys::HtmlElement;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub use tachys::html::event as events;
|
pub use tachys::html::event as events;
|
||||||
|
|
||||||
|
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||||
|
#[macro_use]
|
||||||
|
pub mod logging;
|
||||||
|
|
||||||
/*#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
/*#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::is_server;
|
//! Utilities for simple isomorphic logging to the console or terminal.
|
||||||
use cfg_if::cfg_if;
|
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
/// Uses `println!()`-style formatting to log something to the console (in the browser)
|
/// Uses `println!()`-style formatting to log something to the console (in the browser)
|
||||||
|
@ -41,11 +41,18 @@ macro_rules! debug_warn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn log_to_stdout() -> bool {
|
||||||
|
cfg!(not(all(
|
||||||
|
target_arch = "wasm32",
|
||||||
|
not(any(target_os = "emscripten", target_os = "wasi"))
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Log a string to the console (in the browser)
|
/// Log a string to the console (in the browser)
|
||||||
/// or via `println!()` (if not in the browser).
|
/// or via `println!()` (if not in the browser).
|
||||||
pub fn console_log(s: &str) {
|
pub fn console_log(s: &str) {
|
||||||
#[allow(clippy::print_stdout)]
|
#[allow(clippy::print_stdout)]
|
||||||
if is_server() {
|
if log_to_stdout() {
|
||||||
println!("{s}");
|
println!("{s}");
|
||||||
} else {
|
} else {
|
||||||
web_sys::console::log_1(&JsValue::from_str(s));
|
web_sys::console::log_1(&JsValue::from_str(s));
|
||||||
|
@ -55,7 +62,7 @@ pub fn console_log(s: &str) {
|
||||||
/// Log a warning to the console (in the browser)
|
/// Log a warning to the console (in the browser)
|
||||||
/// or via `println!()` (if not in the browser).
|
/// or via `println!()` (if not in the browser).
|
||||||
pub fn console_warn(s: &str) {
|
pub fn console_warn(s: &str) {
|
||||||
if is_server() {
|
if log_to_stdout() {
|
||||||
eprintln!("{s}");
|
eprintln!("{s}");
|
||||||
} else {
|
} else {
|
||||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||||
|
@ -64,8 +71,9 @@ pub fn console_warn(s: &str) {
|
||||||
|
|
||||||
/// Log an error to the console (in the browser)
|
/// Log an error to the console (in the browser)
|
||||||
/// or via `println!()` (if not in the browser).
|
/// or via `println!()` (if not in the browser).
|
||||||
|
#[inline(always)]
|
||||||
pub fn console_error(s: &str) {
|
pub fn console_error(s: &str) {
|
||||||
if is_server() {
|
if log_to_stdout() {
|
||||||
eprintln!("{s}");
|
eprintln!("{s}");
|
||||||
} else {
|
} else {
|
||||||
web_sys::console::error_1(&JsValue::from_str(s));
|
web_sys::console::error_1(&JsValue::from_str(s));
|
||||||
|
@ -74,16 +82,19 @@ pub fn console_error(s: &str) {
|
||||||
|
|
||||||
/// Log an error to the console (in the browser)
|
/// Log an error to the console (in the browser)
|
||||||
/// or via `println!()` (if not in the browser), but only in a debug build.
|
/// or via `println!()` (if not in the browser), but only in a debug build.
|
||||||
|
#[inline(always)]
|
||||||
pub fn console_debug_warn(s: &str) {
|
pub fn console_debug_warn(s: &str) {
|
||||||
cfg_if! {
|
#[cfg(debug_assertions)]
|
||||||
if #[cfg(debug_assertions)] {
|
{
|
||||||
if is_server() {
|
if log_to_stdout() {
|
||||||
eprintln!("{s}");
|
eprintln!("{s}");
|
||||||
} else {
|
|
||||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let _ = s;
|
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
let _ = s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,14 +10,27 @@ readme = "../README.md"
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reactive_graph = { workspace = true }
|
hydration_context = { workspace = true }
|
||||||
leptos_macro = { workspace = true }
|
reactive_graph = { workspace = true, features = ["hydration"] }
|
||||||
|
#leptos_macro = { workspace = true }
|
||||||
server_fn = { workspace = true }
|
server_fn = { workspace = true }
|
||||||
lazy_static = "1"
|
#lazy_static = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tracing = "0.1"
|
tracing = { version = "0.1", optional = true }
|
||||||
inventory = "0.3"
|
#inventory = "0.3"
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
# serialization formats
|
||||||
|
serde = { version = "1", optional = true }
|
||||||
|
serde_json = { version = "1", optional = true }
|
||||||
|
miniserde = { version = "0.1", optional = true }
|
||||||
|
rkyv = { version = "0.7", optional = true, features = [
|
||||||
|
"validation",
|
||||||
|
"uuid",
|
||||||
|
"strict",
|
||||||
|
] }
|
||||||
|
serde-lite = { version = "0.5", optional = true }
|
||||||
|
base64 = { version = "0.22", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
leptos = { path = "../leptos" }
|
leptos = { path = "../leptos" }
|
||||||
|
@ -25,6 +38,9 @@ leptos = { path = "../leptos" }
|
||||||
[features]
|
[features]
|
||||||
default-tls = ["server_fn/default-tls"]
|
default-tls = ["server_fn/default-tls"]
|
||||||
rustls = ["server_fn/rustls"]
|
rustls = ["server_fn/rustls"]
|
||||||
|
hydration = ["reactive_graph/hydration", "dep:serde", "dep:serde_json"]
|
||||||
|
rkyv = ["dep:rkyv", "dep:base64"]
|
||||||
|
tracing = ["dep:tracing"]
|
||||||
|
|
||||||
[package.metadata.cargo-all-features]
|
[package.metadata.cargo-all-features]
|
||||||
denylist = ["nightly"]
|
denylist = ["nightly"]
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
//#![deny(missing_docs)]
|
//#![deny(missing_docs)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
mod resource;
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub mod serializers;
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub use resource::*;
|
||||||
|
|
||||||
////! # Leptos Server Functions
|
////! # Leptos Server Functions
|
||||||
////!
|
////!
|
||||||
////! This package is based on a simple idea: sometimes it’s useful to write functions
|
////! This package is based on a simple idea: sometimes it’s useful to write functions
|
||||||
|
|
|
@ -0,0 +1,388 @@
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
use crate::serializers::Miniserde;
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
use crate::serializers::Rkyv;
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
use crate::serializers::SerdeLite;
|
||||||
|
use crate::serializers::{SerdeJson, SerializableData, Serializer, Str};
|
||||||
|
use core::{fmt::Debug, marker::PhantomData};
|
||||||
|
use futures::Future;
|
||||||
|
use hydration_context::SerializedDataId;
|
||||||
|
use reactive_graph::{
|
||||||
|
computed::{ArcAsyncDerived, AsyncDerived, AsyncDerivedFuture, AsyncState},
|
||||||
|
owner::Owner,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use std::{future::IntoFuture, ops::Deref};
|
||||||
|
|
||||||
|
pub struct ArcResource<T, Ser> {
|
||||||
|
ser: PhantomData<Ser>,
|
||||||
|
data: ArcAsyncDerived<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> Deref for ArcResource<T, Ser> {
|
||||||
|
type Target = ArcAsyncDerived<T>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ArcResource<T, Str>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Str>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ArcResource<T, SerdeJson>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeJson>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_serde<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
impl<T> ArcResource<T, Miniserde>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Miniserde>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_miniserde<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
impl<T> ArcResource<T, SerdeLite>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeLite>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_serde_lite<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
impl<T> ArcResource<T, SerdeLite>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeLite>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_rkyv<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> ArcResource<T, Ser>
|
||||||
|
where
|
||||||
|
Ser: Serializer,
|
||||||
|
T: Debug + SerializableData<Ser>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_with_encoding<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> ArcResource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Debug + Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
let shared_context = Owner::current_shared_context();
|
||||||
|
let id = shared_context
|
||||||
|
.as_ref()
|
||||||
|
.map(|sc| sc.next_id())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let initial = Self::initial_value(&id);
|
||||||
|
|
||||||
|
let data = ArcAsyncDerived::new_with_initial(initial, fun);
|
||||||
|
|
||||||
|
if let Some(shared_context) = shared_context {
|
||||||
|
let value = data.clone();
|
||||||
|
let ready_fut = data.ready();
|
||||||
|
|
||||||
|
shared_context.write_async(
|
||||||
|
id,
|
||||||
|
Box::pin(async move {
|
||||||
|
ready_fut.await;
|
||||||
|
value
|
||||||
|
.with_untracked(|data| match &data {
|
||||||
|
AsyncState::Complete(val) => val.ser(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.unwrap() // TODO handle
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArcResource {
|
||||||
|
ser: PhantomData,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn initial_value(id: &SerializedDataId) -> AsyncState<T> {
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
{
|
||||||
|
let shared_context = Owner::current_shared_context();
|
||||||
|
if let Some(shared_context) = shared_context {
|
||||||
|
let value = shared_context.read_data(id);
|
||||||
|
if let Some(value) = value {
|
||||||
|
match T::de(&value) {
|
||||||
|
Ok(value) => return AsyncState::Complete(value),
|
||||||
|
Err(e) => {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::error!(
|
||||||
|
"couldn't deserialize from {value:?}: {e:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AsyncState::Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> IntoFuture for ArcResource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Clone + 'static,
|
||||||
|
{
|
||||||
|
type Output = T;
|
||||||
|
type IntoFuture = AsyncDerivedFuture<T>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
self.data.into_future()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Resource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
ser: PhantomData<Ser>,
|
||||||
|
data: AsyncDerived<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + 'static, Ser> Copy for Resource<T, Ser> {}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + 'static, Ser> Clone for Resource<T, Ser> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> Deref for Resource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Target = AsyncDerived<T>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Resource<T, Str>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Str> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Resource<T, SerdeJson>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeJson> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_serde<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
impl<T> Resource<T, Miniserde>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Miniserde> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_miniserde<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
impl<T> Resource<T, SerdeLite>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeLite> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_serde_lite<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
impl<T> Resource<T, Rkyv>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Rkyv> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_rkyv<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> Resource<T, Ser>
|
||||||
|
where
|
||||||
|
Ser: Serializer,
|
||||||
|
T: Debug + SerializableData<Ser> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_with_encoding<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Resource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
let shared_context = Owner::current_shared_context();
|
||||||
|
let id = shared_context
|
||||||
|
.as_ref()
|
||||||
|
.map(|sc| sc.next_id())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let initial = Self::initial_value(&id);
|
||||||
|
|
||||||
|
let data = AsyncDerived::new_with_initial(initial, fun);
|
||||||
|
|
||||||
|
if let Some(shared_context) = shared_context {
|
||||||
|
let value = data;
|
||||||
|
let ready_fut = data.ready();
|
||||||
|
|
||||||
|
shared_context.write_async(
|
||||||
|
id,
|
||||||
|
Box::pin(async move {
|
||||||
|
ready_fut.await;
|
||||||
|
value
|
||||||
|
.with_untracked(|data| match &data {
|
||||||
|
AsyncState::Complete(val) => val.ser(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.unwrap() // TODO handle
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource {
|
||||||
|
ser: PhantomData,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn initial_value(id: &SerializedDataId) -> AsyncState<T> {
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
{
|
||||||
|
let shared_context = Owner::current_shared_context();
|
||||||
|
if let Some(shared_context) = shared_context {
|
||||||
|
let value = shared_context.read_data(id);
|
||||||
|
if let Some(value) = value {
|
||||||
|
match T::de(&value) {
|
||||||
|
Ok(value) => return AsyncState::Complete(value),
|
||||||
|
Err(e) => {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::error!(
|
||||||
|
"couldn't deserialize from {value:?}: {e:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AsyncState::Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> IntoFuture for Resource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Output = T;
|
||||||
|
type IntoFuture = AsyncDerivedFuture<T>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
self.data.into_future()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
use core::str::FromStr;
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
|
pub trait SerializableData<Ser: Serializer>: Sized {
|
||||||
|
type SerErr;
|
||||||
|
type DeErr;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr>;
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Serializer {}
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes using [`ToString`] and deserializes
|
||||||
|
/// using [`FromStr`](core::str::FromStr).
|
||||||
|
pub struct Str;
|
||||||
|
|
||||||
|
impl Serializer for Str {}
|
||||||
|
|
||||||
|
impl<T> SerializableData<Str> for T
|
||||||
|
where
|
||||||
|
T: ToString + FromStr,
|
||||||
|
{
|
||||||
|
type SerErr = ();
|
||||||
|
type DeErr = <T as FromStr>::Err;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
Ok(self.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
T::from_str(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes using [`serde_json`].
|
||||||
|
pub struct SerdeJson;
|
||||||
|
|
||||||
|
impl Serializer for SerdeJson {}
|
||||||
|
|
||||||
|
impl<T> SerializableData<SerdeJson> for T
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Serialize,
|
||||||
|
{
|
||||||
|
type SerErr = serde_json::Error;
|
||||||
|
type DeErr = serde_json::Error;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
serde_json::to_string(&self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
serde_json::from_str(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
mod miniserde {
|
||||||
|
use super::{SerializableData, Serializer};
|
||||||
|
use miniserde::{json, Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes and deserializes using [`miniserde`].
|
||||||
|
pub struct Miniserde;
|
||||||
|
|
||||||
|
impl Serializer for Miniserde {}
|
||||||
|
|
||||||
|
impl<T> SerializableData<Miniserde> for T
|
||||||
|
where
|
||||||
|
T: Deserialize + Serialize,
|
||||||
|
{
|
||||||
|
type SerErr = ();
|
||||||
|
type DeErr = miniserde::Error;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
Ok(json::to_string(&self))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
json::from_str(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
pub use miniserde::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
mod serde_lite {
|
||||||
|
use super::{SerializableData, Serializer};
|
||||||
|
use serde_lite::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SerdeLiteError {
|
||||||
|
#[error("serde_lite error {0:?}")]
|
||||||
|
SerdeLite(serde_lite::Error),
|
||||||
|
#[error("serde_json error {0:?}")]
|
||||||
|
SerdeJson(serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_lite::Error> for SerdeLiteError {
|
||||||
|
fn from(value: serde_lite::Error) -> Self {
|
||||||
|
SerdeLiteError::SerdeLite(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for SerdeLiteError {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
SerdeLiteError::SerdeJson(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes and deserializes using [`serde_lite`].
|
||||||
|
pub struct SerdeLite;
|
||||||
|
|
||||||
|
impl Serializer for SerdeLite {}
|
||||||
|
|
||||||
|
impl<T> SerializableData<SerdeLite> for T
|
||||||
|
where
|
||||||
|
T: Deserialize + Serialize,
|
||||||
|
{
|
||||||
|
type SerErr = SerdeLiteError;
|
||||||
|
type DeErr = SerdeLiteError;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
let intermediate = self.serialize()?;
|
||||||
|
Ok(serde_json::to_string(&intermediate)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
let intermediate = serde_json::from_str(data)?;
|
||||||
|
Ok(Self::deserialize(&intermediate)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
pub use serde_lite::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
mod rkyv {
|
||||||
|
use super::{SerializableData, Serializer};
|
||||||
|
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
|
||||||
|
use rkyv::{
|
||||||
|
de::deserializers::SharedDeserializeMap,
|
||||||
|
ser::serializers::AllocSerializer,
|
||||||
|
validation::validators::DefaultValidator, Archive, CheckBytes,
|
||||||
|
Deserialize, Serialize,
|
||||||
|
};
|
||||||
|
use std::{error::Error, sync::Arc};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes and deserializes using [`rkyv`].
|
||||||
|
pub struct Rkyv;
|
||||||
|
|
||||||
|
impl Serializer for Rkyv {}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RkyvError {
|
||||||
|
#[error("rkyv error {0:?}")]
|
||||||
|
Rkyv(Arc<dyn Error>),
|
||||||
|
#[error("base64 error {0:?}")]
|
||||||
|
Base64Decode(base64::DecodeError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<dyn Error>> for RkyvError {
|
||||||
|
fn from(value: Arc<dyn Error>) -> Self {
|
||||||
|
RkyvError::Rkyv(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<base64::DecodeError> for RkyvError {
|
||||||
|
fn from(value: base64::DecodeError) -> Self {
|
||||||
|
RkyvError::Base64Decode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SerializableData<Rkyv> for T
|
||||||
|
where
|
||||||
|
T: Serialize<AllocSerializer<1024>>,
|
||||||
|
T: Archive,
|
||||||
|
T::Archived: for<'b> CheckBytes<DefaultValidator<'b>>
|
||||||
|
+ Deserialize<T, SharedDeserializeMap>,
|
||||||
|
{
|
||||||
|
type SerErr = RkyvError;
|
||||||
|
type DeErr = RkyvError;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
let bytes = rkyv::to_bytes::<T, 1024>(self)
|
||||||
|
.map_err(|e| Arc::new(e) as Arc<dyn Error>)?;
|
||||||
|
Ok(STANDARD_NO_PAD.encode(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
let bytes = STANDARD_NO_PAD.decode(data.as_bytes())?;
|
||||||
|
Ok(rkyv::from_bytes::<T>(&bytes)
|
||||||
|
.map_err(|e| Arc::new(e) as Arc<dyn Error>)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
pub use rkyv::*;
|
|
@ -16,6 +16,7 @@ indexmap = "2"
|
||||||
send_wrapper = "0.6.0"
|
send_wrapper = "0.6.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
futures = "0.3.30"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
|
|
|
@ -7,9 +7,14 @@ use leptos::{
|
||||||
tachys::{
|
tachys::{
|
||||||
dom::document,
|
dom::document,
|
||||||
error::Result,
|
error::Result,
|
||||||
html::attribute::{
|
html::{
|
||||||
any_attribute::{AnyAttribute, AnyAttributeState},
|
attribute::{
|
||||||
Attribute,
|
any_attribute::{
|
||||||
|
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
|
||||||
|
},
|
||||||
|
Attribute,
|
||||||
|
},
|
||||||
|
class,
|
||||||
},
|
},
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
reactive_graph::RenderEffectState,
|
reactive_graph::RenderEffectState,
|
||||||
|
@ -57,10 +62,17 @@ use web_sys::HtmlElement;
|
||||||
/// ```
|
/// ```
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Body(
|
pub fn Body(
|
||||||
|
/// The `class` attribute on the `<body>`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
mut class: Option<TextProp>,
|
||||||
/// Arbitrary attributes to add to the `<body>`.
|
/// Arbitrary attributes to add to the `<body>`.
|
||||||
#[prop(attrs)]
|
#[prop(attrs)]
|
||||||
mut attributes: Vec<AnyAttribute<Dom>>,
|
mut attributes: Vec<AnyAttribute<Dom>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
if let Some(value) = class.take() {
|
||||||
|
let value = class::class(move || value.get());
|
||||||
|
attributes.push(value.into_any_attr());
|
||||||
|
}
|
||||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||||
let mut meta = meta.inner.write().or_poisoned();
|
let mut meta = meta.inner.write().or_poisoned();
|
||||||
// if we are server rendering, we will not actually use these values via RenderHtml
|
// if we are server rendering, we will not actually use these values via RenderHtml
|
||||||
|
|
|
@ -7,9 +7,15 @@ use leptos::{
|
||||||
tachys::{
|
tachys::{
|
||||||
dom::document,
|
dom::document,
|
||||||
error::Result,
|
error::Result,
|
||||||
html::attribute::{
|
html::{
|
||||||
any_attribute::{AnyAttribute, AnyAttributeState},
|
attribute::{
|
||||||
Attribute,
|
self,
|
||||||
|
any_attribute::{
|
||||||
|
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
|
||||||
|
},
|
||||||
|
Attribute,
|
||||||
|
},
|
||||||
|
class,
|
||||||
},
|
},
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
reactive_graph::RenderEffectState,
|
reactive_graph::RenderEffectState,
|
||||||
|
@ -54,10 +60,30 @@ use web_sys::{Element, HtmlElement};
|
||||||
/// ```
|
/// ```
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Html(
|
pub fn Html(
|
||||||
|
/// The `lang` attribute on the `<html>`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
mut lang: Option<TextProp>,
|
||||||
|
/// The `dir` attribute on the `<html>`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
mut dir: Option<TextProp>,
|
||||||
|
/// The `class` attribute on the `<html>`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
mut class: Option<TextProp>,
|
||||||
/// Arbitrary attributes to add to the `<html>`
|
/// Arbitrary attributes to add to the `<html>`
|
||||||
#[prop(attrs)]
|
#[prop(attrs)]
|
||||||
mut attributes: Vec<AnyAttribute<Dom>>,
|
mut attributes: Vec<AnyAttribute<Dom>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
attributes.extend(
|
||||||
|
lang.take()
|
||||||
|
.map(|value| attribute::lang(move || value.get()).into_any_attr())
|
||||||
|
.into_iter()
|
||||||
|
.chain(dir.take().map(|value| {
|
||||||
|
attribute::dir(move || value.get()).into_any_attr()
|
||||||
|
}))
|
||||||
|
.chain(class.take().map(|value| {
|
||||||
|
class::class(move || value.get()).into_any_attr()
|
||||||
|
})),
|
||||||
|
);
|
||||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||||
let mut meta = meta.inner.write().or_poisoned();
|
let mut meta = meta.inner.write().or_poisoned();
|
||||||
// if we are server rendering, we will not actually use these values via RenderHtml
|
// if we are server rendering, we will not actually use these values via RenderHtml
|
||||||
|
|
|
@ -47,9 +47,10 @@
|
||||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||||
//! which mode your app is operating in.
|
//! which mode your app is operating in.
|
||||||
|
|
||||||
use indexmap::IndexMap;
|
use futures::{Stream, StreamExt};
|
||||||
use leptos::{
|
use leptos::{
|
||||||
component, debug_warn,
|
component,
|
||||||
|
logging::debug_warn,
|
||||||
reactive_graph::owner::{provide_context, use_context},
|
reactive_graph::owner::{provide_context, use_context},
|
||||||
tachys::{
|
tachys::{
|
||||||
dom::document,
|
dom::document,
|
||||||
|
@ -67,13 +68,12 @@ use once_cell::sync::Lazy;
|
||||||
use or_poisoned::OrPoisoned;
|
use or_poisoned::OrPoisoned;
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
rc::Rc,
|
pin::Pin,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{HtmlHeadElement, Node};
|
use web_sys::HtmlHeadElement;
|
||||||
|
|
||||||
mod body;
|
mod body;
|
||||||
mod html;
|
mod html;
|
||||||
|
@ -157,7 +157,61 @@ pub struct ServerMetaContext {
|
||||||
pub(crate) title: TitleContext,
|
pub(crate) title: TitleContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
impl ServerMetaContext {
|
||||||
|
/// Consumes the metadata, injecting it into the the first chunk of an HTML stream in the
|
||||||
|
/// appropriate place.
|
||||||
|
///
|
||||||
|
/// This means that only meta tags rendered during the first chunk of the stream will be
|
||||||
|
/// included.
|
||||||
|
pub async fn inject_meta_context(
|
||||||
|
self,
|
||||||
|
mut stream: impl Stream<Item = String> + Send + Sync + Unpin,
|
||||||
|
) -> impl Stream<Item = String> + Send + Sync {
|
||||||
|
let mut first_chunk = stream.next().await.unwrap_or_default();
|
||||||
|
|
||||||
|
let meta_buf =
|
||||||
|
std::mem::take(&mut self.inner.write().or_poisoned().head_html);
|
||||||
|
|
||||||
|
let title = self.title.as_string();
|
||||||
|
let title_len = title
|
||||||
|
.as_ref()
|
||||||
|
.map(|n| "<title>".len() + n.len() + "</title>".len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let modified_chunk = if title_len == 0 && meta_buf.is_empty() {
|
||||||
|
first_chunk
|
||||||
|
} else {
|
||||||
|
let mut buf = String::with_capacity(
|
||||||
|
first_chunk.len() + title_len + meta_buf.len(),
|
||||||
|
);
|
||||||
|
let head_loc = first_chunk
|
||||||
|
.find("</head>")
|
||||||
|
.expect("you are using leptos_meta without a </head> tag");
|
||||||
|
let marker_loc =
|
||||||
|
first_chunk.find("<!--HEAD-->").unwrap_or_else(|| {
|
||||||
|
first_chunk.find("</head>").unwrap_or(head_loc)
|
||||||
|
});
|
||||||
|
let (before_marker, after_marker) =
|
||||||
|
first_chunk.split_at_mut(marker_loc);
|
||||||
|
let (before_head_close, after_head) =
|
||||||
|
after_marker.split_at_mut(head_loc - marker_loc);
|
||||||
|
buf.push_str(before_marker);
|
||||||
|
if let Some(title) = title {
|
||||||
|
buf.push_str("<title>");
|
||||||
|
buf.push_str(&title);
|
||||||
|
buf.push_str("</title>");
|
||||||
|
}
|
||||||
|
buf.push_str(before_head_close);
|
||||||
|
buf.push_str(&meta_buf);
|
||||||
|
buf.push_str(after_head);
|
||||||
|
buf
|
||||||
|
};
|
||||||
|
|
||||||
|
futures::stream::once(async move { modified_chunk }).chain(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
struct ServerMetaContextInner {
|
struct ServerMetaContextInner {
|
||||||
/*/// Metadata associated with the `<html>` element
|
/*/// Metadata associated with the `<html>` element
|
||||||
pub html: HtmlContext,
|
pub html: HtmlContext,
|
||||||
|
@ -173,7 +227,9 @@ struct ServerMetaContextInner {
|
||||||
|
|
||||||
impl Debug for ServerMetaContext {
|
impl Debug for ServerMetaContext {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("ServerMetaContext").finish_non_exhaustive()
|
f.debug_struct("ServerMetaContext")
|
||||||
|
.field("inner", &self.inner)
|
||||||
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,6 +423,7 @@ pub fn MetaTags() -> impl IntoView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct MetaTagsView {
|
struct MetaTagsView {
|
||||||
context: ServerMetaContext,
|
context: ServerMetaContext,
|
||||||
}
|
}
|
||||||
|
@ -399,14 +456,7 @@ impl RenderHtml<Dom> for MetaTagsView {
|
||||||
const MIN_LENGTH: usize = 0;
|
const MIN_LENGTH: usize = 0;
|
||||||
|
|
||||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||||
if let Some(title) = self.context.title.as_string() {
|
buf.push_str("<!--HEAD-->");
|
||||||
buf.reserve(15 + title.len());
|
|
||||||
buf.push_str("<title>");
|
|
||||||
buf.push_str(&title);
|
|
||||||
buf.push_str("</title>");
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.push_str(&self.context.inner.write().or_poisoned().head_html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
|
|
@ -7,6 +7,7 @@ version.workspace = true
|
||||||
any_spawner = { workspace = true }
|
any_spawner = { workspace = true }
|
||||||
or_poisoned = { workspace = true }
|
or_poisoned = { workspace = true }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
hydration_context = { workspace = true, optional = true }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
rustc-hash = "1.1.0"
|
rustc-hash = "1.1.0"
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
@ -23,6 +24,7 @@ any_spawner = { workspace = true, features = ["tokio"] }
|
||||||
nightly = []
|
nightly = []
|
||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
tracing = ["dep:tracing"]
|
tracing = ["dep:tracing"]
|
||||||
|
hydration = ["dep:hydration_context"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|
|
@ -71,7 +71,7 @@ impl<T> DefinedAt for ArcAsyncDerived<T> {
|
||||||
|
|
||||||
// This helps create a derived async signal.
|
// This helps create a derived async signal.
|
||||||
// It needs to be implemented as a macro because it needs to be flexible over
|
// It needs to be implemented as a macro because it needs to be flexible over
|
||||||
// whether `fun` returns a `Future` that is `Send + Sync`. Doing it as a function would,
|
// whether `fun` returns a `Future` that is `Send`. Doing it as a function would,
|
||||||
// as far as I can tell, require repeating most of the function body.
|
// as far as I can tell, require repeating most of the function body.
|
||||||
macro_rules! spawn_derived {
|
macro_rules! spawn_derived {
|
||||||
($spawner:expr, $initial:ident, $fun:ident) => {{
|
($spawner:expr, $initial:ident, $fun:ident) => {{
|
||||||
|
@ -174,7 +174,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||||
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + Sync + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Self::new_with_initial(AsyncState::Loading, fun)
|
Self::new_with_initial(AsyncState::Loading, fun)
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + Sync + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
spawn_derived!(Executor::spawn, initial_value, fun)
|
spawn_derived!(Executor::spawn, initial_value, fun)
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + Sync + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl<T> Debug for MemoInner<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> MemoInner<T> {
|
impl<T: 'static> MemoInner<T> {
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
fun: Arc<dyn Fn(Option<&T>) -> T + Send + Sync>,
|
fun: Arc<dyn Fn(Option<&T>) -> T + Send + Sync>,
|
||||||
|
@ -49,7 +49,7 @@ impl<T: Send + Sync + 'static> MemoInner<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> ReactiveNode for RwLock<MemoInner<T>> {
|
impl<T: 'static> ReactiveNode for RwLock<MemoInner<T>> {
|
||||||
fn mark_dirty(&self) {
|
fn mark_dirty(&self) {
|
||||||
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
|
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
|
||||||
self.mark_subscribers_check();
|
self.mark_subscribers_check();
|
||||||
|
@ -133,7 +133,7 @@ impl<T: Send + Sync + 'static> ReactiveNode for RwLock<MemoInner<T>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> Source for RwLock<MemoInner<T>> {
|
impl<T: 'static> Source for RwLock<MemoInner<T>> {
|
||||||
fn add_subscriber(&self, subscriber: AnySubscriber) {
|
fn add_subscriber(&self, subscriber: AnySubscriber) {
|
||||||
self.write().or_poisoned().subscribers.subscribe(subscriber);
|
self.write().or_poisoned().subscribers.subscribe(subscriber);
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ impl<T: Send + Sync + 'static> Source for RwLock<MemoInner<T>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> Subscriber for RwLock<MemoInner<T>> {
|
impl<T: 'static> Subscriber for RwLock<MemoInner<T>> {
|
||||||
fn add_source(&self, source: AnySource) {
|
fn add_source(&self, source: AnySource) {
|
||||||
self.write().or_poisoned().sources.insert(source);
|
self.write().or_poisoned().sources.insert(source);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,17 @@ pub struct Memo<T: Send + Sync + 'static> {
|
||||||
inner: StoredValue<ArcMemo<T>>,
|
inner: StoredValue<ArcMemo<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + 'static> From<ArcMemo<T>> for Memo<T> {
|
||||||
|
#[track_caller]
|
||||||
|
fn from(value: ArcMemo<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: Location::caller(),
|
||||||
|
inner: StoredValue::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> Memo<T> {
|
impl<T: Send + Sync + 'static> Memo<T> {
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
|
|
|
@ -84,6 +84,7 @@ pub mod selector;
|
||||||
mod serde;
|
mod serde;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
|
pub mod wrappers;
|
||||||
|
|
||||||
pub use graph::untrack;
|
pub use graph::untrack;
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,11 @@ use crate::{
|
||||||
ArcReadSignal, ArcRwSignal, ArcWriteSignal, ReadSignal, RwSignal,
|
ArcReadSignal, ArcRwSignal, ArcWriteSignal, ReadSignal, RwSignal,
|
||||||
WriteSignal,
|
WriteSignal,
|
||||||
},
|
},
|
||||||
traits::{Read, Set},
|
traits::{Get, Read, Set},
|
||||||
|
wrappers::read::{ArcSignal, Signal},
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! impl_get_fn_traits {
|
macro_rules! impl_get_fn_traits_read {
|
||||||
($($ty:ident $(($method_name:ident))?),*) => {
|
($($ty:ident $(($method_name:ident))?),*) => {
|
||||||
$(
|
$(
|
||||||
#[cfg(feature = "nightly")]
|
#[cfg(feature = "nightly")]
|
||||||
|
@ -16,7 +17,7 @@ macro_rules! impl_get_fn_traits {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ macro_rules! impl_get_fn_traits {
|
||||||
impl<T: 'static> FnMut<()> for $ty<T> {
|
impl<T: 'static> FnMut<()> for $ty<T> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ macro_rules! impl_get_fn_traits {
|
||||||
impl<T: 'static> Fn<()> for $ty<T> {
|
impl<T: 'static> Fn<()> for $ty<T> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
@ -45,6 +46,44 @@ macro_rules! impl_get_fn_traits {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_get_fn_traits_get {
|
||||||
|
($($ty:ident $(($method_name:ident))?),*) => {
|
||||||
|
$(
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: 'static> FnOnce<()> for $ty<T> {
|
||||||
|
type Output = <Self as Get>::Value;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: 'static> FnMut<()> for $ty<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: 'static> Fn<()> for $ty<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
(@method_name $self:ident) => {
|
||||||
|
$self.get()
|
||||||
|
};
|
||||||
|
(@method_name $self:ident $ident:ident) => {
|
||||||
|
$self.$ident()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! impl_set_fn_traits {
|
macro_rules! impl_set_fn_traits {
|
||||||
($($ty:ident $($method_name:ident)?),*) => {
|
($($ty:ident $($method_name:ident)?),*) => {
|
||||||
$(
|
$(
|
||||||
|
@ -83,7 +122,7 @@ macro_rules! impl_set_fn_traits {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_get_fn_traits_send {
|
macro_rules! impl_get_fn_traits_read_send {
|
||||||
($($ty:ident $(($method_name:ident))?),*) => {
|
($($ty:ident $(($method_name:ident))?),*) => {
|
||||||
$(
|
$(
|
||||||
#[cfg(feature = "nightly")]
|
#[cfg(feature = "nightly")]
|
||||||
|
@ -92,7 +131,7 @@ macro_rules! impl_get_fn_traits_send {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits_send!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read_send!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +139,7 @@ macro_rules! impl_get_fn_traits_send {
|
||||||
impl<T: Send + Sync + 'static> FnMut<()> for $ty<T> {
|
impl<T: Send + Sync + 'static> FnMut<()> for $ty<T> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits_send!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read_send!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +147,7 @@ macro_rules! impl_get_fn_traits_send {
|
||||||
impl<T: Send + Sync + 'static> Fn<()> for $ty<T> {
|
impl<T: Send + Sync + 'static> Fn<()> for $ty<T> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits_send!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read_send!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
@ -121,6 +160,43 @@ macro_rules! impl_get_fn_traits_send {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_get_fn_traits_get_send {
|
||||||
|
($($ty:ident $(($method_name:ident))?),*) => {
|
||||||
|
$(
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: Send + Sync + Clone + 'static> FnOnce<()> for $ty<T> {
|
||||||
|
type Output = <Self as Get>::Value;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_get_send!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: Send + Sync + Clone + 'static> FnMut<()> for $ty<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_get_send!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: Send + Sync + Clone + 'static> Fn<()> for $ty<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_get_send!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
(@method_name $self:ident) => {
|
||||||
|
$self.get()
|
||||||
|
};
|
||||||
|
(@method_name $self:ident $ident:ident) => {
|
||||||
|
$self.$ident()
|
||||||
|
};
|
||||||
|
}
|
||||||
macro_rules! impl_set_fn_traits_send {
|
macro_rules! impl_set_fn_traits_send {
|
||||||
($($ty:ident $($method_name:ident)?),*) => {
|
($($ty:ident $($method_name:ident)?),*) => {
|
||||||
$(
|
$(
|
||||||
|
@ -159,7 +235,8 @@ macro_rules! impl_set_fn_traits_send {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_get_fn_traits![ArcReadSignal, ArcRwSignal];
|
impl_get_fn_traits_read![ArcReadSignal, ArcRwSignal];
|
||||||
impl_get_fn_traits_send![ReadSignal, RwSignal, Memo, ArcMemo];
|
impl_get_fn_traits_get_send![ArcSignal, Signal];
|
||||||
|
impl_get_fn_traits_read_send![ReadSignal, RwSignal, Memo, ArcMemo];
|
||||||
impl_set_fn_traits![ArcWriteSignal];
|
impl_set_fn_traits![ArcWriteSignal];
|
||||||
impl_set_fn_traits_send![WriteSignal];
|
impl_set_fn_traits_send![WriteSignal];
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
use hydration_context::SharedContext;
|
||||||
use or_poisoned::OrPoisoned;
|
use or_poisoned::OrPoisoned;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -18,6 +20,8 @@ pub use context::*;
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct Owner {
|
pub struct Owner {
|
||||||
pub(crate) inner: Arc<RwLock<OwnerInner>>,
|
pub(crate) inner: Arc<RwLock<OwnerInner>>,
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub(crate) shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
|
@ -25,9 +29,21 @@ thread_local! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Owner {
|
impl Owner {
|
||||||
|
pub fn debug_id(&self) -> usize {
|
||||||
|
Arc::as_ptr(&self.inner) as usize
|
||||||
|
}
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
#[cfg(not(feature = "hydration"))]
|
||||||
let parent = OWNER
|
let parent = OWNER
|
||||||
.with(|o| o.borrow().as_ref().map(|o| Arc::downgrade(&o.inner)));
|
.with(|o| o.borrow().as_ref().map(|o| Arc::downgrade(&o.inner)));
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
let (parent, shared_context) = OWNER
|
||||||
|
.with(|o| {
|
||||||
|
o.borrow().as_ref().map(|o| {
|
||||||
|
(Some(Arc::clone(&o.inner)), o.shared_context.clone())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or((None, None));
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(RwLock::new(OwnerInner {
|
inner: Arc::new(RwLock::new(OwnerInner {
|
||||||
parent,
|
parent,
|
||||||
|
@ -35,11 +51,29 @@ impl Owner {
|
||||||
contexts: Default::default(),
|
contexts: Default::default(),
|
||||||
cleanups: Default::default(),
|
cleanups: Default::default(),
|
||||||
})),
|
})),
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
shared_context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub fn new_root(
|
||||||
|
shared_context: Arc<dyn SharedContext + Send + Sync>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(RwLock::new(OwnerInner {
|
||||||
|
parent: None,
|
||||||
|
nodes: Default::default(),
|
||||||
|
contexts: Default::default(),
|
||||||
|
cleanups: Default::default(),
|
||||||
|
})),
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
shared_context: Some(shared_context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn child(&self) -> Self {
|
pub fn child(&self) -> Self {
|
||||||
let parent = Some(Arc::downgrade(&self.inner));
|
let parent = Some(Arc::clone(&self.inner));
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(RwLock::new(OwnerInner {
|
inner: Arc::new(RwLock::new(OwnerInner {
|
||||||
parent,
|
parent,
|
||||||
|
@ -47,6 +81,8 @@ impl Owner {
|
||||||
contexts: Default::default(),
|
contexts: Default::default(),
|
||||||
cleanups: Default::default(),
|
cleanups: Default::default(),
|
||||||
})),
|
})),
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
shared_context: self.shared_context.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,11 +133,21 @@ impl Owner {
|
||||||
pub fn current() -> Option<Owner> {
|
pub fn current() -> Option<Owner> {
|
||||||
OWNER.with(|o| o.borrow().clone())
|
OWNER.with(|o| o.borrow().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub fn current_shared_context(
|
||||||
|
) -> Option<Arc<dyn SharedContext + Send + Sync>> {
|
||||||
|
OWNER.with(|o| {
|
||||||
|
o.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|current| current.shared_context.clone())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct OwnerInner {
|
pub(crate) struct OwnerInner {
|
||||||
pub parent: Option<Weak<RwLock<OwnerInner>>>,
|
pub parent: Option<Arc<RwLock<OwnerInner>>>,
|
||||||
nodes: Vec<NodeId>,
|
nodes: Vec<NodeId>,
|
||||||
pub contexts: FxHashMap<TypeId, Box<dyn Any + Send + Sync>>,
|
pub contexts: FxHashMap<TypeId, Box<dyn Any + Send + Sync>>,
|
||||||
pub cleanups: Vec<Box<dyn FnOnce() + Send + Sync>>,
|
pub cleanups: Vec<Box<dyn FnOnce() + Send + Sync>>,
|
||||||
|
|
|
@ -14,7 +14,7 @@ impl Owner {
|
||||||
fn use_context<T: Clone + 'static>(&self) -> Option<T> {
|
fn use_context<T: Clone + 'static>(&self) -> Option<T> {
|
||||||
let ty = TypeId::of::<T>();
|
let ty = TypeId::of::<T>();
|
||||||
let inner = self.inner.read().or_poisoned();
|
let inner = self.inner.read().or_poisoned();
|
||||||
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
let mut parent = inner.parent.as_ref().map(|p| p.clone());
|
||||||
let contexts = &self.inner.read().or_poisoned().contexts;
|
let contexts = &self.inner.read().or_poisoned().contexts;
|
||||||
if let Some(context) = contexts.get(&ty) {
|
if let Some(context) = contexts.get(&ty) {
|
||||||
context.downcast_ref::<T>().cloned()
|
context.downcast_ref::<T>().cloned()
|
||||||
|
@ -28,8 +28,7 @@ impl Owner {
|
||||||
if let Some(value) = downcast {
|
if let Some(value) = downcast {
|
||||||
return Some(value);
|
return Some(value);
|
||||||
} else {
|
} else {
|
||||||
parent =
|
parent = this_parent.parent.as_ref().map(|p| p.clone());
|
||||||
this_parent.parent.as_ref().and_then(|p| p.upgrade());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
|
@ -184,12 +184,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait With: WithUntracked + Track {
|
pub trait With: DefinedAt {
|
||||||
#[track_caller]
|
type Value: ?Sized;
|
||||||
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
|
|
||||||
self.track();
|
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U>;
|
||||||
self.try_with_untracked(fun)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
|
fn with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
|
||||||
|
@ -197,15 +195,23 @@ pub trait With: WithUntracked + Track {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> With for T where T: WithUntracked + Track {}
|
impl<T> With for T
|
||||||
|
|
||||||
pub trait GetUntracked: WithUntracked
|
|
||||||
where
|
where
|
||||||
Self::Value: Clone,
|
T: WithUntracked + Track,
|
||||||
{
|
{
|
||||||
fn try_get_untracked(&self) -> Option<Self::Value> {
|
type Value = <T as WithUntracked>::Value;
|
||||||
self.try_with_untracked(Self::Value::clone)
|
|
||||||
|
#[track_caller]
|
||||||
|
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
|
||||||
|
self.track();
|
||||||
|
self.try_with_untracked(fun)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait GetUntracked: DefinedAt {
|
||||||
|
type Value;
|
||||||
|
|
||||||
|
fn try_get_untracked(&self) -> Option<Self::Value>;
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn get_untracked(&self) -> Self::Value {
|
fn get_untracked(&self) -> Self::Value {
|
||||||
|
@ -219,20 +225,21 @@ where
|
||||||
T: WithUntracked,
|
T: WithUntracked,
|
||||||
T::Value: Clone,
|
T::Value: Clone,
|
||||||
{
|
{
|
||||||
|
type Value = <Self as WithUntracked>::Value;
|
||||||
|
|
||||||
|
fn try_get_untracked(&self) -> Option<Self::Value> {
|
||||||
|
self.try_with_untracked(Self::Value::clone)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Get: With
|
pub trait Get: DefinedAt {
|
||||||
where
|
type Value: Clone;
|
||||||
Self::Value: Clone,
|
|
||||||
{
|
fn try_get(&self) -> Option<Self::Value>;
|
||||||
fn try_get(&self) -> Option<Self::Value> {
|
|
||||||
self.try_with(Self::Value::clone)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn get(&self) -> Self::Value {
|
fn get(&self) -> Self::Value {
|
||||||
self.try_with(Self::Value::clone)
|
self.try_get().unwrap_or_else(unwrap_signal!(self))
|
||||||
.unwrap_or_else(unwrap_signal!(self))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,6 +248,11 @@ where
|
||||||
T: With,
|
T: With,
|
||||||
T::Value: Clone,
|
T::Value: Clone,
|
||||||
{
|
{
|
||||||
|
type Value = <T as With>::Value;
|
||||||
|
|
||||||
|
fn try_get(&self) -> Option<Self::Value> {
|
||||||
|
self.try_with(Self::Value::clone)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Trigger {
|
pub trait Trigger {
|
||||||
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
pub mod read {
|
||||||
|
use crate::{
|
||||||
|
computed::ArcMemo,
|
||||||
|
owner::StoredValue,
|
||||||
|
signal::ArcReadSignal,
|
||||||
|
traits::{DefinedAt, Get, GetUntracked},
|
||||||
|
untrack,
|
||||||
|
};
|
||||||
|
use std::{panic::Location, sync::Arc};
|
||||||
|
|
||||||
|
enum SignalTypes<T: 'static> {
|
||||||
|
ReadSignal(ArcReadSignal<T>),
|
||||||
|
Memo(ArcMemo<T>),
|
||||||
|
DerivedSignal(Arc<dyn Fn() -> T + Send + Sync>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for SignalTypes<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::ReadSignal(arg0) => Self::ReadSignal(arg0.clone()),
|
||||||
|
Self::Memo(arg0) => Self::Memo(arg0.clone()),
|
||||||
|
Self::DerivedSignal(arg0) => Self::DerivedSignal(arg0.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> core::fmt::Debug for SignalTypes<T> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ReadSignal(arg0) => {
|
||||||
|
f.debug_tuple("ReadSignal").field(arg0).finish()
|
||||||
|
}
|
||||||
|
Self::Memo(arg0) => f.debug_tuple("Memo").field(arg0).finish(),
|
||||||
|
Self::DerivedSignal(_) => {
|
||||||
|
f.debug_tuple("DerivedSignal").finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq for SignalTypes<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::ReadSignal(l0), Self::ReadSignal(r0)) => l0 == r0,
|
||||||
|
(Self::Memo(l0), Self::Memo(r0)) => l0 == r0,
|
||||||
|
(Self::DerivedSignal(l0), Self::DerivedSignal(r0)) => {
|
||||||
|
std::ptr::eq(l0, r0)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArcSignal<T: 'static> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: &'static Location<'static>,
|
||||||
|
inner: SignalTypes<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for ArcSignal<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: self.defined_at,
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> core::fmt::Debug for ArcSignal<T> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let mut s = f.debug_struct("ArcSignal");
|
||||||
|
s.field("inner", &self.inner);
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
s.field("defined_at", &self.defined_at);
|
||||||
|
s.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Eq for ArcSignal<T> {}
|
||||||
|
|
||||||
|
impl<T> PartialEq for ArcSignal<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner == other.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DefinedAt for ArcSignal<T> {
|
||||||
|
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Some(self.defined_at)
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GetUntracked for ArcSignal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn try_get_untracked(&self) -> Option<Self::Value> {
|
||||||
|
match &self.inner {
|
||||||
|
SignalTypes::ReadSignal(i) => i.try_get_untracked(),
|
||||||
|
SignalTypes::Memo(i) => i.try_get_untracked(),
|
||||||
|
SignalTypes::DerivedSignal(i) => Some(untrack(|| i())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Get for ArcSignal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn try_get(&self) -> Option<Self::Value> {
|
||||||
|
match &self.inner {
|
||||||
|
SignalTypes::ReadSignal(i) => i.try_get(),
|
||||||
|
SignalTypes::Memo(i) => i.try_get(),
|
||||||
|
SignalTypes::DerivedSignal(i) => Some(i()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Signal<T: 'static> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: &'static Location<'static>,
|
||||||
|
inner: StoredValue<SignalTypes<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for Signal<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Copy for Signal<T> {}
|
||||||
|
|
||||||
|
impl<T> core::fmt::Debug for Signal<T> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let mut s = f.debug_struct("Signal");
|
||||||
|
s.field("inner", &self.inner);
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
s.field("defined_at", &self.defined_at);
|
||||||
|
s.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Eq for Signal<T> {}
|
||||||
|
|
||||||
|
impl<T> PartialEq for Signal<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner == other.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DefinedAt for Signal<T> {
|
||||||
|
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Some(self.defined_at)
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GetUntracked for Signal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn try_get_untracked(&self) -> Option<Self::Value> {
|
||||||
|
self.inner
|
||||||
|
.with_value(|inner| match &inner {
|
||||||
|
SignalTypes::ReadSignal(i) => i.try_get_untracked(),
|
||||||
|
SignalTypes::Memo(i) => i.try_get_untracked(),
|
||||||
|
SignalTypes::DerivedSignal(i) => Some(untrack(|| i())),
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Get for Signal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn try_get(&self) -> Option<Self::Value> {
|
||||||
|
self.inner
|
||||||
|
.with_value(|inner| match &inner {
|
||||||
|
SignalTypes::ReadSignal(i) => i.try_get(),
|
||||||
|
SignalTypes::Memo(i) => i.try_get(),
|
||||||
|
SignalTypes::DerivedSignal(i) => Some(i()),
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Signal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
/// Wraps a derived signal, i.e., any computation that accesses one or more
|
||||||
|
/// reactive signals.
|
||||||
|
/// ```rust
|
||||||
|
/// # use leptos_reactive::*;
|
||||||
|
/// # let runtime = create_runtime();
|
||||||
|
/// let (count, set_count) = create_signal(2);
|
||||||
|
/// let double_count = Signal::derive(move || count.get() * 2);
|
||||||
|
///
|
||||||
|
/// // this function takes any kind of wrapped signal
|
||||||
|
/// fn above_3(arg: &Signal<i32>) -> bool {
|
||||||
|
/// arg.get() > 3
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// assert_eq!(above_3(&count.into()), false);
|
||||||
|
/// assert_eq!(above_3(&double_count), true);
|
||||||
|
/// # runtime.dispose();
|
||||||
|
/// ```
|
||||||
|
#[track_caller]
|
||||||
|
pub fn derive(
|
||||||
|
derived_signal: impl Fn() -> T + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
let span = ::tracing::Span::current();
|
||||||
|
|
||||||
|
let derived_signal = move || {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
let _guard = span.enter();
|
||||||
|
derived_signal()
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner: StoredValue::new(SignalTypes::DerivedSignal(Arc::new(
|
||||||
|
derived_signal,
|
||||||
|
))),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: std::panic::Location::caller(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for Signal<T>
|
||||||
|
where
|
||||||
|
T: Default + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::derive(|| Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use tachys::{renderer::Renderer, view::RenderHtml};
|
use tachys::{renderer::Renderer, view::RenderHtml};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
/// A route that this application can serve.
|
/// A route that this application can serve.
|
||||||
pub struct RouteListing {
|
pub struct RouteListing {
|
||||||
path: Vec<PathSegment>,
|
path: Vec<PathSegment>,
|
||||||
|
@ -64,6 +64,10 @@ impl RouteListing {
|
||||||
self.static_mode.as_ref().map(|n| &n.1)
|
self.static_mode.as_ref().map(|n| &n.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_static_parts(self) -> Option<(StaticMode, StaticDataMap)> {
|
||||||
|
self.static_mode
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
|
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
|
||||||
/// is not marked as statically rendered. All route parameters to use when resolving all paths
|
/// is not marked as statically rendered. All route parameters to use when resolving all paths
|
||||||
|
@ -100,6 +104,12 @@ impl RouteListing {
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct RouteList(Vec<RouteListing>);
|
pub struct RouteList(Vec<RouteListing>);
|
||||||
|
|
||||||
|
impl From<Vec<RouteListing>> for RouteList {
|
||||||
|
fn from(value: Vec<RouteListing>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RouteList {
|
impl RouteList {
|
||||||
pub fn push(&mut self, data: RouteListing) {
|
pub fn push(&mut self, data: RouteListing) {
|
||||||
self.0.push(data);
|
self.0.push(data);
|
||||||
|
|
|
@ -20,11 +20,6 @@ impl fmt::Debug for BrowserUrl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserUrl {
|
impl BrowserUrl {
|
||||||
pub fn new() -> Result<Self, JsValue> {
|
|
||||||
let url = ArcRwSignal::new(Self::current()?);
|
|
||||||
Ok(Self { url })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_to_el(loc_scroll: bool) {
|
fn scroll_to_el(loc_scroll: bool) {
|
||||||
if let Ok(hash) = window().location().hash() {
|
if let Ok(hash) = window().location().hash() {
|
||||||
if !hash.is_empty() {
|
if !hash.is_empty() {
|
||||||
|
@ -50,6 +45,11 @@ impl BrowserUrl {
|
||||||
impl Location for BrowserUrl {
|
impl Location for BrowserUrl {
|
||||||
type Error = JsValue;
|
type Error = JsValue;
|
||||||
|
|
||||||
|
fn new() -> Result<Self, JsValue> {
|
||||||
|
let url = ArcRwSignal::new(Self::current()?);
|
||||||
|
Ok(Self { url })
|
||||||
|
}
|
||||||
|
|
||||||
fn as_url(&self) -> &ArcRwSignal<Url> {
|
fn as_url(&self) -> &ArcRwSignal<Url> {
|
||||||
&self.url
|
&self.url
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,9 +71,11 @@ impl Default for LocationChange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Location {
|
pub trait Location: Sized {
|
||||||
type Error: Debug;
|
type Error: Debug;
|
||||||
|
|
||||||
|
fn new() -> Result<Self, Self::Error>;
|
||||||
|
|
||||||
fn as_url(&self) -> &ArcRwSignal<Url>;
|
fn as_url(&self) -> &ArcRwSignal<Url>;
|
||||||
|
|
||||||
fn current() -> Result<Url, Self::Error>;
|
fn current() -> Result<Url, Self::Error>;
|
||||||
|
|
|
@ -11,6 +11,12 @@ impl RequestUrl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for RequestUrl {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for RequestUrl {
|
impl Default for RequestUrl {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new("/")
|
Self::new("/")
|
||||||
|
@ -18,7 +24,7 @@ impl Default for RequestUrl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestUrl {
|
impl RequestUrl {
|
||||||
fn parse(url: &str) -> Result<Url, url::ParseError> {
|
pub fn parse(url: &str) -> Result<Url, url::ParseError> {
|
||||||
Self::parse_with_base(url, BASE)
|
Self::parse_with_base(url, BASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -214,14 +214,12 @@ where
|
||||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
||||||
let mut segment_routes = Vec::new();
|
let mut segment_routes = Vec::new();
|
||||||
self.segments.generate_path(&mut segment_routes);
|
self.segments.generate_path(&mut segment_routes);
|
||||||
let segment_routes = segment_routes.into_iter();
|
let children = self.children.as_ref();
|
||||||
let children_routes = self.children.as_ref().into_iter().flat_map(|child| child.generate_routes().into_iter());
|
match children {
|
||||||
children_routes.map(move |child_routes| {
|
None => Either::Left(iter::once(segment_routes)),
|
||||||
segment_routes
|
Some(children) => {
|
||||||
.clone()
|
Either::Right(children.generate_routes().into_iter())
|
||||||
.chain(child_routes)
|
}
|
||||||
.filter(|seg| seg != &PathSegment::Unit)
|
}
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -316,7 +316,7 @@ macro_rules! tuples {
|
||||||
}
|
}
|
||||||
|
|
||||||
tuples!(EitherOf3 => A = 0, B = 1, C = 2);
|
tuples!(EitherOf3 => A = 0, B = 1, C = 2);
|
||||||
/*tuples!(EitherOf4 => A = 0, B = 1, C = 2, D = 3);
|
tuples!(EitherOf4 => A = 0, B = 1, C = 2, D = 3);
|
||||||
tuples!(EitherOf5 => A = 0, B = 1, C = 2, D = 3, E = 4);
|
tuples!(EitherOf5 => A = 0, B = 1, C = 2, D = 3, E = 4);
|
||||||
tuples!(EitherOf6 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5);
|
tuples!(EitherOf6 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5);
|
||||||
tuples!(EitherOf7 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6);
|
tuples!(EitherOf7 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6);
|
||||||
|
@ -329,4 +329,3 @@ tuples!(EitherOf13 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I
|
||||||
tuples!(EitherOf14 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13);
|
tuples!(EitherOf14 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13);
|
||||||
tuples!(EitherOf15 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14);
|
tuples!(EitherOf15 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14);
|
||||||
tuples!(EitherOf16 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15);
|
tuples!(EitherOf16 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15);
|
||||||
*/
|
|
||||||
|
|
|
@ -7,3 +7,14 @@ pub enum PathSegment {
|
||||||
Param(Cow<'static, str>),
|
Param(Cow<'static, str>),
|
||||||
Splat(Cow<'static, str>),
|
Splat(Cow<'static, str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PathSegment {
|
||||||
|
pub fn as_raw_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
PathSegment::Unit => "",
|
||||||
|
PathSegment::Static(i) => i,
|
||||||
|
PathSegment::Param(i) => i,
|
||||||
|
PathSegment::Splat(i) => i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,12 @@ impl Params {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, key: &str) -> Option<&str> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.find_map(|(k, v)| (k == key).then_some(v.as_str()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K, V> FromIterator<(K, V)> for Params
|
impl<K, V> FromIterator<(K, V)> for Params
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
generate_route_list::RouteList,
|
generate_route_list::RouteList,
|
||||||
location::Location,
|
location::{Location, RequestUrl},
|
||||||
matching::{
|
matching::{
|
||||||
MatchInterface, MatchNestedRoutes, PossibleRouteMatch, RouteMatchId,
|
MatchInterface, MatchNestedRoutes, PossibleRouteMatch, RouteMatchId,
|
||||||
Routes,
|
Routes,
|
||||||
},
|
},
|
||||||
ChooseView, MatchParams, Params,
|
ChooseView, MatchParams, Method, Params, PathSegment, RouteListing,
|
||||||
|
SsrMode,
|
||||||
};
|
};
|
||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
use either_of::*;
|
use either_of::*;
|
||||||
|
@ -13,17 +14,23 @@ use once_cell::unsync::Lazy;
|
||||||
use reactive_graph::{
|
use reactive_graph::{
|
||||||
computed::{ArcMemo, Memo},
|
computed::{ArcMemo, Memo},
|
||||||
effect::RenderEffect,
|
effect::RenderEffect,
|
||||||
owner::Owner,
|
owner::{use_context, Owner},
|
||||||
signal::ArcRwSignal,
|
signal::ArcRwSignal,
|
||||||
traits::{Get, Read, Set, Track},
|
traits::{Get, Read, Set, Track},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any, borrow::Cow, cell::RefCell, collections::VecDeque, rc::Rc,
|
any::Any,
|
||||||
|
borrow::Cow,
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
collections::VecDeque,
|
||||||
|
fmt::Debug,
|
||||||
|
iter,
|
||||||
|
rc::Rc,
|
||||||
};
|
};
|
||||||
use tachys::{
|
use tachys::{
|
||||||
html::attribute::Attribute,
|
html::attribute::Attribute,
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
renderer::Renderer,
|
renderer::{dom::Dom, Renderer},
|
||||||
ssr::StreamBuilder,
|
ssr::StreamBuilder,
|
||||||
view::{
|
view::{
|
||||||
add_attr::AddAnyAttr,
|
add_attr::AddAnyAttr,
|
||||||
|
@ -36,7 +43,7 @@ use tachys::{
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Router<Rndr, Loc, Children, FallbackFn> {
|
pub struct Router<Rndr, Loc, Children, FallbackFn> {
|
||||||
base: Option<Cow<'static, str>>,
|
base: Option<Cow<'static, str>>,
|
||||||
location: Loc,
|
location: PhantomData<Loc>,
|
||||||
pub routes: Routes<Children, Rndr>,
|
pub routes: Routes<Children, Rndr>,
|
||||||
fallback: FallbackFn,
|
fallback: FallbackFn,
|
||||||
}
|
}
|
||||||
|
@ -49,13 +56,12 @@ where
|
||||||
FallbackFn: Fn() -> Fallback,
|
FallbackFn: Fn() -> Fallback,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
location: Loc,
|
|
||||||
routes: Routes<Children, Rndr>,
|
routes: Routes<Children, Rndr>,
|
||||||
fallback: FallbackFn,
|
fallback: FallbackFn,
|
||||||
) -> Router<Rndr, Loc, Children, FallbackFn> {
|
) -> Router<Rndr, Loc, Children, FallbackFn> {
|
||||||
Self {
|
Self {
|
||||||
base: None,
|
base: None,
|
||||||
location,
|
location: PhantomData,
|
||||||
routes,
|
routes,
|
||||||
fallback,
|
fallback,
|
||||||
}
|
}
|
||||||
|
@ -63,13 +69,12 @@ where
|
||||||
|
|
||||||
pub fn new_with_base(
|
pub fn new_with_base(
|
||||||
base: impl Into<Cow<'static, str>>,
|
base: impl Into<Cow<'static, str>>,
|
||||||
location: Loc,
|
|
||||||
routes: Routes<Children, Rndr>,
|
routes: Routes<Children, Rndr>,
|
||||||
fallback: FallbackFn,
|
fallback: FallbackFn,
|
||||||
) -> Router<Rndr, Loc, Children, FallbackFn> {
|
) -> Router<Rndr, Loc, Children, FallbackFn> {
|
||||||
Self {
|
Self {
|
||||||
base: Some(base.into()),
|
base: Some(base.into()),
|
||||||
location,
|
location: PhantomData,
|
||||||
routes,
|
routes,
|
||||||
fallback,
|
fallback,
|
||||||
}
|
}
|
||||||
|
@ -87,7 +92,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RouteData<R>
|
pub struct RouteData<R = Dom>
|
||||||
where
|
where
|
||||||
R: Renderer + 'static,
|
R: Renderer + 'static,
|
||||||
{
|
{
|
||||||
|
@ -120,8 +125,9 @@ where
|
||||||
type FallibleState = (); // TODO
|
type FallibleState = (); // TODO
|
||||||
|
|
||||||
fn build(self) -> Self::State {
|
fn build(self) -> Self::State {
|
||||||
self.location.init(self.base);
|
let location = Loc::new().unwrap(); // TODO
|
||||||
let url = self.location.as_url().clone();
|
location.init(self.base);
|
||||||
|
let url = location.as_url().clone();
|
||||||
let path = ArcMemo::new({
|
let path = ArcMemo::new({
|
||||||
let url = url.clone();
|
let url = url.clone();
|
||||||
move |_| url.read().path().to_string()
|
move |_| url.read().path().to_string()
|
||||||
|
@ -185,6 +191,178 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Rndr, Loc, FallbackFn, Fallback, Children> RenderHtml<Rndr>
|
||||||
|
for Router<Rndr, Loc, Children, FallbackFn>
|
||||||
|
where
|
||||||
|
Loc: Location,
|
||||||
|
FallbackFn: Fn() -> Fallback + 'static,
|
||||||
|
Fallback: RenderHtml<Rndr>,
|
||||||
|
Children: MatchNestedRoutes<Rndr> + 'static,
|
||||||
|
Children::View: RenderHtml<Rndr>,
|
||||||
|
/*View: Render<Rndr> + IntoAny<Rndr> + 'static,
|
||||||
|
View::State: 'static,*/
|
||||||
|
Fallback: RenderHtml<Rndr>,
|
||||||
|
Fallback::State: 'static,
|
||||||
|
Rndr: Renderer + 'static,
|
||||||
|
Children::Match: std::fmt::Debug,
|
||||||
|
<Children::Match as MatchInterface<Rndr>>::Child: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
// TODO probably pick a max length here
|
||||||
|
const MIN_LENGTH: usize = Fallback::MIN_LENGTH;
|
||||||
|
|
||||||
|
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||||
|
// if this is being run on the server for the first time, generating all possible routes
|
||||||
|
if RouteList::is_generating() {
|
||||||
|
// add routes
|
||||||
|
let (base, routes) = self.routes.generate_routes();
|
||||||
|
let mut routes = routes
|
||||||
|
.into_iter()
|
||||||
|
.map(|segments| {
|
||||||
|
let path = base
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|base| {
|
||||||
|
iter::once(PathSegment::Static(
|
||||||
|
base.to_string().into(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.chain(segments)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
// TODO add non-defaults for mode, etc.
|
||||||
|
RouteListing::new(
|
||||||
|
path,
|
||||||
|
SsrMode::OutOfOrder,
|
||||||
|
[Method::Get],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// add fallback
|
||||||
|
// TODO fix: causes overlapping route issues on Axum
|
||||||
|
/*routes.push(RouteListing::new(
|
||||||
|
[PathSegment::Static(
|
||||||
|
base.unwrap_or_default().to_string().into(),
|
||||||
|
)],
|
||||||
|
SsrMode::Async,
|
||||||
|
[
|
||||||
|
Method::Get,
|
||||||
|
Method::Post,
|
||||||
|
Method::Put,
|
||||||
|
Method::Patch,
|
||||||
|
Method::Delete,
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
));*/
|
||||||
|
|
||||||
|
RouteList::register(RouteList::from(routes));
|
||||||
|
} else {
|
||||||
|
let outer_owner = Owner::current()
|
||||||
|
.expect("creating Router, but no Owner was found");
|
||||||
|
let url = use_context::<RequestUrl>()
|
||||||
|
.expect("could not find request URL in context");
|
||||||
|
// TODO base
|
||||||
|
let url =
|
||||||
|
RequestUrl::parse(url.as_ref()).expect("could not parse URL");
|
||||||
|
// TODO query params
|
||||||
|
let new_match = self.routes.match_route(url.path());
|
||||||
|
match new_match {
|
||||||
|
Some(matched) => {
|
||||||
|
Either::Left(NestedRouteView::new(&outer_owner, matched))
|
||||||
|
}
|
||||||
|
_ => Either::Right((self.fallback)()),
|
||||||
|
}
|
||||||
|
.to_html_with_buf(buf, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||||
|
self,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let outer_owner =
|
||||||
|
Owner::current().expect("creating Router, but no Owner was found");
|
||||||
|
let url = use_context::<RequestUrl>()
|
||||||
|
.expect("could not find request URL in context");
|
||||||
|
// TODO base
|
||||||
|
let url = RequestUrl::parse(url.as_ref()).expect("could not parse URL");
|
||||||
|
// TODO query params
|
||||||
|
let new_match = self.routes.match_route(url.path());
|
||||||
|
match new_match {
|
||||||
|
Some(matched) => {
|
||||||
|
Either::Left(NestedRouteView::new(&outer_owner, matched))
|
||||||
|
}
|
||||||
|
_ => Either::Right((self.fallback)()),
|
||||||
|
}
|
||||||
|
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
cursor: &Cursor<Rndr>,
|
||||||
|
position: &PositionState,
|
||||||
|
) -> Self::State {
|
||||||
|
let location = Loc::new().unwrap(); // TODO
|
||||||
|
location.init(self.base);
|
||||||
|
let url = location.as_url().clone();
|
||||||
|
let path = ArcMemo::new({
|
||||||
|
let url = url.clone();
|
||||||
|
move |_| url.read().path().to_string()
|
||||||
|
});
|
||||||
|
let search_params = ArcMemo::new({
|
||||||
|
let url = url.clone();
|
||||||
|
move |_| url.read().search_params().clone()
|
||||||
|
});
|
||||||
|
let outer_owner =
|
||||||
|
Owner::current().expect("creating Router, but no Owner was found");
|
||||||
|
|
||||||
|
let cursor = cursor.clone();
|
||||||
|
let position = position.clone();
|
||||||
|
RenderEffect::new(move |prev: Option<EitherState<_, _, _>>| {
|
||||||
|
let path = path.read();
|
||||||
|
let new_match = self.routes.match_route(&path);
|
||||||
|
|
||||||
|
if let Some(mut prev) = prev {
|
||||||
|
if let Some(new_match) = new_match {
|
||||||
|
match &mut prev.state {
|
||||||
|
Either::Left(prev) => {
|
||||||
|
rebuild_nested(&outer_owner, prev, new_match);
|
||||||
|
}
|
||||||
|
Either::Right(_) => {
|
||||||
|
Either::<_, Fallback>::Left(NestedRouteView::new(
|
||||||
|
&outer_owner,
|
||||||
|
new_match,
|
||||||
|
))
|
||||||
|
.rebuild(&mut prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Either::<NestedRouteView<Children::Match, Rndr>, _>::Right(
|
||||||
|
(self.fallback)(),
|
||||||
|
)
|
||||||
|
.rebuild(&mut prev);
|
||||||
|
}
|
||||||
|
prev
|
||||||
|
} else {
|
||||||
|
match new_match {
|
||||||
|
Some(matched) => {
|
||||||
|
Either::Left(NestedRouteView::new_hydrate(
|
||||||
|
&outer_owner,
|
||||||
|
matched,
|
||||||
|
&cursor,
|
||||||
|
&position,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => Either::Right((self.fallback)()),
|
||||||
|
}
|
||||||
|
.hydrate::<true>(&cursor, &position)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct NestedRouteView<Matcher, R>
|
pub struct NestedRouteView<Matcher, R>
|
||||||
where
|
where
|
||||||
Matcher: MatchInterface<R>,
|
Matcher: MatchInterface<R>,
|
||||||
|
@ -238,6 +416,53 @@ where
|
||||||
ty: PhantomData,
|
ty: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_hydrate(
|
||||||
|
outer_owner: &Owner,
|
||||||
|
route_match: Matcher,
|
||||||
|
cursor: &Cursor<Rndr>,
|
||||||
|
position: &PositionState,
|
||||||
|
) -> Self {
|
||||||
|
// keep track of all outlets, for diffing
|
||||||
|
let mut outlets = VecDeque::new();
|
||||||
|
|
||||||
|
// build this view
|
||||||
|
let owner = outer_owner.child();
|
||||||
|
let id = route_match.as_id();
|
||||||
|
let params =
|
||||||
|
ArcRwSignal::new(route_match.to_params().into_iter().collect());
|
||||||
|
let (view, child) = route_match.into_view_and_child();
|
||||||
|
|
||||||
|
let outlet = child
|
||||||
|
.map(|child| {
|
||||||
|
get_inner_view_hydrate(
|
||||||
|
&mut outlets,
|
||||||
|
&owner,
|
||||||
|
child,
|
||||||
|
cursor,
|
||||||
|
position,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let route_data = RouteData {
|
||||||
|
params: ArcMemo::new({
|
||||||
|
let params = params.clone();
|
||||||
|
move |_| params.get()
|
||||||
|
}),
|
||||||
|
outlet,
|
||||||
|
};
|
||||||
|
let view = owner.with(|| view.choose(route_data));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
params,
|
||||||
|
outlets,
|
||||||
|
view,
|
||||||
|
ty: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NestedRouteState<Matcher, Rndr>
|
pub struct NestedRouteState<Matcher, Rndr>
|
||||||
|
@ -270,11 +495,11 @@ where
|
||||||
.map(|child| get_inner_view(outlets, &owner, child))
|
.map(|child| get_inner_view(outlets, &owner, child))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let inner = Rc::new(RefCell::new(OutletStateInner {
|
let view = Rc::new(Lazy::new({
|
||||||
state: Lazy::new({
|
let owner = owner.clone();
|
||||||
let params = params.clone();
|
let params = params.clone();
|
||||||
let owner = owner.clone();
|
Box::new(move || {
|
||||||
Box::new(move || {
|
RefCell::new(Some(
|
||||||
owner
|
owner
|
||||||
.with(|| {
|
.with(|| {
|
||||||
view.choose(RouteData {
|
view.choose(RouteData {
|
||||||
|
@ -282,10 +507,68 @@ where
|
||||||
outlet,
|
outlet,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.into_any()
|
.into_any(),
|
||||||
.build()
|
))
|
||||||
})
|
}) as Box<dyn FnOnce() -> RefCell<Option<AnyView<R>>>>
|
||||||
}),
|
}));
|
||||||
|
let inner = Rc::new(RefCell::new(OutletStateInner {
|
||||||
|
view: Rc::clone(&view),
|
||||||
|
state: Lazy::new(Box::new(move || view.take().unwrap().build())),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let outlet = Outlet {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
params,
|
||||||
|
inner,
|
||||||
|
};
|
||||||
|
outlets.push_back(outlet.clone());
|
||||||
|
outlet
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_inner_view_hydrate<Match, R>(
|
||||||
|
outlets: &mut VecDeque<Outlet<R>>,
|
||||||
|
parent: &Owner,
|
||||||
|
route_match: Match,
|
||||||
|
cursor: &Cursor<R>,
|
||||||
|
position: &PositionState,
|
||||||
|
) -> Outlet<R>
|
||||||
|
where
|
||||||
|
Match: MatchInterface<R> + MatchParams,
|
||||||
|
R: Renderer + 'static,
|
||||||
|
{
|
||||||
|
let owner = parent.child();
|
||||||
|
let id = route_match.as_id();
|
||||||
|
let params =
|
||||||
|
ArcRwSignal::new(route_match.to_params().into_iter().collect());
|
||||||
|
let (view, child) = route_match.into_view_and_child();
|
||||||
|
let outlet = child
|
||||||
|
.map(|child| get_inner_view(outlets, &owner, child))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let view = Rc::new(Lazy::new({
|
||||||
|
let owner = owner.clone();
|
||||||
|
let params = params.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
RefCell::new(Some(
|
||||||
|
owner
|
||||||
|
.with(|| {
|
||||||
|
view.choose(RouteData {
|
||||||
|
params: ArcMemo::new(move |_| params.get()),
|
||||||
|
outlet,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.into_any(),
|
||||||
|
))
|
||||||
|
}) as Box<dyn FnOnce() -> RefCell<Option<AnyView<R>>>>
|
||||||
|
}));
|
||||||
|
let inner = Rc::new(RefCell::new(OutletStateInner {
|
||||||
|
view: Rc::clone(&view),
|
||||||
|
state: Lazy::new(Box::new({
|
||||||
|
let cursor = cursor.clone();
|
||||||
|
let position = position.clone();
|
||||||
|
move || view.take().unwrap().hydrate::<true>(&cursor, &position)
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let outlet = Outlet {
|
let outlet = Outlet {
|
||||||
|
@ -332,9 +615,7 @@ where
|
||||||
id: RouteMatchId(0),
|
id: RouteMatchId(0),
|
||||||
owner: Owner::current().unwrap(),
|
owner: Owner::current().unwrap(),
|
||||||
params: ArcRwSignal::new(Params::new()),
|
params: ArcRwSignal::new(Params::new()),
|
||||||
inner: Rc::new(RefCell::new(OutletStateInner {
|
inner: Default::default(),
|
||||||
state: Lazy::new(Box::new(|| ().into_any().build())),
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,7 +654,19 @@ where
|
||||||
const MIN_LENGTH: usize = 0; // TODO
|
const MIN_LENGTH: usize = 0; // TODO
|
||||||
|
|
||||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||||
todo!()
|
let view = self.inner.borrow().view.take().unwrap();
|
||||||
|
view.to_html_with_buf(buf, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||||
|
self,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let view = self.inner.borrow().view.take().unwrap();
|
||||||
|
view.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
@ -381,24 +674,41 @@ where
|
||||||
cursor: &Cursor<R>,
|
cursor: &Cursor<R>,
|
||||||
position: &PositionState,
|
position: &PositionState,
|
||||||
) -> Self::State {
|
) -> Self::State {
|
||||||
todo!()
|
let view = self.inner.borrow().view.take().unwrap();
|
||||||
|
let state = view.hydrate::<FROM_SERVER>(cursor, position);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct OutletStateInner<R>
|
pub struct OutletStateInner<R>
|
||||||
where
|
where
|
||||||
R: Renderer + 'static,
|
R: Renderer + 'static,
|
||||||
{
|
{
|
||||||
|
view: Rc<
|
||||||
|
Lazy<
|
||||||
|
RefCell<Option<AnyView<R>>>,
|
||||||
|
Box<dyn FnOnce() -> RefCell<Option<AnyView<R>>>>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
state: Lazy<AnyViewState<R>, Box<dyn FnOnce() -> AnyViewState<R>>>,
|
state: Lazy<AnyViewState<R>, Box<dyn FnOnce() -> AnyViewState<R>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R: Renderer> Debug for OutletStateInner<R> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("OutletStateInner").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<R> Default for OutletStateInner<R>
|
impl<R> Default for OutletStateInner<R>
|
||||||
where
|
where
|
||||||
R: Renderer + 'static,
|
R: Renderer + 'static,
|
||||||
{
|
{
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let view =
|
||||||
|
Rc::new(Lazy::new(Box::new(|| RefCell::new(Some(().into_any())))
|
||||||
|
as Box<dyn FnOnce() -> RefCell<Option<AnyView<R>>>>));
|
||||||
Self {
|
Self {
|
||||||
|
view,
|
||||||
state: Lazy::new(Box::new(|| ().into_any().build())),
|
state: Lazy::new(Box::new(|| ().into_any().build())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -583,6 +893,52 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Matcher, R> RenderHtml<R> for NestedRouteView<Matcher, R>
|
||||||
|
where
|
||||||
|
Matcher: MatchInterface<R>,
|
||||||
|
Matcher::View: Sized + 'static,
|
||||||
|
R: Renderer + 'static,
|
||||||
|
{
|
||||||
|
const MIN_LENGTH: usize = Matcher::View::MIN_LENGTH;
|
||||||
|
|
||||||
|
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||||
|
self.view.to_html_with_buf(buf, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||||
|
self,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.view
|
||||||
|
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
cursor: &Cursor<R>,
|
||||||
|
position: &PositionState,
|
||||||
|
) -> Self::State {
|
||||||
|
let NestedRouteView {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
params,
|
||||||
|
outlets,
|
||||||
|
view,
|
||||||
|
ty,
|
||||||
|
} = self;
|
||||||
|
NestedRouteState {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
outlets,
|
||||||
|
params,
|
||||||
|
view: view.hydrate::<FROM_SERVER>(cursor, position),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<Matcher, R> Mountable<R> for NestedRouteState<Matcher, R>
|
impl<Matcher, R> Mountable<R> for NestedRouteState<Matcher, R>
|
||||||
where
|
where
|
||||||
Matcher: MatchInterface<R>,
|
Matcher: MatchInterface<R>,
|
||||||
|
@ -605,37 +961,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rndr, Loc, FallbackFn, Fallback, Children> RenderHtml<Rndr>
|
|
||||||
for Router<Rndr, Loc, Children, FallbackFn>
|
|
||||||
where
|
|
||||||
Loc: Location,
|
|
||||||
FallbackFn: Fn() -> Fallback + 'static,
|
|
||||||
Fallback: RenderHtml<Rndr>,
|
|
||||||
Children: MatchNestedRoutes<Rndr> + 'static,
|
|
||||||
Children::View: RenderHtml<Rndr>,
|
|
||||||
/*View: Render<Rndr> + IntoAny<Rndr> + 'static,
|
|
||||||
View::State: 'static,*/
|
|
||||||
Fallback::State: 'static,
|
|
||||||
Rndr: Renderer + 'static,
|
|
||||||
Children::Match: std::fmt::Debug,
|
|
||||||
<Children::Match as MatchInterface<Rndr>>::Child: std::fmt::Debug,
|
|
||||||
{
|
|
||||||
// TODO probably pick a max length here
|
|
||||||
const MIN_LENGTH: usize = Fallback::MIN_LENGTH;
|
|
||||||
|
|
||||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
|
||||||
self,
|
|
||||||
cursor: &Cursor<Rndr>,
|
|
||||||
position: &PositionState,
|
|
||||||
) -> Self::State {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Rndr, Loc, FallbackFn, Fallback, Children, View> AddAnyAttr<Rndr>
|
impl<Rndr, Loc, FallbackFn, Fallback, Children, View> AddAnyAttr<Rndr>
|
||||||
for Router<Rndr, Loc, Children, FallbackFn>
|
for Router<Rndr, Loc, Children, FallbackFn>
|
||||||
where
|
where
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub enum StaticMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StaticDataMap;
|
pub struct StaticDataMap;
|
||||||
|
|
||||||
impl StaticDataMap {
|
impl StaticDataMap {
|
||||||
|
|
|
@ -66,6 +66,8 @@ web-sys = { version = "0.3", optional = true, features = [
|
||||||
"console",
|
"console",
|
||||||
"ReadableStream",
|
"ReadableStream",
|
||||||
"ReadableStreamDefaultReader",
|
"ReadableStreamDefaultReader",
|
||||||
|
"AbortController",
|
||||||
|
"AbortSignal"
|
||||||
] }
|
] }
|
||||||
|
|
||||||
# reqwest client
|
# reqwest client
|
||||||
|
@ -74,6 +76,7 @@ reqwest = { version = "0.12", default-features = false, optional = true, feature
|
||||||
"stream",
|
"stream",
|
||||||
] }
|
] }
|
||||||
url = "2"
|
url = "2"
|
||||||
|
pin-project-lite = "0.2.13"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["json", "cbor"]
|
default = ["json", "cbor"]
|
||||||
|
|
|
@ -38,11 +38,19 @@ pub trait Client<CustErr> {
|
||||||
pub mod browser {
|
pub mod browser {
|
||||||
use super::Client;
|
use super::Client;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ServerFnError, request::browser::BrowserRequest,
|
error::ServerFnError,
|
||||||
|
request::browser::{AbortOnDrop, BrowserRequest, RequestInner},
|
||||||
response::browser::BrowserResponse,
|
response::browser::BrowserResponse,
|
||||||
};
|
};
|
||||||
|
use gloo_net::{http::Response, Error};
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use std::future::Future;
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
marker::PhantomData,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
use web_sys::AbortController;
|
||||||
|
|
||||||
/// Implements [`Client`] for a `fetch` request in the browser.
|
/// Implements [`Client`] for a `fetch` request in the browser.
|
||||||
pub struct BrowserClient;
|
pub struct BrowserClient;
|
||||||
|
@ -56,8 +64,17 @@ pub mod browser {
|
||||||
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
|
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
|
||||||
+ Send {
|
+ Send {
|
||||||
SendWrapper::new(async move {
|
SendWrapper::new(async move {
|
||||||
req.0
|
let mut req = req.0.take();
|
||||||
.take()
|
let RequestInner {
|
||||||
|
request,
|
||||||
|
abort_ctrl,
|
||||||
|
} = req;
|
||||||
|
/*BrowserRequestFuture {
|
||||||
|
request_fut: request.send(),
|
||||||
|
abort_ctrl,
|
||||||
|
cust_err: PhantomData,
|
||||||
|
}*/
|
||||||
|
request
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map(|res| BrowserResponse(SendWrapper::new(res)))
|
.map(|res| BrowserResponse(SendWrapper::new(res)))
|
||||||
|
@ -65,6 +82,36 @@ pub mod browser {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*pin_project_lite::pin_project! {
|
||||||
|
struct BrowserRequestFuture<Fut, CustErr>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = Result<Response, Error>>,
|
||||||
|
{
|
||||||
|
#[pin]
|
||||||
|
request_fut: Fut,
|
||||||
|
abort_ctrl: Option<AbortOnDrop>,
|
||||||
|
cust_err: PhantomData<CustErr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Fut, CustErr> Future for BrowserRequestFuture<Fut, CustErr>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = Result<Response, Error>>,
|
||||||
|
{
|
||||||
|
type Output = Result<BrowserResponse, ServerFnError<CustErr>>;
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Self::Output> {
|
||||||
|
let this = self.project();
|
||||||
|
match this.request_fut.poll(cx) {
|
||||||
|
Poll::Ready(value) => todo!(),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "reqwest")]
|
#[cfg(feature = "reqwest")]
|
||||||
|
|
|
@ -8,27 +8,39 @@ use send_wrapper::SendWrapper;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use wasm_streams::ReadableStream;
|
use wasm_streams::ReadableStream;
|
||||||
use web_sys::{FormData, Headers, RequestInit, UrlSearchParams};
|
use web_sys::{
|
||||||
|
AbortController, AbortSignal, FormData, Headers, RequestInit,
|
||||||
|
UrlSearchParams,
|
||||||
|
};
|
||||||
|
|
||||||
/// A `fetch` request made in the browser.
|
/// A `fetch` request made in the browser.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BrowserRequest(pub(crate) SendWrapper<Request>);
|
pub struct BrowserRequest(pub(crate) SendWrapper<RequestInner>);
|
||||||
|
|
||||||
impl From<Request> for BrowserRequest {
|
#[derive(Debug)]
|
||||||
fn from(value: Request) -> Self {
|
pub(crate) struct RequestInner {
|
||||||
Self(SendWrapper::new(value))
|
pub(crate) request: Request,
|
||||||
|
pub(crate) abort_ctrl: Option<AbortOnDrop>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct AbortOnDrop(AbortController);
|
||||||
|
|
||||||
|
impl Drop for AbortOnDrop {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BrowserRequest> for Request {
|
impl From<BrowserRequest> for Request {
|
||||||
fn from(value: BrowserRequest) -> Self {
|
fn from(value: BrowserRequest) -> Self {
|
||||||
value.0.take()
|
value.0.take().request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BrowserRequest> for web_sys::Request {
|
impl From<BrowserRequest> for web_sys::Request {
|
||||||
fn from(value: BrowserRequest) -> Self {
|
fn from(value: BrowserRequest) -> Self {
|
||||||
value.0.take().into()
|
value.0.take().request.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,13 +48,13 @@ impl Deref for BrowserRequest {
|
||||||
type Target = Request;
|
type Target = Request;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
self.0.deref()
|
&self.0.deref().request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for BrowserRequest {
|
impl DerefMut for BrowserRequest {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
self.0.deref_mut()
|
&mut self.0.deref_mut().request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +68,12 @@ impl From<FormData> for BrowserFormData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn abort_signal() -> (Option<AbortOnDrop>, Option<AbortSignal>) {
|
||||||
|
let ctrl = AbortController::new().ok();
|
||||||
|
let signal = ctrl.as_ref().map(|ctrl| ctrl.signal());
|
||||||
|
(ctrl.map(AbortOnDrop), signal)
|
||||||
|
}
|
||||||
|
|
||||||
impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
type FormData = BrowserFormData;
|
type FormData = BrowserFormData;
|
||||||
|
|
||||||
|
@ -65,6 +83,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let server_url = get_server_url();
|
let server_url = get_server_url();
|
||||||
let mut url = String::with_capacity(
|
let mut url = String::with_capacity(
|
||||||
server_url.len() + path.len() + 1 + query.len(),
|
server_url.len() + path.len() + 1 + query.len(),
|
||||||
|
@ -73,13 +92,15 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
url.push_str(path);
|
url.push_str(path);
|
||||||
url.push('?');
|
url.push('?');
|
||||||
url.push_str(query);
|
url.push_str(query);
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::get(&url)
|
request: Request::get(&url)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", content_type)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_post(
|
fn try_new_post(
|
||||||
|
@ -88,17 +109,20 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: String,
|
body: String,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let server_url = get_server_url();
|
let server_url = get_server_url();
|
||||||
let mut url = String::with_capacity(server_url.len() + path.len());
|
let mut url = String::with_capacity(server_url.len() + path.len());
|
||||||
url.push_str(server_url);
|
url.push_str(server_url);
|
||||||
url.push_str(path);
|
url.push_str(path);
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::post(&url)
|
request: Request::post(&url)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", content_type)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.body(body)
|
.body(body)
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_post_bytes(
|
fn try_new_post_bytes(
|
||||||
|
@ -107,19 +131,22 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: Bytes,
|
body: Bytes,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let server_url = get_server_url();
|
let server_url = get_server_url();
|
||||||
let mut url = String::with_capacity(server_url.len() + path.len());
|
let mut url = String::with_capacity(server_url.len() + path.len());
|
||||||
url.push_str(server_url);
|
url.push_str(server_url);
|
||||||
url.push_str(path);
|
url.push_str(path);
|
||||||
let body: &[u8] = &body;
|
let body: &[u8] = &body;
|
||||||
let body = Uint8Array::from(body).buffer();
|
let body = Uint8Array::from(body).buffer();
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::post(&url)
|
request: Request::post(&url)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", content_type)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.body(body)
|
.body(body)
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_multipart(
|
fn try_new_multipart(
|
||||||
|
@ -127,16 +154,19 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
accepts: &str,
|
accepts: &str,
|
||||||
body: Self::FormData,
|
body: Self::FormData,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let server_url = get_server_url();
|
let server_url = get_server_url();
|
||||||
let mut url = String::with_capacity(server_url.len() + path.len());
|
let mut url = String::with_capacity(server_url.len() + path.len());
|
||||||
url.push_str(server_url);
|
url.push_str(server_url);
|
||||||
url.push_str(path);
|
url.push_str(path);
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::post(&url)
|
request: Request::post(&url)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.body(body.0.take())
|
.body(body.0.take())
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_post_form_data(
|
fn try_new_post_form_data(
|
||||||
|
@ -145,6 +175,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: Self::FormData,
|
body: Self::FormData,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let form_data = body.0.take();
|
let form_data = body.0.take();
|
||||||
let url_params =
|
let url_params =
|
||||||
UrlSearchParams::new_with_str_sequence_sequence(&form_data)
|
UrlSearchParams::new_with_str_sequence_sequence(&form_data)
|
||||||
|
@ -156,13 +187,15 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::post(path)
|
request: Request::post(path)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", content_type)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.body(url_params)
|
.body(url_params)
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_streaming(
|
fn try_new_streaming(
|
||||||
|
@ -171,9 +204,13 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: impl Stream<Item = Bytes> + 'static,
|
body: impl Stream<Item = Bytes> + 'static,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
// TODO abort signal
|
||||||
let req = streaming_request(path, accepts, content_type, body)
|
let req = streaming_request(path, accepts, content_type, body)
|
||||||
.map_err(|e| ServerFnError::Request(format!("{e:?}")))?;
|
.map_err(|e| ServerFnError::Request(format!("{e:?}")))?;
|
||||||
Ok(Self(SendWrapper::new(req)))
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
|
request: req,
|
||||||
|
abort_ctrl: None,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +220,7 @@ fn streaming_request(
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: impl Stream<Item = Bytes> + 'static,
|
body: impl Stream<Item = Bytes> + 'static,
|
||||||
) -> Result<Request, JsValue> {
|
) -> Result<Request, JsValue> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let stream = ReadableStream::from_stream(body.map(|bytes| {
|
let stream = ReadableStream::from_stream(body.map(|bytes| {
|
||||||
let data = Uint8Array::from(bytes.as_ref());
|
let data = Uint8Array::from(bytes.as_ref());
|
||||||
let data = JsValue::from(data);
|
let data = JsValue::from(data);
|
||||||
|
|
|
@ -2,6 +2,7 @@ use super::{Attribute, NextAttribute};
|
||||||
use crate::renderer::Renderer;
|
use crate::renderer::Renderer;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
|
fmt::Debug,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,6 +18,15 @@ pub struct AnyAttribute<R: Renderer> {
|
||||||
fn(Box<dyn Any>, &R::Element) -> AnyAttributeState<R>,
|
fn(Box<dyn Any>, &R::Element) -> AnyAttributeState<R>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R> Debug for AnyAttribute<R>
|
||||||
|
where
|
||||||
|
R: Renderer,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("AnyAttribute").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AnyAttributeState<R>
|
pub struct AnyAttributeState<R>
|
||||||
where
|
where
|
||||||
R: Renderer,
|
R: Renderer,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
renderer::DomRenderer,
|
renderer::DomRenderer,
|
||||||
view::{Position, ToTemplate},
|
view::{Position, ToTemplate},
|
||||||
};
|
};
|
||||||
use std::marker::PhantomData;
|
use std::{marker::PhantomData, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn class<C, R>(class: C) -> Class<C, R>
|
pub fn class<C, R>(class: C) -> Class<C, R>
|
||||||
|
@ -114,7 +114,7 @@ impl<'a, R> IntoClass<R> for &'a str
|
||||||
where
|
where
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
type State = (R::Element, &'a str);
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
fn to_html(self, class: &mut String) {
|
fn to_html(self, class: &mut String) {
|
||||||
class.push_str(self);
|
class.push_str(self);
|
||||||
|
@ -145,7 +145,7 @@ impl<R> IntoClass<R> for String
|
||||||
where
|
where
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
type State = (R::Element, String);
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
fn to_html(self, class: &mut String) {
|
fn to_html(self, class: &mut String) {
|
||||||
IntoClass::<R>::to_html(self.as_str(), class);
|
IntoClass::<R>::to_html(self.as_str(), class);
|
||||||
|
@ -172,6 +172,68 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R> IntoClass<R> for Rc<str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, class: &mut String) {
|
||||||
|
IntoClass::<R>::to_html(self.as_ref(), class);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &R::Element) -> Self::State {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
let (el, prev) = state;
|
||||||
|
if !Rc::ptr_eq(&self, prev) {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
*prev = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> IntoClass<R> for Arc<str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, class: &mut String) {
|
||||||
|
IntoClass::<R>::to_html(self.as_ref(), class);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &R::Element) -> Self::State {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
let (el, prev) = state;
|
||||||
|
if !Arc::ptr_eq(&self, prev) {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
*prev = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<R> IntoClass<R> for (&'static str, bool)
|
impl<R> IntoClass<R> for (&'static str, bool)
|
||||||
where
|
where
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
|
|
|
@ -4,12 +4,12 @@ use crate::{
|
||||||
renderer::{DomRenderer, Renderer},
|
renderer::{DomRenderer, Renderer},
|
||||||
view::add_attr::AddAnyAttr,
|
view::add_attr::AddAnyAttr,
|
||||||
};
|
};
|
||||||
use std::marker::PhantomData;
|
use std::{marker::PhantomData, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn inner_html<T, R>(value: T) -> InnerHtml<T, R>
|
pub fn inner_html<T, R>(value: T) -> InnerHtml<T, R>
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: InnerHtmlValue<R>,
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
InnerHtml {
|
InnerHtml {
|
||||||
|
@ -21,8 +21,8 @@ where
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct InnerHtml<T, R>
|
pub struct InnerHtml<T, R>
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: InnerHtmlValue<R>,
|
||||||
R: Renderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
value: T,
|
value: T,
|
||||||
rndr: PhantomData<R>,
|
rndr: PhantomData<R>,
|
||||||
|
@ -30,12 +30,12 @@ where
|
||||||
|
|
||||||
impl<T, R> Attribute<R> for InnerHtml<T, R>
|
impl<T, R> Attribute<R> for InnerHtml<T, R>
|
||||||
where
|
where
|
||||||
T: AsRef<str> + PartialEq,
|
T: InnerHtmlValue<R>,
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
const MIN_LENGTH: usize = 0;
|
const MIN_LENGTH: usize = 0;
|
||||||
|
|
||||||
type State = (R::Element, T);
|
type State = T::State;
|
||||||
|
|
||||||
fn to_html(
|
fn to_html(
|
||||||
self,
|
self,
|
||||||
|
@ -44,33 +44,28 @@ where
|
||||||
_style: &mut String,
|
_style: &mut String,
|
||||||
inner_html: &mut String,
|
inner_html: &mut String,
|
||||||
) {
|
) {
|
||||||
inner_html.push_str(self.value.as_ref());
|
self.value.to_html(inner_html);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
self,
|
self,
|
||||||
el: &<R as Renderer>::Element,
|
el: &<R as Renderer>::Element,
|
||||||
) -> Self::State {
|
) -> Self::State {
|
||||||
(el.clone(), self.value)
|
self.value.hydrate::<FROM_SERVER>(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
R::set_inner_html(el, self.value.as_ref());
|
self.value.build(el)
|
||||||
(el.clone(), self.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild(self, state: &mut Self::State) {
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
let (el, prev) = state;
|
self.value.rebuild(state);
|
||||||
if self.value != *prev {
|
|
||||||
R::set_inner_html(el, self.value.as_ref());
|
|
||||||
*prev = self.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, R> NextAttribute<R> for InnerHtml<T, R>
|
impl<T, R> NextAttribute<R> for InnerHtml<T, R>
|
||||||
where
|
where
|
||||||
T: AsRef<str> + PartialEq,
|
T: InnerHtmlValue<R>,
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
type Output<NewAttr: Attribute<R>> = (Self, NewAttr);
|
type Output<NewAttr: Attribute<R>> = (Self, NewAttr);
|
||||||
|
@ -85,7 +80,7 @@ where
|
||||||
|
|
||||||
pub trait InnerHtmlAttribute<T, Rndr>
|
pub trait InnerHtmlAttribute<T, Rndr>
|
||||||
where
|
where
|
||||||
T: AsRef<str> + PartialEq,
|
T: InnerHtmlValue<Rndr>,
|
||||||
Rndr: DomRenderer,
|
Rndr: DomRenderer,
|
||||||
Self: Sized + AddAnyAttr<Rndr>,
|
Self: Sized + AddAnyAttr<Rndr>,
|
||||||
{
|
{
|
||||||
|
@ -103,7 +98,7 @@ where
|
||||||
Self: AddAnyAttr<Rndr>,
|
Self: AddAnyAttr<Rndr>,
|
||||||
E: ElementWithChildren,
|
E: ElementWithChildren,
|
||||||
At: Attribute<Rndr>,
|
At: Attribute<Rndr>,
|
||||||
T: AsRef<str> + PartialEq,
|
T: InnerHtmlValue<Rndr>,
|
||||||
Rndr: DomRenderer,
|
Rndr: DomRenderer,
|
||||||
{
|
{
|
||||||
fn inner_html(
|
fn inner_html(
|
||||||
|
@ -113,3 +108,188 @@ where
|
||||||
self.add_any_attr(inner_html(value))
|
self.add_any_attr(inner_html(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait InnerHtmlValue<R: DomRenderer> {
|
||||||
|
type State;
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String);
|
||||||
|
|
||||||
|
fn to_template(buf: &mut String);
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State;
|
||||||
|
|
||||||
|
fn build(self, el: &R::Element) -> Self::State;
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> InnerHtmlValue<R> for String
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
buf.push_str(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
if self != state.1 {
|
||||||
|
R::set_inner_html(&state.0, &self);
|
||||||
|
state.1 = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> InnerHtmlValue<R> for Rc<str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
buf.push_str(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
if !Rc::ptr_eq(&self, &state.1) {
|
||||||
|
R::set_inner_html(&state.0, &self);
|
||||||
|
state.1 = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> InnerHtmlValue<R> for Arc<str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
buf.push_str(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
if !Arc::ptr_eq(&self, &state.1) {
|
||||||
|
R::set_inner_html(&state.0, &self);
|
||||||
|
state.1 = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, R> InnerHtmlValue<R> for &'a str
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
buf.push_str(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_inner_html(el, self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
R::set_inner_html(el, self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
if self != state.1 {
|
||||||
|
R::set_inner_html(&state.0, self);
|
||||||
|
state.1 = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, R> InnerHtmlValue<R> for Option<T>
|
||||||
|
where
|
||||||
|
T: InnerHtmlValue<R>,
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = Option<T::State>;
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
if let Some(value) = self {
|
||||||
|
value.to_html(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
self.map(|n| n.hydrate::<FROM_SERVER>(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
self.map(|n| n.build(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
html::attribute::AttributeValue,
|
html::{attribute::AttributeValue, class::IntoClass},
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
prelude::{Mountable, Render, RenderHtml},
|
prelude::{Mountable, Render, RenderHtml},
|
||||||
renderer::Renderer,
|
renderer::{DomRenderer, Renderer},
|
||||||
view::{strings::StrState, Position, PositionState, ToTemplate},
|
view::{strings::StrState, Position, PositionState, ToTemplate},
|
||||||
};
|
};
|
||||||
use oco::Oco;
|
use oco::Oco;
|
||||||
|
@ -142,3 +142,34 @@ where
|
||||||
*prev_value = self;
|
*prev_value = self;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R> IntoClass<R> for Oco<'static, str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, class: &mut String) {
|
||||||
|
IntoClass::<R>::to_html(self.as_str(), class);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &R::Element) -> Self::State {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
let (el, prev) = state;
|
||||||
|
if self != *prev {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
*prev = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use crate::{
|
||||||
error::AnyError,
|
error::AnyError,
|
||||||
html::{
|
html::{
|
||||||
attribute::{Attribute, AttributeValue},
|
attribute::{Attribute, AttributeValue},
|
||||||
|
element::InnerHtmlValue,
|
||||||
property::IntoProperty,
|
property::IntoProperty,
|
||||||
},
|
},
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
|
@ -554,6 +555,56 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<F, V, R> InnerHtmlValue<R> for F
|
||||||
|
where
|
||||||
|
F: FnMut() -> V + 'static,
|
||||||
|
V: InnerHtmlValue<R>,
|
||||||
|
V::State: 'static,
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = RenderEffectState<V::State>;
|
||||||
|
|
||||||
|
fn to_html(mut self, buf: &mut String) {
|
||||||
|
let value = self();
|
||||||
|
value.to_html(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
mut self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
let el = el.to_owned();
|
||||||
|
RenderEffect::new(move |prev| {
|
||||||
|
let value = self();
|
||||||
|
if let Some(mut state) = prev {
|
||||||
|
value.rebuild(&mut state);
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
value.hydrate::<FROM_SERVER>(&el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(mut self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
let el = el.to_owned();
|
||||||
|
RenderEffect::new(move |prev| {
|
||||||
|
let value = self();
|
||||||
|
if let Some(mut state) = prev {
|
||||||
|
value.rebuild(&mut state);
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
value.build(&el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, _state: &mut Self::State) {}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{Mountable, Position, PositionState, Render, RenderHtml};
|
use super::{Mountable, Position, PositionState, Render, RenderHtml};
|
||||||
use crate::{hydration::Cursor, renderer::Renderer};
|
use crate::{hydration::Cursor, renderer::Renderer, ssr::StreamBuilder};
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
|
@ -12,8 +12,9 @@ where
|
||||||
{
|
{
|
||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
value: Box<dyn Any>,
|
value: Box<dyn Any>,
|
||||||
// TODO add async HTML rendering for AnyView
|
|
||||||
to_html: fn(Box<dyn Any>, &mut String, &mut Position),
|
to_html: fn(Box<dyn Any>, &mut String, &mut Position),
|
||||||
|
to_html_async: fn(Box<dyn Any>, &mut StreamBuilder, &mut Position),
|
||||||
|
to_html_async_ooo: fn(Box<dyn Any>, &mut StreamBuilder, &mut Position),
|
||||||
build: fn(Box<dyn Any>) -> AnyViewState<R>,
|
build: fn(Box<dyn Any>) -> AnyViewState<R>,
|
||||||
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState<R>),
|
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState<R>),
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
|
@ -122,6 +123,24 @@ where
|
||||||
.expect("AnyView::to_html could not be downcast");
|
.expect("AnyView::to_html could not be downcast");
|
||||||
value.to_html_with_buf(buf, position);
|
value.to_html_with_buf(buf, position);
|
||||||
};
|
};
|
||||||
|
let to_html_async =
|
||||||
|
|value: Box<dyn Any>,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position| {
|
||||||
|
let value = value
|
||||||
|
.downcast::<T>()
|
||||||
|
.expect("AnyView::to_html could not be downcast");
|
||||||
|
value.to_html_async_with_buf::<false>(buf, position);
|
||||||
|
};
|
||||||
|
let to_html_async_ooo =
|
||||||
|
|value: Box<dyn Any>,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position| {
|
||||||
|
let value = value
|
||||||
|
.downcast::<T>()
|
||||||
|
.expect("AnyView::to_html could not be downcast");
|
||||||
|
value.to_html_async_with_buf::<true>(buf, position);
|
||||||
|
};
|
||||||
let build = |value: Box<dyn Any>| {
|
let build = |value: Box<dyn Any>| {
|
||||||
let value = value
|
let value = value
|
||||||
.downcast::<T>()
|
.downcast::<T>()
|
||||||
|
@ -198,6 +217,8 @@ where
|
||||||
type_id: TypeId::of::<T>(),
|
type_id: TypeId::of::<T>(),
|
||||||
value,
|
value,
|
||||||
to_html,
|
to_html,
|
||||||
|
to_html_async,
|
||||||
|
to_html_async_ooo,
|
||||||
build,
|
build,
|
||||||
rebuild,
|
rebuild,
|
||||||
hydrate_from_server,
|
hydrate_from_server,
|
||||||
|
@ -243,6 +264,20 @@ where
|
||||||
(self.to_html)(self.value, buf, position);
|
(self.to_html)(self.value, buf, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||||
|
self,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
if OUT_OF_ORDER {
|
||||||
|
(self.to_html_async_ooo)(self.value, buf, position);
|
||||||
|
} else {
|
||||||
|
(self.to_html_async)(self.value, buf, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
self,
|
self,
|
||||||
cursor: &Cursor<R>,
|
cursor: &Cursor<R>,
|
||||||
|
|
Loading…
Reference in New Issue