mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 14:52:35 -05:00
Compare commits
22 Commits
3086
...
PenguinWit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faa73ff4db | ||
|
|
7291efc077 | ||
|
|
ee66f6c395 | ||
|
|
eba08ad592 | ||
|
|
4833b4e287 | ||
|
|
9d1be64e4d | ||
|
|
d6e6cd3be0 | ||
|
|
70476f9277 | ||
|
|
d8ddfc26e9 | ||
|
|
c8acc3e8bd | ||
|
|
547442243b | ||
|
|
6e58266f54 | ||
|
|
f0cd0fb41d | ||
|
|
7585faf57e | ||
|
|
da7f6a34e8 | ||
|
|
4f7fa41262 | ||
|
|
4becfa39ca | ||
|
|
f8388b122d | ||
|
|
f57a57b92b | ||
|
|
f0bcbd9cfe | ||
|
|
115477ef1d | ||
|
|
832b9cb321 |
42
Cargo.toml
42
Cargo.toml
@@ -40,36 +40,36 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0-gamma"
|
||||
version = "0.7.0-gamma3"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-gamma" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-gamma" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-gamma" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-gamma" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma3" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-gamma3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma3" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-gamma3" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma3" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-gamma3" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma3" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-gamma" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-gamma" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma3" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma3" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-gamma3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma3" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-gamma3" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0-gamma"
|
||||
version = "0.2.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -9,6 +9,7 @@ description = "Spawn asynchronous tasks in an executor-independent way."
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.30"
|
||||
glib = { version = "0.20.0", optional = true }
|
||||
thiserror = "1.0"
|
||||
@@ -19,12 +20,14 @@ tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.42", optional = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
tracing = ["dep:tracing"]
|
||||
tokio = ["dep:tokio"]
|
||||
glib = ["dep:glib"]
|
||||
wasm-bindgen = ["dep:wasm-bindgen-futures"]
|
||||
futures-executor = ["futures/thread-pool", "futures/executor"]
|
||||
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
@@ -32,11 +32,14 @@
|
||||
use std::{future::Future, pin::Pin, sync::OnceLock};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
pub(crate) type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
/// A future that has been pinned.
|
||||
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
/// A future that has been pinned.
|
||||
pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
static SPAWN: OnceLock<fn(PinnedFuture<()>)> = OnceLock::new();
|
||||
static SPAWN_LOCAL: OnceLock<fn(PinnedLocalFuture<()>)> = OnceLock::new();
|
||||
static POLL_LOCAL: OnceLock<fn()> = OnceLock::new();
|
||||
|
||||
/// Errors that can occur when using the executor.
|
||||
#[derive(Error, Debug)]
|
||||
@@ -115,6 +118,14 @@ impl Executor {
|
||||
});
|
||||
_ = rx.await;
|
||||
}
|
||||
|
||||
/// Polls the current async executor.
|
||||
/// Not all async executors support polling, so this function may not do anything.
|
||||
pub fn poll_local() {
|
||||
if let Some(poller) = POLL_LOCAL.get() {
|
||||
poller()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Executor {
|
||||
@@ -193,13 +204,15 @@ impl Executor {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
|
||||
pub fn init_futures_executor() -> Result<(), ExecutorError> {
|
||||
use futures::{
|
||||
executor::{LocalPool, ThreadPool},
|
||||
executor::{LocalPool, LocalSpawner, ThreadPool},
|
||||
task::{LocalSpawnExt, SpawnExt},
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
|
||||
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_POOL: LocalPool = LocalPool::new();
|
||||
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
|
||||
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
|
||||
}
|
||||
|
||||
fn get_thread_pool() -> &'static ThreadPool {
|
||||
@@ -218,28 +231,97 @@ impl Executor {
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
let spawner = pool.spawner();
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
if let Ok(mut pool) = pool.try_borrow_mut() {
|
||||
pool.run_until_stalled();
|
||||
}
|
||||
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
|
||||
});
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use crate::Executor;
|
||||
use std::rc::Rc;
|
||||
Executor::init_futures_executor().expect("couldn't set executor");
|
||||
let rc = Rc::new(());
|
||||
Executor::spawn_local(async {
|
||||
_ = rc;
|
||||
});
|
||||
Executor::spawn(async {});
|
||||
/// Globally sets the [`async_executor`] executor as the executor used to spawn tasks,
|
||||
/// lazily creating a thread pool to spawn tasks into.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
///
|
||||
/// Requires the `async-executor` feature to be activated on this crate.
|
||||
#[cfg(feature = "async-executor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-executor")))]
|
||||
pub fn init_async_executor() -> Result<(), ExecutorError> {
|
||||
use async_executor::{Executor, LocalExecutor};
|
||||
|
||||
static THREAD_POOL: OnceLock<Executor> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
|
||||
}
|
||||
|
||||
fn get_thread_pool() -> &'static Executor<'static> {
|
||||
THREAD_POOL.get_or_init(Executor::new)
|
||||
}
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
get_thread_pool().spawn(fut).detach();
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
LOCAL_POOL.with(|pool| pool.spawn(fut).detach());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
LOCAL_POOL.with(|pool| pool.try_tick());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets a custom executor as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
pub fn init_custom_executor(
|
||||
custom_executor: impl CustomExecutor + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
|
||||
EXECUTOR
|
||||
.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
EXECUTOR.get().unwrap().spawn(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| EXECUTOR.get().unwrap().spawn_local(fut))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| EXECUTOR.get().unwrap().poll_local())
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for custom executors.
|
||||
/// Custom executors can be used to integrate with any executor that supports spawning futures.
|
||||
///
|
||||
/// All methods can be called recursively.
|
||||
pub trait CustomExecutor: Send + Sync {
|
||||
/// Spawns a future, usually on a thread pool.
|
||||
fn spawn(&self, fut: PinnedFuture<()>);
|
||||
/// Spawns a local future. May require calling `poll_local` to make progress.
|
||||
fn spawn_local(&self, fut: PinnedLocalFuture<()>);
|
||||
/// Polls the executor, if it supports polling.
|
||||
fn poll_local(&self);
|
||||
}
|
||||
|
||||
55
any_spawner/tests/custom_runtime.rs
Normal file
55
any_spawner/tests/custom_runtime.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
#[cfg(feature = "futures-executor")]
|
||||
use any_spawner::{CustomExecutor, Executor, PinnedFuture, PinnedLocalFuture};
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_create_custom_executor() {
|
||||
use futures::{
|
||||
executor::{LocalPool, LocalSpawner},
|
||||
task::LocalSpawnExt,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
|
||||
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
|
||||
}
|
||||
|
||||
struct CustomFutureExecutor;
|
||||
impl CustomExecutor for CustomFutureExecutor {
|
||||
fn spawn(&self, _fut: PinnedFuture<()>) {
|
||||
panic!("not supported in this test");
|
||||
}
|
||||
|
||||
fn spawn_local(&self, fut: PinnedLocalFuture<()>) {
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
}
|
||||
|
||||
fn poll_local(&self) {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
if let Ok(mut pool) = pool.try_borrow_mut() {
|
||||
pool.run_until_stalled();
|
||||
}
|
||||
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Executor::init_custom_executor(CustomFutureExecutor)
|
||||
.expect("couldn't set executor");
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let counter_clone = Arc::clone(&counter);
|
||||
Executor::spawn_local(async move {
|
||||
counter_clone.store(1, Ordering::Release);
|
||||
});
|
||||
Executor::poll_local();
|
||||
assert_eq!(counter.load(Ordering::Acquire), 1);
|
||||
}
|
||||
38
any_spawner/tests/futures_runtime.rs
Normal file
38
any_spawner/tests/futures_runtime.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
#[cfg(feature = "futures-executor")]
|
||||
use any_spawner::Executor;
|
||||
// All tests in this file use the same executor.
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use std::rc::Rc;
|
||||
|
||||
let _ = Executor::init_futures_executor();
|
||||
let rc = Rc::new(());
|
||||
Executor::spawn_local(async {
|
||||
_ = rc;
|
||||
});
|
||||
Executor::spawn(async {});
|
||||
}
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_make_local_progress() {
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
let _ = Executor::init_futures_executor();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
Executor::spawn_local({
|
||||
let counter = Arc::clone(&counter);
|
||||
async move {
|
||||
assert_eq!(counter.fetch_add(1, Ordering::AcqRel), 0);
|
||||
Executor::spawn_local(async {
|
||||
// Should not crash
|
||||
});
|
||||
}
|
||||
});
|
||||
Executor::poll_local();
|
||||
assert_eq!(counter.load(Ordering::Acquire), 1);
|
||||
}
|
||||
@@ -5,13 +5,14 @@ use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{
|
||||
Form, Outlet, ParentRoute, ProtectedRoute, Redirect, Route, Router,
|
||||
Routes, A,
|
||||
Routes, RoutingProgress, A,
|
||||
},
|
||||
hooks::{use_navigate, use_params, use_query_map},
|
||||
params::Params,
|
||||
MatchNestedRoutes,
|
||||
};
|
||||
use leptos_router_macro::path;
|
||||
use std::time::Duration;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -26,9 +27,14 @@ pub fn RouterExample() -> impl IntoView {
|
||||
|
||||
// this signal will be ued to set whether we are allowed to access a protected route
|
||||
let (logged_in, set_logged_in) = signal(true);
|
||||
let (is_routing, set_is_routing) = signal(false);
|
||||
|
||||
view! {
|
||||
<Router>
|
||||
<Router set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<div class="routing-progress">
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
|
||||
</div>
|
||||
<nav>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
// using <A> has two effects:
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use chrono::{Local, NaiveDate};
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Field, Patch, Store};
|
||||
use reactive_stores_macro::{Patch, Store};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ID starts higher than 0 because we have a few starting todos by default
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.2.0-gamma"
|
||||
version = "0.2.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -16,8 +16,6 @@ axum = { version = "0.7.5", default-features = false, features = [
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
http-body-util = "0.1.2"
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||
leptos_macro = { workspace = true, features = ["axum"] }
|
||||
@@ -26,7 +24,6 @@ leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.39", default-features = false }
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
tower-http = "0.5.2"
|
||||
|
||||
@@ -606,9 +606,9 @@ where
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// http::Request,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::{config::LeptosOptions, context::provide_context, prelude::*};
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
@@ -806,9 +806,9 @@ where
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// http::Request,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::context::provide_context;
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
@@ -1025,9 +1025,9 @@ where
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// http::Request,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::context::provide_context;
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
@@ -1093,9 +1093,9 @@ where
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// http::Request,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::context::provide_context;
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
@@ -1342,8 +1342,7 @@ where
|
||||
.with(|| {
|
||||
// stub out a path for now
|
||||
provide_context(RequestUrl::new(""));
|
||||
let (mock_parts, _) =
|
||||
http::Request::new(Body::from("")).into_parts();
|
||||
let (mock_parts, _) = Request::new(Body::from("")).into_parts();
|
||||
let (mock_meta, _) = ServerMetaContext::new();
|
||||
provide_contexts("", &mock_meta, mock_parts, Default::default());
|
||||
additional_context();
|
||||
@@ -1402,8 +1401,8 @@ impl StaticRouteGenerator {
|
||||
let add_context = additional_context.clone();
|
||||
move || {
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
let mock_req = http::Request::builder()
|
||||
.method(http::Method::GET)
|
||||
let mock_req = Request::builder()
|
||||
.method(Method::GET)
|
||||
.header("Accept", "text/html")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
@@ -1495,10 +1494,12 @@ impl StaticRouteGenerator {
|
||||
_ = routes;
|
||||
_ = app_fn;
|
||||
_ = additional_context;
|
||||
panic!(
|
||||
"Static routes are not currently supported on WASM32 server \
|
||||
targets."
|
||||
);
|
||||
Self(Box::new(|_| {
|
||||
panic!(
|
||||
"Static routes are not currently supported on WASM32 \
|
||||
server targets."
|
||||
);
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1933,7 +1934,7 @@ where
|
||||
///
|
||||
/// #[server]
|
||||
/// pub async fn request_method() -> Result<String, ServerFnError> {
|
||||
/// use http::Method;
|
||||
/// use axum::http::Method;
|
||||
/// use leptos_axum::extract;
|
||||
///
|
||||
/// // you can extract anything that a regular Axum extractor can extract
|
||||
|
||||
@@ -45,7 +45,7 @@ web-sys = { version = "0.3.70", features = [
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "=0.2.93"
|
||||
serde_qs = "0.13.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.30"
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use crate::TextProp;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A collection of additional HTML attributes to be applied to an element,
|
||||
/// each of which may or may not be reactive.
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
#[deprecated = "Most uses of `AdditionalAttributes` can be replaced with `#[prop(attrs)]` \
|
||||
and the `attr:` syntax. If you have a use case that still requires `AdditionalAttributes`, please \
|
||||
open a GitHub issue here and share it: https://github.com/leptos-rs/leptos"]
|
||||
pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>);
|
||||
|
||||
impl<I, T, U> From<I> for AdditionalAttributes
|
||||
where
|
||||
I: IntoIterator<Item = (T, U)>,
|
||||
T: Into<String>,
|
||||
U: Into<TextProp>,
|
||||
{
|
||||
fn from(value: I) -> Self {
|
||||
Self(
|
||||
value
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.into(), v.into()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AdditionalAttributes {
|
||||
fn default() -> Self {
|
||||
Self([].into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over additional HTML attributes.
|
||||
#[repr(transparent)]
|
||||
pub struct AdditionalAttributesIter<'a>(
|
||||
std::slice::Iter<'a, (String, TextProp)>,
|
||||
);
|
||||
|
||||
impl<'a> Iterator for AdditionalAttributesIter<'a> {
|
||||
type Item = &'a (String, TextProp);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a AdditionalAttributes {
|
||||
type Item = &'a (String, TextProp);
|
||||
type IntoIter = AdditionalAttributesIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
AdditionalAttributesIter(self.0.iter())
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
use crate::{ChildrenFn, Show};
|
||||
use crate::{children::ChildrenFn, component, control_flow::Show, IntoView};
|
||||
use core::time::Duration;
|
||||
use leptos::component;
|
||||
use leptos_dom::{helpers::TimeoutHandle, IntoView};
|
||||
use leptos_dom::helpers::TimeoutHandle;
|
||||
use leptos_macro::view;
|
||||
use leptos_reactive::{
|
||||
create_render_effect, on_cleanup, signal_prelude::*, store_value,
|
||||
StoredValue,
|
||||
use reactive_graph::{
|
||||
effect::RenderEffect,
|
||||
owner::{on_cleanup, StoredValue},
|
||||
signal::RwSignal,
|
||||
traits::{Get, GetUntracked, GetValue, Set, SetValue},
|
||||
wrappers::read::Signal,
|
||||
};
|
||||
use tachys::prelude::*;
|
||||
|
||||
/// A component that will show its children when the `when` condition is `true`.
|
||||
/// Additionally, you need to specify a `hide_delay`. If the `when` condition changes to `false`,
|
||||
@@ -16,10 +19,10 @@ use leptos_reactive::{
|
||||
///
|
||||
/// ```rust
|
||||
/// # use core::time::Duration;
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// # #[component]
|
||||
/// # pub fn App() -> impl IntoView {
|
||||
/// let show = create_rw_signal(false);
|
||||
/// let show = RwSignal::new(false);
|
||||
///
|
||||
/// view! {
|
||||
/// <div
|
||||
@@ -50,7 +53,7 @@ pub fn AnimatedShow(
|
||||
children: ChildrenFn,
|
||||
/// If the component should show or not
|
||||
#[prop(into)]
|
||||
when: MaybeSignal<bool>,
|
||||
when: Signal<bool>,
|
||||
/// Optional CSS class to apply if `when == true`
|
||||
#[prop(optional)]
|
||||
show_class: &'static str,
|
||||
@@ -60,15 +63,15 @@ pub fn AnimatedShow(
|
||||
/// The timeout after which the component will be unmounted if `when == false`
|
||||
hide_delay: Duration,
|
||||
) -> impl IntoView {
|
||||
let handle: StoredValue<Option<TimeoutHandle>> = store_value(None);
|
||||
let cls = create_rw_signal(if when.get_untracked() {
|
||||
let handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
|
||||
let cls = RwSignal::new(if when.get_untracked() {
|
||||
show_class
|
||||
} else {
|
||||
hide_class
|
||||
});
|
||||
let show = create_rw_signal(when.get_untracked());
|
||||
let show = RwSignal::new(when.get_untracked());
|
||||
|
||||
create_render_effect(move |_| {
|
||||
let eff = RenderEffect::new(move |_| {
|
||||
if when.get() {
|
||||
// clear any possibly active timer
|
||||
if let Some(h) = handle.get_value() {
|
||||
@@ -93,6 +96,7 @@ pub fn AnimatedShow(
|
||||
if let Some(Some(h)) = handle.try_get_value() {
|
||||
h.clear();
|
||||
}
|
||||
drop(eff);
|
||||
});
|
||||
|
||||
view! {
|
||||
|
||||
@@ -174,7 +174,9 @@ pub mod prelude {
|
||||
pub use server_fn::{self, ServerFnError};
|
||||
pub use tachys::{
|
||||
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
|
||||
view::template::ViewTemplate,
|
||||
view::{
|
||||
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
|
||||
},
|
||||
};
|
||||
}
|
||||
pub use export_types::*;
|
||||
@@ -202,8 +204,9 @@ pub mod error {
|
||||
|
||||
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
|
||||
pub mod control_flow {
|
||||
pub use crate::{await_::*, for_loop::*, show::*};
|
||||
pub use crate::{animated_show::*, await_::*, for_loop::*, show::*};
|
||||
}
|
||||
mod animated_show;
|
||||
mod await_;
|
||||
mod for_loop;
|
||||
mod show;
|
||||
@@ -326,233 +329,3 @@ pub use tracing;
|
||||
pub use wasm_bindgen;
|
||||
#[doc(hidden)]
|
||||
pub use web_sys;
|
||||
|
||||
/*mod additional_attributes;
|
||||
pub use additional_attributes::*;
|
||||
pub use await_::*;
|
||||
pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
#[cfg(not(all(
|
||||
target_arch = "wasm32",
|
||||
any(feature = "csr", feature = "hydrate")
|
||||
)))]
|
||||
/// Utilities for server-side rendering HTML.
|
||||
pub mod ssr {
|
||||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, document, ev,
|
||||
helpers::{
|
||||
event_target, event_target_checked, event_target_value,
|
||||
request_animation_frame, request_animation_frame_with_handle,
|
||||
request_idle_callback, request_idle_callback_with_handle, set_interval,
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
},
|
||||
html,
|
||||
html::Binding,
|
||||
math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class,
|
||||
CollectView, Errors, EventHandlerFn, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
pub mod error {
|
||||
pub use server_fn::error::{Error, Result};
|
||||
}
|
||||
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
|
||||
pub use leptos_macro::template;
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "template_macro")))]
|
||||
pub use leptos_macro::view as template;
|
||||
pub use leptos_macro::{component, island, slice, slot, view, Params};
|
||||
cfg_if::cfg_if!(
|
||||
if #[cfg(feature="spin")] {
|
||||
pub use leptos_spin_macro::server;
|
||||
} else {
|
||||
pub use leptos_macro::server;
|
||||
}
|
||||
);
|
||||
pub use leptos_reactive::*;
|
||||
pub use leptos_server::{
|
||||
self, create_action, create_multi_action, create_server_action,
|
||||
create_server_multi_action, Action, MultiAction, ServerFnError,
|
||||
ServerFnErrorErr,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
mod animated_show;
|
||||
mod for_loop;
|
||||
mod provider;
|
||||
mod show;
|
||||
pub use animated_show::*;
|
||||
pub use for_loop::*;
|
||||
pub use provider::*;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
pub use serde;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
pub use serde_json;
|
||||
pub use show::*;
|
||||
//pub use suspense_component::*;
|
||||
mod suspense_component;
|
||||
//mod transition;
|
||||
#[cfg(feature = "tracing")]
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
pub use transition::*;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder::Optional;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder_macro;
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(
|
||||
feature = "csr",
|
||||
feature = "hydrate",
|
||||
feature = "template_macro"
|
||||
))]
|
||||
pub use wasm_bindgen; // used in islands
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(
|
||||
feature = "csr",
|
||||
feature = "hydrate",
|
||||
feature = "template_macro"
|
||||
))]
|
||||
pub use web_sys; // used in islands
|
||||
|
||||
mod children;
|
||||
mod portal;
|
||||
mod view_fn;
|
||||
pub use children::*;
|
||||
pub use portal::*;
|
||||
pub use view_fn::*;
|
||||
|
||||
extern crate self as leptos;
|
||||
|
||||
/// A type for taking anything that implements [`IntoAttribute`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyHeading(
|
||||
/// text: String,
|
||||
/// #[prop(optional, into)] class: Option<AttributeValue>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <h1 class=class>{text}</h1>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub type AttributeValue = Box<dyn IntoAttribute>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Component<P> {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Props {
|
||||
type Builder;
|
||||
fn builder() -> Self::Builder;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait DynAttrs {
|
||||
fn dyn_attrs(self, _args: Vec<(&'static str, Attribute)>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DynAttrs for () {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait DynBindings {
|
||||
fn dyn_bindings<B: Into<Binding>>(
|
||||
self,
|
||||
_args: impl IntoIterator<Item = B>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DynBindings for () {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait PropsOrNoPropsBuilder {
|
||||
type Builder;
|
||||
fn builder_or_not() -> Self::Builder;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct EmptyPropsBuilder {}
|
||||
|
||||
impl EmptyPropsBuilder {
|
||||
pub fn build(self) {}
|
||||
}
|
||||
|
||||
impl<P: Props> PropsOrNoPropsBuilder for P {
|
||||
type Builder = <P as Props>::Builder;
|
||||
fn builder_or_not() -> Self::Builder {
|
||||
Self::builder()
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
|
||||
type Builder = EmptyPropsBuilder;
|
||||
fn builder_or_not() -> Self::Builder {
|
||||
EmptyPropsBuilder {}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, R> Component<EmptyPropsBuilder> for F where F: FnOnce() -> R {}
|
||||
|
||||
impl<P, F, R> Component<P> for F
|
||||
where
|
||||
F: FnOnce(P) -> R,
|
||||
P: Props,
|
||||
{
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
|
||||
_f: &impl Component<P>,
|
||||
) -> <P as PropsOrNoPropsBuilder>::Builder {
|
||||
<P as PropsOrNoPropsBuilder>::builder_or_not()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_view<P>(f: impl ComponentConstructor<P>, props: P) -> View {
|
||||
f.construct(props)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait ComponentConstructor<P> {
|
||||
fn construct(self, props: P) -> View;
|
||||
}
|
||||
|
||||
impl<Func, V> ComponentConstructor<()> for Func
|
||||
where
|
||||
Func: FnOnce() -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
fn construct(self, (): ()) -> View {
|
||||
(self)().into_view()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Func, V, P> ComponentConstructor<P> for Func
|
||||
where
|
||||
Func: FnOnce(P) -> V,
|
||||
V: IntoView,
|
||||
P: PropsOrNoPropsBuilder,
|
||||
{
|
||||
fn construct(self, props: P) -> View {
|
||||
(self)(props).into_view()
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
use leptos_dom::{IntoView, View};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
|
||||
/// to enable optional props in for example `<Show>` and `<Suspense>`.
|
||||
#[derive(Clone)]
|
||||
pub struct ViewFn(Rc<dyn Fn() -> View>);
|
||||
|
||||
impl Default for ViewFn {
|
||||
fn default() -> Self {
|
||||
Self(Rc::new(|| ().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, IV> From<F> for ViewFn
|
||||
where
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
fn from(value: F) -> Self {
|
||||
Self(Rc::new(move || value().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewFn {
|
||||
/// Execute the wrapped function
|
||||
pub fn run(&self) -> View {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.7.0-gamma"
|
||||
version = "0.7.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -52,10 +52,22 @@ axum = ["server_fn_macro/axum"]
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
skip_feature_sets = [
|
||||
["csr", "hydrate"],
|
||||
["hydrate", "csr"],
|
||||
["hydrate", "ssr"],
|
||||
["actix", "axum"]
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"csr",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
|
||||
|
||||
let gen = quote! {
|
||||
impl Params for #name {
|
||||
fn from_map(map: &::leptos_router::params::ParamsMap) -> Result<Self, ::leptos_router::params::ParamsError> {
|
||||
fn from_map(map: &::leptos_router::params::ParamsMap) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
|
||||
Ok(Self {
|
||||
#(#fields,)*
|
||||
})
|
||||
|
||||
@@ -185,7 +185,7 @@ impl FromEncodedStr for [u8] {
|
||||
mod view_implementations {
|
||||
use crate::Resource;
|
||||
use reactive_graph::traits::Read;
|
||||
use std::{future::Future, pin::Pin};
|
||||
use std::future::Future;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
@@ -219,16 +219,11 @@ mod view_implementations {
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute> = Box<
|
||||
dyn FnMut() -> Suspend<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = <T as AddAnyAttr>::Output<
|
||||
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
>,
|
||||
> + Send,
|
||||
<T as AddAnyAttr>::Output<
|
||||
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
|
||||
>,
|
||||
>
|
||||
+ Send
|
||||
>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.7.0-gamma"
|
||||
version = "0.7.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "next_tuple"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -75,7 +75,7 @@ impl<T: Serialize + 'static, St: Storage<T>> Serialize for ArcMemo<T, St> {
|
||||
|
||||
impl<T, St> Serialize for MaybeSignal<T, St>
|
||||
where
|
||||
T: Send + Sync + Serialize,
|
||||
T: Clone + Send + Sync + Serialize,
|
||||
St: Storage<SignalTypes<T, St>> + Storage<T>,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
||||
@@ -623,3 +623,46 @@ where
|
||||
Display::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper that implements [`Deref`] and [`Borrow`] for itself.
|
||||
pub struct Derefable<T>(pub T);
|
||||
|
||||
impl<T> Clone for Derefable<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Derefable(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for Derefable<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Borrow<T> for Derefable<T> {
|
||||
fn borrow(&self) -> &T {
|
||||
self.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<T> for Derefable<T>
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.deref() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Display for Derefable<T>
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,11 +168,20 @@ pub trait ReadUntracked: Sized + DefinedAt {
|
||||
self.try_read_untracked()
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
|
||||
/// This is a backdoor to allow overriding the [`Read::try_read`] implementation despite it being auto implemented.
|
||||
///
|
||||
/// If your type contains a [`Signal`](crate::wrappers::read::Signal),
|
||||
/// call it's [`ReadUntracked::custom_try_read`] here, else return `None`.
|
||||
#[track_caller]
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Give read-only access to a signal's value by reference through a guard type,
|
||||
/// and subscribes the active reactive observer (an effect or computed) to changes in its value.
|
||||
pub trait Read {
|
||||
pub trait Read: DefinedAt {
|
||||
/// The guard type that will be returned, which can be dereferenced to the value.
|
||||
type Value: Deref;
|
||||
|
||||
@@ -185,7 +194,9 @@ pub trait Read {
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that has been disposed.
|
||||
#[track_caller]
|
||||
fn read(&self) -> Self::Value;
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Read for T
|
||||
@@ -195,13 +206,18 @@ where
|
||||
type Value = T::Value;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
self.track();
|
||||
self.try_read_untracked()
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.track();
|
||||
self.read_untracked()
|
||||
// The [`Read`] trait is auto implemented for types that implement [`ReadUntracked`] + [`Track`]. The [`Read`] trait then auto implements the [`With`] and [`Get`] traits too.
|
||||
//
|
||||
// This is a problem for e.g. the [`Signal`](crate::wrappers::read::Signal) type,
|
||||
// this type must use a custom [`Read::try_read`] implementation to avoid an unnecessary clone.
|
||||
//
|
||||
// This is a backdoor to allow overriding the [`Read::try_read`] implementation despite it being auto implemented.
|
||||
if let Some(custom) = self.custom_try_read() {
|
||||
custom
|
||||
} else {
|
||||
self.track();
|
||||
self.try_read_untracked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,14 +323,13 @@ pub trait With: DefinedAt {
|
||||
|
||||
impl<T> With for T
|
||||
where
|
||||
T: WithUntracked + Track,
|
||||
T: Read,
|
||||
{
|
||||
type Value = <T as WithUntracked>::Value;
|
||||
type Value = <<T as Read>::Value as Deref>::Target;
|
||||
|
||||
#[track_caller]
|
||||
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
|
||||
self.track();
|
||||
self.try_with_untracked(fun)
|
||||
self.try_read().map(|val| fun(&val))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ pub mod read {
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
|
||||
With, WithValue,
|
||||
},
|
||||
unwrap_signal,
|
||||
};
|
||||
@@ -201,29 +200,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for ArcSignal<T, SyncStorage>
|
||||
where
|
||||
T: Default + Send + Sync + 'static,
|
||||
@@ -285,22 +261,23 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for ArcSignal<T, S>
|
||||
impl<T, S> Track for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
T: Clone,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
fn track(&self) {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => i.try_with(fun),
|
||||
SignalTypes::Memo(i) => i.try_with(fun),
|
||||
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
|
||||
SignalTypes::Stored(i) => i.try_with_value(fun),
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,32 +305,27 @@ pub mod read {
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
/// Overriding the default auto implemented [`Read::try_read`] to combine read and track,
|
||||
/// to avoid 2 clones and just have 1 in the [`SignalTypes::DerivedSignal`].
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
Some(
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
SignalTypes::Memo(i) => i.try_read().map(SignalReadGuard::Memo),
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
.map(ReadGuard::new),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,27 +404,31 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for Signal<T, S>
|
||||
impl<T, S> Track for Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
S: Storage<T> + Storage<SignalTypes<T, S>>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
self.inner
|
||||
fn track(&self) {
|
||||
let inner = self
|
||||
.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| match &inner {
|
||||
SignalTypes::ReadSignal(i) => i.try_with(fun),
|
||||
SignalTypes::Memo(i) => i.try_with(fun),
|
||||
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
|
||||
SignalTypes::Stored(i) => i.try_with_value(fun),
|
||||
})
|
||||
.unwrap_or_else(unwrap_signal!(self));
|
||||
match inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,41 +462,33 @@ pub mod read {
|
||||
.map(ReadGuard::new)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
self.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| {
|
||||
match &inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
/// Overriding the default auto implemented [`Read::try_read`] to combine read and track,
|
||||
/// to avoid 2 clones and just have 1 in the [`SignalTypes::DerivedSignal`].
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
Some(
|
||||
self.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| {
|
||||
match &inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
})
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
.map(ReadGuard::new)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,36 +588,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
let inner = self
|
||||
.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.unwrap_or_else(unwrap_signal!(self));
|
||||
match inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Signal<T>
|
||||
where
|
||||
T: Send + Sync + Default + 'static,
|
||||
@@ -830,6 +768,20 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Signal<String> {
|
||||
#[track_caller]
|
||||
fn from(value: &str) -> Self {
|
||||
Signal::stored(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Signal<String, LocalStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: &str) -> Self {
|
||||
Signal::stored_local(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for a value that is *either* `T` or [`Signal<T>`].
|
||||
///
|
||||
/// This allows you to create APIs that take either a reactive or a non-reactive value
|
||||
@@ -903,20 +855,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for MaybeSignal<T, S>
|
||||
impl<T, S> Track for MaybeSignal<T, S>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
S: Storage<T> + Storage<SignalTypes<T, S>>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
fn track(&self) {
|
||||
match self {
|
||||
Self::Static(t) => Some(fun(t)),
|
||||
Self::Dynamic(s) => s.try_with(fun),
|
||||
Self::Static(_) => {}
|
||||
Self::Dynamic(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -936,27 +882,13 @@ pub mod read {
|
||||
Self::Dynamic(s) => s.try_read_untracked(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for MaybeSignal<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
match self {
|
||||
Self::Static(t) => {
|
||||
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
|
||||
}
|
||||
Self::Dynamic(s) => s.try_read(),
|
||||
Self::Static(_) => None,
|
||||
Self::Dynamic(s) => s.custom_try_read(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeSignal<T>
|
||||
@@ -980,21 +912,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> MaybeSignal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match self {
|
||||
Self::Static(_) => {}
|
||||
Self::Dynamic(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeSignal<T, SyncStorage>
|
||||
where
|
||||
SyncStorage: Storage<T>,
|
||||
@@ -1192,20 +1109,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for MaybeProp<T, S>
|
||||
impl<T, S> Track for MaybeProp<T, S>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
fn track(&self) {
|
||||
match &self.0 {
|
||||
None => Some(fun(&None)),
|
||||
Some(inner) => inner.try_with(fun),
|
||||
None => {}
|
||||
Some(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1223,25 +1134,13 @@ pub mod read {
|
||||
Some(inner) => inner.try_read_untracked(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for MaybeProp<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
match &self.0 {
|
||||
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
|
||||
Some(inner) => inner.try_read(),
|
||||
None => None,
|
||||
Some(inner) => inner.custom_try_read(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeProp<T>
|
||||
@@ -1257,21 +1156,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> MaybeProp<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match &self.0 {
|
||||
None => {}
|
||||
Some(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeProp<T>
|
||||
where
|
||||
SyncStorage: Storage<Option<T>>,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Stores for holding deeply-nested reactive state while maintaining fine-grained reactive tracking."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -11,10 +16,10 @@ or_poisoned = { workspace = true }
|
||||
paste = "1.0"
|
||||
reactive_graph = { workspace = true }
|
||||
rustc-hash = "2.0"
|
||||
reactive_stores_macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.39", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
reactive_stores_macro = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["effects"] }
|
||||
|
||||
15
reactive_stores/README.md
Normal file
15
reactive_stores/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Stores
|
||||
|
||||
Stores are a data structure for nested reactivity.
|
||||
|
||||
The [`reactive_graph`](https://crates.io/crates/reactive_graph) crate provides primitives for fine-grained reactivity
|
||||
via signals, memos, and effects.
|
||||
|
||||
This crate extends that reactivity to support reactive access to nested dested, without the need to create nested signals.
|
||||
|
||||
Using the `#[derive(Store)]` macro on a struct creates a series of getters that allow accessing each field. Individual fields
|
||||
can then be read as if they were signals. Changes to parents will notify their children, but changing one sibling field will
|
||||
not notify any of the others, nor will it require diffing those sibling fields (unlike earlier solutions using memoized “slices”).
|
||||
|
||||
This is published for use with the Leptos framework but can be used in any scenario where `reactive_graph` is being used
|
||||
for reactivity.
|
||||
@@ -10,6 +10,7 @@ use reactive_graph::{
|
||||
Write,
|
||||
},
|
||||
};
|
||||
pub use reactive_stores_macro::*;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -444,7 +445,6 @@ mod tests {
|
||||
effect::Effect,
|
||||
traits::{Read, ReadUntracked, Set, Update, Write},
|
||||
};
|
||||
use reactive_stores_macro::{Patch, Store};
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
|
||||
@@ -51,7 +51,6 @@ mod tests {
|
||||
effect::Effect,
|
||||
traits::{Get, Read, ReadUntracked, Set, Write},
|
||||
};
|
||||
use reactive_stores_macro::Store;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
[package]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Stores for holding deeply-nested reactive state while maintaining fine-grained reactive tracking."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
|
||||
1
reactive_stores_macro/README.md
Normal file
1
reactive_stores_macro/README.md
Normal file
@@ -0,0 +1 @@
|
||||
This crate provides macro that are helpful or required when using the `reactive_stores` crate.
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.7.0-gamma"
|
||||
version = "0.7.0-gamma3"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -22,13 +22,11 @@ url = "2.5"
|
||||
js-sys = { version = "0.3.69" }
|
||||
wasm-bindgen = { version = "0.2.93" }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
paste = "1.0"
|
||||
once_cell = "1.19"
|
||||
send_wrapper = "0.6.0"
|
||||
thiserror = "1.0"
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
gloo-net = "0.6.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.70"
|
||||
|
||||
@@ -248,7 +248,10 @@ where
|
||||
base.upgrade_inplace();
|
||||
base
|
||||
});
|
||||
let routes = Routes::new(children.into_inner());
|
||||
let routes = Routes::new_with_base(
|
||||
children.into_inner(),
|
||||
base.clone().unwrap_or_default(),
|
||||
);
|
||||
let outer_owner =
|
||||
Owner::current().expect("creating Routes, but no Owner was found");
|
||||
move || {
|
||||
@@ -294,7 +297,10 @@ where
|
||||
base.upgrade_inplace();
|
||||
base
|
||||
});
|
||||
let routes = Routes::new(children.into_inner());
|
||||
let routes = Routes::new_with_base(
|
||||
children.into_inner(),
|
||||
base.clone().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let outer_owner =
|
||||
Owner::current().expect("creating Router, but no Owner was found");
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use either_of::{Either, EitherOf3};
|
||||
use futures::{future::join_all, FutureExt};
|
||||
use futures::{channel::oneshot, future::join_all, FutureExt};
|
||||
use leptos::{component, oco::Oco};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
@@ -16,6 +16,7 @@ use reactive_graph::{
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::{ArcRwSignal, ArcTrigger},
|
||||
traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track},
|
||||
transition::AsyncTransition,
|
||||
wrappers::write::SignalSetter,
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
@@ -47,7 +48,6 @@ pub(crate) struct NestedRoutesView<Loc, Defs, FalFn> {
|
||||
pub current_url: ArcRwSignal<Url>,
|
||||
pub base: Option<Oco<'static, str>>,
|
||||
pub fallback: FalFn,
|
||||
#[allow(unused)] // TODO
|
||||
pub set_is_routing: Option<SignalSetter<bool>>,
|
||||
}
|
||||
|
||||
@@ -155,23 +155,37 @@ where
|
||||
state.outlets.clear();
|
||||
}
|
||||
Some(route) => {
|
||||
let mut loaders = Vec::new();
|
||||
if let Some(set_is_routing) = self.set_is_routing {
|
||||
set_is_routing.set(true);
|
||||
}
|
||||
|
||||
let mut preloaders = Vec::new();
|
||||
let mut full_loaders = Vec::new();
|
||||
route.rebuild_nested_route(
|
||||
&self.current_url.read_untracked(),
|
||||
self.base,
|
||||
&mut 0,
|
||||
&mut loaders,
|
||||
&mut preloaders,
|
||||
&mut full_loaders,
|
||||
&mut state.outlets,
|
||||
&self.outer_owner,
|
||||
self.set_is_routing.is_some(),
|
||||
);
|
||||
|
||||
let location = self.location.clone();
|
||||
Executor::spawn_local(async move {
|
||||
let triggers = join_all(loaders).await;
|
||||
let triggers = join_all(preloaders).await;
|
||||
// tell each one of the outlet triggers that it's ready
|
||||
for trigger in triggers {
|
||||
trigger.notify();
|
||||
}
|
||||
});
|
||||
|
||||
Executor::spawn_local(async move {
|
||||
join_all(full_loaders).await;
|
||||
if let Some(set_is_routing) = self.set_is_routing {
|
||||
set_is_routing.set(false);
|
||||
}
|
||||
if let Some(loc) = location {
|
||||
loc.ready_to_complete();
|
||||
}
|
||||
@@ -426,9 +440,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
type OutletViewFn = Box<
|
||||
dyn Fn() -> Suspend<Pin<Box<dyn Future<Output = AnyView> + Send>>> + Send,
|
||||
>;
|
||||
type OutletViewFn = Box<dyn Fn() -> Suspend<AnyView> + Send>;
|
||||
|
||||
pub(crate) struct RouteContext {
|
||||
id: RouteMatchId,
|
||||
@@ -486,14 +498,17 @@ trait AddNestedRoute {
|
||||
parent: &Owner,
|
||||
);
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn rebuild_nested_route(
|
||||
self,
|
||||
url: &Url,
|
||||
base: Option<Oco<'static, str>>,
|
||||
items: &mut usize,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
full_loaders: &mut Vec<oneshot::Receiver<()>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
parent: &Owner,
|
||||
set_is_routing: bool,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -629,14 +644,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn rebuild_nested_route(
|
||||
self,
|
||||
url: &Url,
|
||||
base: Option<Oco<'static, str>>,
|
||||
items: &mut usize,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
full_loaders: &mut Vec<oneshot::Receiver<()>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
parent: &Owner,
|
||||
set_is_routing: bool,
|
||||
) {
|
||||
let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets
|
||||
.iter()
|
||||
@@ -647,7 +665,7 @@ where
|
||||
match current {
|
||||
// if there's nothing currently in the routes at this point, build from here
|
||||
None => {
|
||||
self.build_nested_route(url, base, loaders, outlets, parent);
|
||||
self.build_nested_route(url, base, preloaders, outlets, parent);
|
||||
}
|
||||
Some(current) => {
|
||||
// a unique ID for each route, which allows us to compare when we get new matches
|
||||
@@ -716,11 +734,14 @@ where
|
||||
let old_owner =
|
||||
mem::replace(&mut current.owner, parent.child());
|
||||
let owner = current.owner.clone();
|
||||
let (full_tx, full_rx) = oneshot::channel();
|
||||
let full_tx = Mutex::new(Some(full_tx));
|
||||
full_loaders.push(full_rx);
|
||||
|
||||
// send the new view, with the new owner, through the channel to the Outlet,
|
||||
// and notify the trigger so that the reactive view inside the Outlet tracking
|
||||
// the trigger runs again
|
||||
loaders.push(Box::pin(owner.with(|| {
|
||||
preloaders.push(Box::pin(owner.with(|| {
|
||||
ScopedFuture::new({
|
||||
let owner = owner.clone();
|
||||
let trigger = current.trigger.clone();
|
||||
@@ -736,15 +757,26 @@ where
|
||||
Box::new(move || {
|
||||
let owner = owner.clone();
|
||||
let view = view.clone();
|
||||
let full_tx =
|
||||
full_tx.lock().or_poisoned().take();
|
||||
Suspend::new(Box::pin(async move {
|
||||
let view = SendWrapper::new(
|
||||
owner.with(|| {
|
||||
ScopedFuture::new(
|
||||
view.choose(),
|
||||
async move {
|
||||
if set_is_routing {
|
||||
AsyncTransition::run(|| view.choose()).await
|
||||
} else {
|
||||
view.choose().await
|
||||
}
|
||||
}
|
||||
)
|
||||
}),
|
||||
);
|
||||
let view = view.await;
|
||||
if let Some(tx) = full_tx {
|
||||
_ = tx.send(());
|
||||
}
|
||||
owner.with(|| {
|
||||
OwnedView::new(view).into_any()
|
||||
})
|
||||
@@ -766,7 +798,7 @@ where
|
||||
// if this children has matches, then rebuild the lower section of the tree
|
||||
if let Some(child) = child {
|
||||
child.build_nested_route(
|
||||
url, base, loaders, outlets, &owner,
|
||||
url, base, preloaders, outlets, &owner,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -782,7 +814,14 @@ where
|
||||
let owner = current.owner.clone();
|
||||
*items += 1;
|
||||
child.rebuild_nested_route(
|
||||
url, base, items, loaders, outlets, &owner,
|
||||
url,
|
||||
base,
|
||||
items,
|
||||
preloaders,
|
||||
full_loaders,
|
||||
outlets,
|
||||
&owner,
|
||||
set_is_routing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router_macro"
|
||||
version = "0.7.0-gamma"
|
||||
version = "0.7.0-gamma3"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tachys"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -180,6 +180,4 @@ tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing", "sledgehammer"]
|
||||
skip_feature_sets = [
|
||||
["ssr", "hydrate"],
|
||||
]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
@@ -180,6 +180,84 @@ pub trait IntoClass: Send {
|
||||
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send;
|
||||
}
|
||||
|
||||
impl<T: IntoClass> IntoClass for Option<T> {
|
||||
type AsyncOutput = Option<T::AsyncOutput>;
|
||||
type State = (crate::renderer::types::Element, Option<T::State>);
|
||||
type Cloneable = Option<T::Cloneable>;
|
||||
type CloneableOwned = Option<T::CloneableOwned>;
|
||||
|
||||
fn html_len(&self) -> usize {
|
||||
self.as_ref().map_or(0, IntoClass::html_len)
|
||||
}
|
||||
|
||||
fn to_html(self, class: &mut String) {
|
||||
if let Some(t) = self {
|
||||
t.to_html(class);
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
) -> Self::State {
|
||||
if let Some(t) = self {
|
||||
(el.clone(), Some(t.hydrate::<FROM_SERVER>(el)))
|
||||
} else {
|
||||
(el.clone(), None)
|
||||
}
|
||||
}
|
||||
|
||||
fn build(self, el: &crate::renderer::types::Element) -> Self::State {
|
||||
if let Some(t) = self {
|
||||
(el.clone(), Some(t.build(el)))
|
||||
} else {
|
||||
(el.clone(), None)
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let el = &state.0;
|
||||
let prev_state = &mut state.1;
|
||||
let maybe_next_t_state = match (prev_state, self) {
|
||||
(Some(_prev_t_state), None) => {
|
||||
Rndr::remove_attribute(el, "class");
|
||||
Some(None)
|
||||
}
|
||||
(None, Some(t)) => Some(Some(t.build(el))),
|
||||
(Some(prev_t_state), Some(t)) => {
|
||||
t.rebuild(prev_t_state);
|
||||
None
|
||||
}
|
||||
(None, None) => Some(None),
|
||||
};
|
||||
if let Some(next_t_state) = maybe_next_t_state {
|
||||
state.1 = next_t_state;
|
||||
}
|
||||
}
|
||||
|
||||
fn into_cloneable(self) -> Self::Cloneable {
|
||||
self.map(|t| t.into_cloneable())
|
||||
}
|
||||
|
||||
fn into_cloneable_owned(self) -> Self::CloneableOwned {
|
||||
self.map(|t| t.into_cloneable_owned())
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
if let Some(t) = self {
|
||||
t.dry_resolve();
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
if let Some(t) = self {
|
||||
Some(t.resolve().await)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoClass for &'a str {
|
||||
type AsyncOutput = Self;
|
||||
type State = (crate::renderer::types::Element, Self);
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use super::{ReactiveFunction, SharedReactiveFunction, Suspend};
|
||||
use super::{ReactiveFunction, SharedReactiveFunction};
|
||||
use crate::{html::class::IntoClass, renderer::Rndr};
|
||||
use futures::FutureExt;
|
||||
use reactive_graph::{effect::RenderEffect, signal::guards::ReadGuard};
|
||||
use std::{
|
||||
borrow::Borrow, cell::RefCell, future::Future, ops::Deref, rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{borrow::Borrow, ops::Deref, sync::Arc};
|
||||
|
||||
impl<F, C> IntoClass for F
|
||||
where
|
||||
@@ -709,6 +705,7 @@ mod stable {
|
||||
class_signal!(ArcSignal);
|
||||
}
|
||||
|
||||
/*
|
||||
impl<Fut> IntoClass for Suspend<Fut>
|
||||
where
|
||||
Fut: Clone + Future + Send + 'static,
|
||||
@@ -789,3 +786,4 @@ where
|
||||
self.inner.await
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::{
|
||||
use reactive_graph::effect::RenderEffect;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
@@ -361,9 +360,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, V> AttributeValue for Suspend<Fut>
|
||||
impl<V> AttributeValue for Suspend<V>
|
||||
where
|
||||
Fut: Future<Output = V> + Send + 'static,
|
||||
V: AttributeValue + 'static,
|
||||
V::State: 'static,
|
||||
{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::html::{element::ElementType, node_ref::NodeRefContainer};
|
||||
use reactive_graph::{
|
||||
signal::RwSignal,
|
||||
traits::{DefinedAt, Set, Track, WithUntracked},
|
||||
signal::{
|
||||
guards::{Derefable, ReadGuard},
|
||||
RwSignal,
|
||||
},
|
||||
traits::{DefinedAt, ReadUntracked, Set, Track},
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use wasm_bindgen::JsCast;
|
||||
@@ -75,19 +78,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> WithUntracked for NodeRef<E>
|
||||
impl<E> ReadUntracked for NodeRef<E>
|
||||
where
|
||||
E: ElementType,
|
||||
E::Output: JsCast + Clone + 'static,
|
||||
{
|
||||
type Value = Option<E::Output>;
|
||||
type Value = ReadGuard<Option<E::Output>, Derefable<Option<E::Output>>>;
|
||||
|
||||
fn try_with_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
self.0
|
||||
.try_with_untracked(|inner| fun(&inner.as_deref().cloned()))
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
Some(ReadGuard::new(Derefable(
|
||||
self.0.try_read_untracked()?.as_deref().cloned(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use super::{ReactiveFunction, SharedReactiveFunction, Suspend};
|
||||
use super::{ReactiveFunction, SharedReactiveFunction};
|
||||
use crate::{html::style::IntoStyle, renderer::Rndr};
|
||||
use futures::FutureExt;
|
||||
use reactive_graph::effect::RenderEffect;
|
||||
use std::{borrow::Cow, cell::RefCell, future::Future, rc::Rc};
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl<F, S> IntoStyle for (&'static str, F)
|
||||
where
|
||||
@@ -438,6 +437,7 @@ mod stable {
|
||||
style_signal!(ArcSignal);
|
||||
}
|
||||
|
||||
/*
|
||||
impl<Fut> IntoStyle for Suspend<Fut>
|
||||
where
|
||||
Fut: Clone + Future + Send + 'static,
|
||||
@@ -514,3 +514,4 @@ where
|
||||
self.inner.await
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -36,10 +36,9 @@ use std::{
|
||||
use throw_error::ErrorHook;
|
||||
|
||||
/// A suspended `Future`, which can be used in the view.
|
||||
#[derive(Clone)]
|
||||
pub struct Suspend<Fut> {
|
||||
pub struct Suspend<T> {
|
||||
pub(crate) subscriber: SuspendSubscriber,
|
||||
pub(crate) inner: Pin<Box<ScopedFuture<Fut>>>,
|
||||
pub(crate) inner: Pin<Box<dyn Future<Output = T> + Send>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -114,9 +113,9 @@ impl ToAnySubscriber for SuspendSubscriber {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> Suspend<Fut> {
|
||||
impl<T> Suspend<T> {
|
||||
/// Creates a new suspended view.
|
||||
pub fn new(fut: Fut) -> Self {
|
||||
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
|
||||
let subscriber = SuspendSubscriber::new();
|
||||
let any_subscriber = subscriber.to_any_subscriber();
|
||||
let inner =
|
||||
@@ -125,7 +124,7 @@ impl<Fut> Suspend<Fut> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> Debug for Suspend<Fut> {
|
||||
impl<T> Debug for Suspend<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Suspend").finish()
|
||||
}
|
||||
@@ -160,12 +159,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> Render for Suspend<Fut>
|
||||
impl<T> Render for Suspend<T>
|
||||
where
|
||||
Fut: Future + 'static,
|
||||
Fut::Output: Render,
|
||||
T: Render + 'static,
|
||||
{
|
||||
type State = SuspendState<Fut::Output>;
|
||||
type State = SuspendState<T>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let Self { subscriber, inner } = self;
|
||||
@@ -255,22 +253,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> AddAnyAttr for Suspend<Fut>
|
||||
impl<T> AddAnyAttr for Suspend<T>
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: AddAnyAttr,
|
||||
T: Send + AddAnyAttr + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute> = Suspend<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = <Fut::Output as AddAnyAttr>::Output<
|
||||
SomeNewAttr::CloneableOwned,
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
>,
|
||||
>;
|
||||
type Output<SomeNewAttr: Attribute> =
|
||||
Suspend<<T as AddAnyAttr>::Output<SomeNewAttr::CloneableOwned>>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
@@ -280,21 +268,20 @@ where
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
let attr = attr.into_cloneable_owned();
|
||||
Suspend::new(Box::pin(async move {
|
||||
Suspend::new(async move {
|
||||
let this = self.inner.await;
|
||||
this.add_any_attr(attr)
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> RenderHtml for Suspend<Fut>
|
||||
impl<T> RenderHtml for Suspend<T>
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: RenderHtml,
|
||||
T: RenderHtml + Sized + 'static,
|
||||
{
|
||||
type AsyncOutput = Option<Fut::Output>;
|
||||
type AsyncOutput = Option<T>;
|
||||
|
||||
const MIN_LENGTH: usize = Fut::Output::MIN_LENGTH;
|
||||
const MIN_LENGTH: usize = T::MIN_LENGTH;
|
||||
|
||||
fn to_html_with_buf(
|
||||
self,
|
||||
@@ -440,6 +427,23 @@ where
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
self.inner.as_mut().now_or_never();
|
||||
// this is a little crazy, but if a Suspend is immediately available, we end up
|
||||
// with a situation where polling it multiple times (here in dry_resolve and then in
|
||||
// resolve) causes a runtime panic
|
||||
// (see https://github.com/leptos-rs/leptos/issues/3113)
|
||||
//
|
||||
// at the same time, we do need to dry_resolve Suspend so that we can register synchronous
|
||||
// resource reads that happen inside them
|
||||
// (see https://github.com/leptos-rs/leptos/issues/2917)
|
||||
//
|
||||
// fuse()-ing the Future doesn't work, because that will cause the subsequent resolve()
|
||||
// simply to be pending forever
|
||||
//
|
||||
// in this case, though, we can simply... discover that the data are already here, and then
|
||||
// stuff them back into a new Future, which can safely be polled after its completion
|
||||
if let Some(inner) = self.inner.as_mut().now_or_never() {
|
||||
self.inner = Box::pin(async move { inner })
|
||||
as Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ impl<'a> RenderHtml for &'a str {
|
||||
if matches!(position, Position::NextChildAfterText) {
|
||||
buf.push_str("<!>")
|
||||
}
|
||||
if self.is_empty() {
|
||||
if self.is_empty() && escape {
|
||||
buf.push(' ');
|
||||
} else if escape {
|
||||
let escaped = html_escape::encode_text(self);
|
||||
|
||||
Reference in New Issue
Block a user