work related to 0.7 blog port

This commit is contained in:
Greg Johnston 2024-03-16 16:33:15 -04:00
parent c29081b12a
commit b41fde3ff9
49 changed files with 2775 additions and 507 deletions

View File

@ -25,7 +25,7 @@ use futures::Stream;
pub use hydrate::*;
use serde::{Deserialize, Serialize};
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`].
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;

View File

@ -98,7 +98,8 @@ impl SharedContext for SsrSharedContext {
.into_iter()
.map(|(id, data)| async move {
let data = data.await;
format!("__RESOLVED_RESOURCES[{}] = {data:?};", id.0)
let data = data.replace('<', "\\u003c");
format!("__RESOLVED_RESOURCES[{}] = {:?};", id.0, data)
})
.collect::<FuturesUnordered<_>>();

View File

@ -9,34 +9,33 @@ description = "Axum integrations for the Leptos web framework."
rust-version.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.7", default-features = false, features = [
"matched-path",
] }
futures = "0.3"
http-body-util = "0.1"
leptos = { workspace = true, features = ["ssr"] }
leptos = { workspace = true, features = ["nonce", "hydration"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
routing = { workspace = true }
#leptos_integration_utils = { workspace = true }
parking_lot = "0.12"
serde_json = "1"
tokio = { version = "1", default-features = false }
tokio-util = { version = "0.7", features = ["rt"] }
tracing = "0.1"
once_cell = "1.18"
cfg-if = "1.0"
[dev-dependencies]
axum = "0.7"
tokio = { version = "1", features = ["net"] }
[features]
nonce = ["leptos/nonce"]
wasm = []
default = ["tokio/fs", "tokio/sync"]
experimental-islands = ["leptos_integration_utils/experimental-islands"]
#experimental-islands = ["leptos_integration_utils/experimental-islands"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ rust-version.workspace = true
[dependencies]
futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos = { workspace = true }
leptos_hot_reload = { workspace = true }
leptos_meta = { workspace = true }
leptos_config = { workspace = true }

View File

@ -7,20 +7,23 @@ license = "MIT"
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."
readme = "../README.md"
rust-version.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
base64 = { version = "0.22", optional = true }
cfg-if = "1"
hydration_context = { workspace = true, optional = true }
leptos_dom = { workspace = true }
leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
leptos-spin-macro = { git = "https://github.com/fermyon/leptos-spin", optional = true }
oco = { workspace = true }
leptos-spin-macro = { version = "0.1", optional = true }
paste = "1"
rand = { version = "0.8", optional = true }
reactive_graph = { workspace = true, features = ["serde"] }
tachys = { workspace = true, features = ["oco", "reactive_graph"] }
tachys = { workspace = true, features = ["reactive_graph"] }
tracing = "0.1"
typed-builder = "0.18"
typed-builder-macro = "0.18"
@ -37,7 +40,7 @@ web-sys = { version = "0.3.63", features = [
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = { version = "0.2" }
wasm-bindgen = { version = "0.2", optional = true }
[features]
default = ["serde"]
@ -94,6 +97,8 @@ denylist = [
"wasm-bindgen",
"rkyv", # was causing clippy issues on nightly
"trace-component-props",
"spin",
"experimental-islands",
]
skip_feature_sets = [
[

View File

@ -156,6 +156,8 @@ pub mod children;
pub mod component;
mod for_loop;
mod hydration_scripts;
#[cfg(feature = "nonce")]
pub mod nonce;
mod show;
pub mod text_prop;
pub use for_loop::*;
@ -165,7 +167,7 @@ pub use reactive_graph::{
self,
signal::{arc_signal, create_signal, signal},
};
pub use server_fn::error;
pub use server_fn::{self, error};
pub use show::*;
#[doc(hidden)]
pub use typed_builder;
@ -175,12 +177,28 @@ mod into_view;
pub use into_view::IntoView;
pub use leptos_dom;
pub use tachys;
pub mod logging;
mod mount;
pub mod mount;
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 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;
pub use additional_attributes::*;
mod await_;

View File

@ -4,11 +4,59 @@ use reactive_graph::owner::Owner;
use std::marker::PhantomData;
use tachys::{
dom::body,
hydration::Cursor,
renderer::{dom::Dom, Renderer},
view::{Mountable, Render},
view::{Mountable, PositionState, Render, RenderHtml},
};
use wasm_bindgen::JsCast;
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>`.
pub fn mount_to_body<F, N>(f: F)
where

128
leptos/src/nonce.rs Normal file
View File

@ -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()
}
}

View File

@ -13,6 +13,10 @@ use web_sys::HtmlElement;
pub mod helpers;
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(unboxed_closures))]
// to prevent warnings from popping up when a nightly feature is stabilized

View File

@ -1,5 +1,5 @@
use crate::is_server;
use cfg_if::cfg_if;
//! Utilities for simple isomorphic logging to the console or terminal.
use wasm_bindgen::JsValue;
/// 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)
/// or via `println!()` (if not in the browser).
pub fn console_log(s: &str) {
#[allow(clippy::print_stdout)]
if is_server() {
if log_to_stdout() {
println!("{s}");
} else {
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)
/// or via `println!()` (if not in the browser).
pub fn console_warn(s: &str) {
if is_server() {
if log_to_stdout() {
eprintln!("{s}");
} else {
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)
/// or via `println!()` (if not in the browser).
#[inline(always)]
pub fn console_error(s: &str) {
if is_server() {
if log_to_stdout() {
eprintln!("{s}");
} else {
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)
/// or via `println!()` (if not in the browser), but only in a debug build.
#[inline(always)]
pub fn console_debug_warn(s: &str) {
cfg_if! {
if #[cfg(debug_assertions)] {
if is_server() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
#[cfg(debug_assertions)]
{
if log_to_stdout() {
eprintln!("{s}");
} else {
let _ = s;
web_sys::console::warn_1(&JsValue::from_str(s));
}
}
#[cfg(not(debug_assertions))]
{
let _ = s;
}
}

View File

@ -10,14 +10,27 @@ readme = "../README.md"
rust-version.workspace = true
[dependencies]
reactive_graph = { workspace = true }
leptos_macro = { workspace = true }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
#leptos_macro = { workspace = true }
server_fn = { workspace = true }
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
#lazy_static = "1"
thiserror = "1"
tracing = "0.1"
inventory = "0.3"
tracing = { version = "0.1", optional = true }
#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]
leptos = { path = "../leptos" }
@ -25,6 +38,9 @@ leptos = { path = "../leptos" }
[features]
default-tls = ["server_fn/default-tls"]
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]
denylist = ["nightly"]

View File

@ -1,6 +1,13 @@
//#![deny(missing_docs)]
#![forbid(unsafe_code)]
#[cfg(feature = "hydration")]
mod resource;
#[cfg(feature = "hydration")]
pub mod serializers;
#[cfg(feature = "hydration")]
pub use resource::*;
////! # Leptos Server Functions
////!
////! This package is based on a simple idea: sometimes its useful to write functions

View File

@ -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()
}
}

View File

@ -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::*;

View File

@ -16,6 +16,7 @@ indexmap = "2"
send_wrapper = "0.6.0"
tracing = "0.1"
wasm-bindgen = "0.2"
futures = "0.3.30"
[dependencies.web-sys]
version = "0.3"

View File

@ -7,9 +7,14 @@ use leptos::{
tachys::{
dom::document,
error::Result,
html::attribute::{
any_attribute::{AnyAttribute, AnyAttributeState},
Attribute,
html::{
attribute::{
any_attribute::{
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
},
Attribute,
},
class,
},
hydration::Cursor,
reactive_graph::RenderEffectState,
@ -57,10 +62,17 @@ use web_sys::HtmlElement;
/// ```
#[component]
pub fn Body(
/// The `class` attribute on the `<body>`.
#[prop(optional, into)]
mut class: Option<TextProp>,
/// Arbitrary attributes to add to the `<body>`.
#[prop(attrs)]
mut attributes: Vec<AnyAttribute<Dom>>,
) -> 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>() {
let mut meta = meta.inner.write().or_poisoned();
// if we are server rendering, we will not actually use these values via RenderHtml

View File

@ -7,9 +7,15 @@ use leptos::{
tachys::{
dom::document,
error::Result,
html::attribute::{
any_attribute::{AnyAttribute, AnyAttributeState},
Attribute,
html::{
attribute::{
self,
any_attribute::{
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
},
Attribute,
},
class,
},
hydration::Cursor,
reactive_graph::RenderEffectState,
@ -54,10 +60,30 @@ use web_sys::{Element, HtmlElement};
/// ```
#[component]
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>`
#[prop(attrs)]
mut attributes: Vec<AnyAttribute<Dom>>,
) -> 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>() {
let mut meta = meta.inner.write().or_poisoned();
// if we are server rendering, we will not actually use these values via RenderHtml

View File

@ -47,9 +47,10 @@
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
use indexmap::IndexMap;
use futures::{Stream, StreamExt};
use leptos::{
component, debug_warn,
component,
logging::debug_warn,
reactive_graph::owner::{provide_context, use_context},
tachys::{
dom::document,
@ -67,13 +68,12 @@ use once_cell::sync::Lazy;
use or_poisoned::OrPoisoned;
use send_wrapper::SendWrapper;
use std::{
cell::{Cell, RefCell},
fmt::Debug,
rc::Rc,
pin::Pin,
sync::{Arc, RwLock},
};
use wasm_bindgen::JsCast;
use web_sys::{HtmlHeadElement, Node};
use web_sys::HtmlHeadElement;
mod body;
mod html;
@ -157,7 +157,61 @@ pub struct ServerMetaContext {
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 {
/*/// Metadata associated with the `<html>` element
pub html: HtmlContext,
@ -173,7 +227,9 @@ struct ServerMetaContextInner {
impl Debug for ServerMetaContext {
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 {
context: ServerMetaContext,
}
@ -399,14 +456,7 @@ impl RenderHtml<Dom> for MetaTagsView {
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
if let Some(title) = self.context.title.as_string() {
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);
buf.push_str("<!--HEAD-->");
}
fn hydrate<const FROM_SERVER: bool>(

View File

@ -7,6 +7,7 @@ version.workspace = true
any_spawner = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3"
hydration_context = { workspace = true, optional = true }
pin-project-lite = "0.2"
rustc-hash = "1.1.0"
serde = { version = "1", features = ["derive"], optional = true }
@ -23,6 +24,7 @@ any_spawner = { workspace = true, features = ["tokio"] }
nightly = []
serde = ["dep:serde"]
tracing = ["dep:tracing"]
hydration = ["dep:hydration_context"]
[package.metadata.docs.rs]
all-features = true

View File

@ -71,7 +71,7 @@ impl<T> DefinedAt for ArcAsyncDerived<T> {
// This helps create a derived async signal.
// 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.
macro_rules! spawn_derived {
($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
where
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
Self::new_with_initial(AsyncState::Loading, fun)
}
@ -186,7 +186,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
) -> Self
where
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
spawn_derived!(Executor::spawn, initial_value, fun)
}

View File

@ -55,7 +55,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
) -> Self
where
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
Self {
#[cfg(debug_assertions)]

View File

@ -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)]
pub fn new(
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) {
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
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) {
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) {
self.write().or_poisoned().sources.insert(source);
}

View File

@ -12,6 +12,17 @@ pub struct Memo<T: Send + Sync + 'static> {
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> {
#[track_caller]
#[cfg_attr(

View File

@ -84,6 +84,7 @@ pub mod selector;
mod serde;
pub mod signal;
pub mod traits;
pub mod wrappers;
pub use graph::untrack;

View File

@ -4,10 +4,11 @@ use crate::{
ArcReadSignal, ArcRwSignal, ArcWriteSignal, ReadSignal, RwSignal,
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))?),*) => {
$(
#[cfg(feature = "nightly")]
@ -16,7 +17,7 @@ macro_rules! impl_get_fn_traits {
#[inline(always)]
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> {
#[inline(always)]
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> {
#[inline(always)]
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 {
($($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))?),*) => {
$(
#[cfg(feature = "nightly")]
@ -92,7 +131,7 @@ macro_rules! impl_get_fn_traits_send {
#[inline(always)]
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> {
#[inline(always)]
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> {
#[inline(always)]
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 {
($($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_send![ReadSignal, RwSignal, Memo, ArcMemo];
impl_get_fn_traits_read![ArcReadSignal, ArcRwSignal];
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_send![WriteSignal];

View File

@ -1,3 +1,5 @@
#[cfg(feature = "hydration")]
use hydration_context::SharedContext;
use or_poisoned::OrPoisoned;
use rustc_hash::FxHashMap;
use std::{
@ -18,6 +20,8 @@ pub use context::*;
#[must_use]
pub struct Owner {
pub(crate) inner: Arc<RwLock<OwnerInner>>,
#[cfg(feature = "hydration")]
pub(crate) shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
}
thread_local! {
@ -25,9 +29,21 @@ thread_local! {
}
impl Owner {
pub fn debug_id(&self) -> usize {
Arc::as_ptr(&self.inner) as usize
}
pub fn new() -> Self {
#[cfg(not(feature = "hydration"))]
let parent = OWNER
.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 {
inner: Arc::new(RwLock::new(OwnerInner {
parent,
@ -35,11 +51,29 @@ impl Owner {
contexts: 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 {
let parent = Some(Arc::downgrade(&self.inner));
let parent = Some(Arc::clone(&self.inner));
Self {
inner: Arc::new(RwLock::new(OwnerInner {
parent,
@ -47,6 +81,8 @@ impl Owner {
contexts: 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> {
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)]
pub(crate) struct OwnerInner {
pub parent: Option<Weak<RwLock<OwnerInner>>>,
pub parent: Option<Arc<RwLock<OwnerInner>>>,
nodes: Vec<NodeId>,
pub contexts: FxHashMap<TypeId, Box<dyn Any + Send + Sync>>,
pub cleanups: Vec<Box<dyn FnOnce() + Send + Sync>>,

View File

@ -14,7 +14,7 @@ impl Owner {
fn use_context<T: Clone + 'static>(&self) -> Option<T> {
let ty = TypeId::of::<T>();
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;
if let Some(context) = contexts.get(&ty) {
context.downcast_ref::<T>().cloned()
@ -28,8 +28,7 @@ impl Owner {
if let Some(value) = downcast {
return Some(value);
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
parent = this_parent.parent.as_ref().map(|p| p.clone());
}
}
None

View File

@ -184,12 +184,10 @@ where
}
}
pub trait With: WithUntracked + Track {
#[track_caller]
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
self.track();
self.try_with_untracked(fun)
}
pub trait With: DefinedAt {
type Value: ?Sized;
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U>;
#[track_caller]
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 {}
pub trait GetUntracked: WithUntracked
impl<T> With for T
where
Self::Value: Clone,
T: WithUntracked + Track,
{
fn try_get_untracked(&self) -> Option<Self::Value> {
self.try_with_untracked(Self::Value::clone)
type Value = <T as WithUntracked>::Value;
#[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]
fn get_untracked(&self) -> Self::Value {
@ -219,20 +225,21 @@ where
T: WithUntracked,
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
where
Self::Value: Clone,
{
fn try_get(&self) -> Option<Self::Value> {
self.try_with(Self::Value::clone)
}
pub trait Get: DefinedAt {
type Value: Clone;
fn try_get(&self) -> Option<Self::Value>;
#[track_caller]
fn get(&self) -> Self::Value {
self.try_with(Self::Value::clone)
.unwrap_or_else(unwrap_signal!(self))
self.try_get().unwrap_or_else(unwrap_signal!(self))
}
}
@ -241,6 +248,11 @@ where
T: With,
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 {

View File

@ -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())
}
}
}

View File

@ -7,7 +7,7 @@ use std::{
};
use tachys::{renderer::Renderer, view::RenderHtml};
#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
/// A route that this application can serve.
pub struct RouteListing {
path: Vec<PathSegment>,
@ -64,6 +64,10 @@ impl RouteListing {
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
/// is not marked as statically rendered. All route parameters to use when resolving all paths
@ -100,6 +104,12 @@ impl RouteListing {
#[derive(Debug, Default)]
pub struct RouteList(Vec<RouteListing>);
impl From<Vec<RouteListing>> for RouteList {
fn from(value: Vec<RouteListing>) -> Self {
Self(value)
}
}
impl RouteList {
pub fn push(&mut self, data: RouteListing) {
self.0.push(data);

View File

@ -20,11 +20,6 @@ impl fmt::Debug for 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) {
if let Ok(hash) = window().location().hash() {
if !hash.is_empty() {
@ -50,6 +45,11 @@ impl BrowserUrl {
impl Location for BrowserUrl {
type Error = JsValue;
fn new() -> Result<Self, JsValue> {
let url = ArcRwSignal::new(Self::current()?);
Ok(Self { url })
}
fn as_url(&self) -> &ArcRwSignal<Url> {
&self.url
}

View File

@ -71,9 +71,11 @@ impl Default for LocationChange {
}
}
pub trait Location {
pub trait Location: Sized {
type Error: Debug;
fn new() -> Result<Self, Self::Error>;
fn as_url(&self) -> &ArcRwSignal<Url>;
fn current() -> Result<Url, Self::Error>;

View File

@ -11,6 +11,12 @@ impl RequestUrl {
}
}
impl AsRef<str> for RequestUrl {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Default for RequestUrl {
fn default() -> Self {
Self::new("/")
@ -18,7 +24,7 @@ impl Default for 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)
}

View File

@ -214,14 +214,12 @@ where
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
let mut segment_routes = Vec::new();
self.segments.generate_path(&mut segment_routes);
let segment_routes = segment_routes.into_iter();
let children_routes = self.children.as_ref().into_iter().flat_map(|child| child.generate_routes().into_iter());
children_routes.map(move |child_routes| {
segment_routes
.clone()
.chain(child_routes)
.filter(|seg| seg != &PathSegment::Unit)
.collect()
})
let children = self.children.as_ref();
match children {
None => Either::Left(iter::once(segment_routes)),
Some(children) => {
Either::Right(children.generate_routes().into_iter())
}
}
}
}

View File

@ -316,7 +316,7 @@ macro_rules! tuples {
}
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!(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);
@ -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!(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);
*/

View File

@ -7,3 +7,14 @@ pub enum PathSegment {
Param(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,
}
}
}

View File

@ -7,6 +7,12 @@ impl Params {
pub fn new() -> Self {
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

View File

@ -1,11 +1,12 @@
use crate::{
generate_route_list::RouteList,
location::Location,
location::{Location, RequestUrl},
matching::{
MatchInterface, MatchNestedRoutes, PossibleRouteMatch, RouteMatchId,
Routes,
},
ChooseView, MatchParams, Params,
ChooseView, MatchParams, Method, Params, PathSegment, RouteListing,
SsrMode,
};
use core::marker::PhantomData;
use either_of::*;
@ -13,17 +14,23 @@ use once_cell::unsync::Lazy;
use reactive_graph::{
computed::{ArcMemo, Memo},
effect::RenderEffect,
owner::Owner,
owner::{use_context, Owner},
signal::ArcRwSignal,
traits::{Get, Read, Set, Track},
};
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::{
html::attribute::Attribute,
hydration::Cursor,
renderer::Renderer,
renderer::{dom::Dom, Renderer},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
@ -36,7 +43,7 @@ use tachys::{
#[derive(Debug)]
pub struct Router<Rndr, Loc, Children, FallbackFn> {
base: Option<Cow<'static, str>>,
location: Loc,
location: PhantomData<Loc>,
pub routes: Routes<Children, Rndr>,
fallback: FallbackFn,
}
@ -49,13 +56,12 @@ where
FallbackFn: Fn() -> Fallback,
{
pub fn new(
location: Loc,
routes: Routes<Children, Rndr>,
fallback: FallbackFn,
) -> Router<Rndr, Loc, Children, FallbackFn> {
Self {
base: None,
location,
location: PhantomData,
routes,
fallback,
}
@ -63,13 +69,12 @@ where
pub fn new_with_base(
base: impl Into<Cow<'static, str>>,
location: Loc,
routes: Routes<Children, Rndr>,
fallback: FallbackFn,
) -> Router<Rndr, Loc, Children, FallbackFn> {
Self {
base: Some(base.into()),
location,
location: PhantomData,
routes,
fallback,
}
@ -87,7 +92,7 @@ where
}
}
pub struct RouteData<R>
pub struct RouteData<R = Dom>
where
R: Renderer + 'static,
{
@ -120,8 +125,9 @@ where
type FallibleState = (); // TODO
fn build(self) -> Self::State {
self.location.init(self.base);
let url = self.location.as_url().clone();
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()
@ -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>
where
Matcher: MatchInterface<R>,
@ -238,6 +416,53 @@ where
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>
@ -270,11 +495,11 @@ where
.map(|child| get_inner_view(outlets, &owner, child))
.unwrap_or_default();
let inner = Rc::new(RefCell::new(OutletStateInner {
state: Lazy::new({
let params = params.clone();
let owner = owner.clone();
Box::new(move || {
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 {
@ -282,10 +507,68 @@ where
outlet,
})
})
.into_any()
.build()
})
}),
.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(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 {
@ -332,9 +615,7 @@ where
id: RouteMatchId(0),
owner: Owner::current().unwrap(),
params: ArcRwSignal::new(Params::new()),
inner: Rc::new(RefCell::new(OutletStateInner {
state: Lazy::new(Box::new(|| ().into_any().build())),
})),
inner: Default::default(),
}
}
}
@ -373,7 +654,19 @@ where
const MIN_LENGTH: usize = 0; // TODO
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>(
@ -381,24 +674,41 @@ where
cursor: &Cursor<R>,
position: &PositionState,
) -> 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>
where
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>>>,
}
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>
where
R: Renderer + 'static,
{
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 {
view,
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>
where
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>
for Router<Rndr, Loc, Children, FallbackFn>
where

View File

@ -10,7 +10,7 @@ pub enum StaticMode {
}
// TODO
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct StaticDataMap;
impl StaticDataMap {

View File

@ -66,6 +66,8 @@ web-sys = { version = "0.3", optional = true, features = [
"console",
"ReadableStream",
"ReadableStreamDefaultReader",
"AbortController",
"AbortSignal"
] }
# reqwest client
@ -74,6 +76,7 @@ reqwest = { version = "0.12", default-features = false, optional = true, feature
"stream",
] }
url = "2"
pin-project-lite = "0.2.13"
[features]
default = ["json", "cbor"]

View File

@ -38,11 +38,19 @@ pub trait Client<CustErr> {
pub mod browser {
use super::Client;
use crate::{
error::ServerFnError, request::browser::BrowserRequest,
error::ServerFnError,
request::browser::{AbortOnDrop, BrowserRequest, RequestInner},
response::browser::BrowserResponse,
};
use gloo_net::{http::Response, Error};
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.
pub struct BrowserClient;
@ -56,8 +64,17 @@ pub mod browser {
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
SendWrapper::new(async move {
req.0
.take()
let mut req = req.0.take();
let RequestInner {
request,
abort_ctrl,
} = req;
/*BrowserRequestFuture {
request_fut: request.send(),
abort_ctrl,
cust_err: PhantomData,
}*/
request
.send()
.await
.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")]

View File

@ -8,27 +8,39 @@ use send_wrapper::SendWrapper;
use std::ops::{Deref, DerefMut};
use wasm_bindgen::JsValue;
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.
#[derive(Debug)]
pub struct BrowserRequest(pub(crate) SendWrapper<Request>);
pub struct BrowserRequest(pub(crate) SendWrapper<RequestInner>);
impl From<Request> for BrowserRequest {
fn from(value: Request) -> Self {
Self(SendWrapper::new(value))
#[derive(Debug)]
pub(crate) struct RequestInner {
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 {
fn from(value: BrowserRequest) -> Self {
value.0.take()
value.0.take().request
}
}
impl From<BrowserRequest> for web_sys::Request {
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;
fn deref(&self) -> &Self::Target {
self.0.deref()
&self.0.deref().request
}
}
impl DerefMut for BrowserRequest {
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 {
type FormData = BrowserFormData;
@ -65,6 +83,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
query: &str,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(
server_url.len() + path.len() + 1 + query.len(),
@ -73,13 +92,15 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
url.push_str(path);
url.push('?');
url.push_str(query);
Ok(Self(SendWrapper::new(
Request::get(&url)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::get(&url)
.header("Content-Type", content_type)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_post(
@ -88,17 +109,20 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
body: String,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
url.push_str(server_url);
url.push_str(path);
Ok(Self(SendWrapper::new(
Request::post(&url)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::post(&url)
.header("Content-Type", content_type)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_post_bytes(
@ -107,19 +131,22 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
body: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
url.push_str(server_url);
url.push_str(path);
let body: &[u8] = &body;
let body = Uint8Array::from(body).buffer();
Ok(Self(SendWrapper::new(
Request::post(&url)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::post(&url)
.header("Content-Type", content_type)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_multipart(
@ -127,16 +154,19 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
accepts: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
url.push_str(server_url);
url.push_str(path);
Ok(Self(SendWrapper::new(
Request::post(&url)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::post(&url)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body.0.take())
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_post_form_data(
@ -145,6 +175,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let form_data = body.0.take();
let url_params =
UrlSearchParams::new_with_str_sequence_sequence(&form_data)
@ -156,13 +187,15 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
},
))
})?;
Ok(Self(SendWrapper::new(
Request::post(path)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::post(path)
.header("Content-Type", content_type)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(url_params)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_streaming(
@ -171,9 +204,13 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
body: impl Stream<Item = Bytes> + 'static,
) -> Result<Self, ServerFnError<CustErr>> {
// TODO abort signal
let req = streaming_request(path, accepts, content_type, body)
.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,
body: impl Stream<Item = Bytes> + 'static,
) -> Result<Request, JsValue> {
let (abort_ctrl, abort_signal) = abort_signal();
let stream = ReadableStream::from_stream(body.map(|bytes| {
let data = Uint8Array::from(bytes.as_ref());
let data = JsValue::from(data);

View File

@ -2,6 +2,7 @@ use super::{Attribute, NextAttribute};
use crate::renderer::Renderer;
use std::{
any::{Any, TypeId},
fmt::Debug,
marker::PhantomData,
};
@ -17,6 +18,15 @@ pub struct AnyAttribute<R: Renderer> {
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>
where
R: Renderer,

View File

@ -3,7 +3,7 @@ use crate::{
renderer::DomRenderer,
view::{Position, ToTemplate},
};
use std::marker::PhantomData;
use std::{marker::PhantomData, rc::Rc, sync::Arc};
#[inline(always)]
pub fn class<C, R>(class: C) -> Class<C, R>
@ -114,7 +114,7 @@ impl<'a, R> IntoClass<R> for &'a str
where
R: DomRenderer,
{
type State = (R::Element, &'a str);
type State = (R::Element, Self);
fn to_html(self, class: &mut String) {
class.push_str(self);
@ -145,7 +145,7 @@ impl<R> IntoClass<R> for String
where
R: DomRenderer,
{
type State = (R::Element, String);
type State = (R::Element, Self);
fn to_html(self, class: &mut String) {
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)
where
R: DomRenderer,

View File

@ -4,12 +4,12 @@ use crate::{
renderer::{DomRenderer, Renderer},
view::add_attr::AddAnyAttr,
};
use std::marker::PhantomData;
use std::{marker::PhantomData, rc::Rc, sync::Arc};
#[inline(always)]
pub fn inner_html<T, R>(value: T) -> InnerHtml<T, R>
where
T: AsRef<str>,
T: InnerHtmlValue<R>,
R: DomRenderer,
{
InnerHtml {
@ -21,8 +21,8 @@ where
#[derive(Debug, Clone, Copy)]
pub struct InnerHtml<T, R>
where
T: AsRef<str>,
R: Renderer,
T: InnerHtmlValue<R>,
R: DomRenderer,
{
value: T,
rndr: PhantomData<R>,
@ -30,12 +30,12 @@ where
impl<T, R> Attribute<R> for InnerHtml<T, R>
where
T: AsRef<str> + PartialEq,
T: InnerHtmlValue<R>,
R: DomRenderer,
{
const MIN_LENGTH: usize = 0;
type State = (R::Element, T);
type State = T::State;
fn to_html(
self,
@ -44,33 +44,28 @@ where
_style: &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>(
self,
el: &<R as Renderer>::Element,
) -> Self::State {
(el.clone(), self.value)
self.value.hydrate::<FROM_SERVER>(el)
}
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
R::set_inner_html(el, self.value.as_ref());
(el.clone(), self.value)
self.value.build(el)
}
fn rebuild(self, state: &mut Self::State) {
let (el, prev) = state;
if self.value != *prev {
R::set_inner_html(el, self.value.as_ref());
*prev = self.value;
}
self.value.rebuild(state);
}
}
impl<T, R> NextAttribute<R> for InnerHtml<T, R>
where
T: AsRef<str> + PartialEq,
T: InnerHtmlValue<R>,
R: DomRenderer,
{
type Output<NewAttr: Attribute<R>> = (Self, NewAttr);
@ -85,7 +80,7 @@ where
pub trait InnerHtmlAttribute<T, Rndr>
where
T: AsRef<str> + PartialEq,
T: InnerHtmlValue<Rndr>,
Rndr: DomRenderer,
Self: Sized + AddAnyAttr<Rndr>,
{
@ -103,7 +98,7 @@ where
Self: AddAnyAttr<Rndr>,
E: ElementWithChildren,
At: Attribute<Rndr>,
T: AsRef<str> + PartialEq,
T: InnerHtmlValue<Rndr>,
Rndr: DomRenderer,
{
fn inner_html(
@ -113,3 +108,188 @@ where
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!()
}
}

View File

@ -1,8 +1,8 @@
use crate::{
html::attribute::AttributeValue,
html::{attribute::AttributeValue, class::IntoClass},
hydration::Cursor,
prelude::{Mountable, Render, RenderHtml},
renderer::Renderer,
renderer::{DomRenderer, Renderer},
view::{strings::StrState, Position, PositionState, ToTemplate},
};
use oco::Oco;
@ -142,3 +142,34 @@ where
*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;
}
}

View File

@ -3,6 +3,7 @@ use crate::{
error::AnyError,
html::{
attribute::{Attribute, AttributeValue},
element::InnerHtmlValue,
property::IntoProperty,
},
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)]
mod tests {

View File

@ -1,5 +1,5 @@
use super::{Mountable, Position, PositionState, Render, RenderHtml};
use crate::{hydration::Cursor, renderer::Renderer};
use crate::{hydration::Cursor, renderer::Renderer, ssr::StreamBuilder};
use std::{
any::{Any, TypeId},
fmt::Debug,
@ -12,8 +12,9 @@ where
{
type_id: TypeId,
value: Box<dyn Any>,
// TODO add async HTML rendering for AnyView
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>,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState<R>),
#[allow(clippy::type_complexity)]
@ -122,6 +123,24 @@ where
.expect("AnyView::to_html could not be downcast");
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 value = value
.downcast::<T>()
@ -198,6 +217,8 @@ where
type_id: TypeId::of::<T>(),
value,
to_html,
to_html_async,
to_html_async_ooo,
build,
rebuild,
hydrate_from_server,
@ -243,6 +264,20 @@ where
(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>(
self,
cursor: &Cursor<R>,