Compare commits

..

2 Commits

Author SHA1 Message Date
Greg Johnston
9c4a6ba9ba fix sandboxed tests 2024-10-08 15:16:33 -04:00
Greg Johnston
d63c2fd507 chore: re-add regression tests from #2639 2024-10-07 21:15:31 -04:00
62 changed files with 1101 additions and 1311 deletions

View File

@@ -94,7 +94,7 @@ jobs:
fi
done
- name: Install Deno
uses: denoland/setup-deno@v2
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Maybe install gtk-rs dependencies

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.0-gamma3"
version = "0.7.0-gamma"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
throw_error = { path = "./any_error/", version = "0.2.0-gamma" }
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-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" }
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" }
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-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" }
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" }
[profile.release]
codegen-units = 1

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.2.0-gamma3"
version = "0.2.0-gamma"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -9,7 +9,6 @@ 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"
@@ -20,14 +19,12 @@ 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"]

View File

@@ -32,14 +32,11 @@
use std::{future::Future, pin::Pin, sync::OnceLock};
use thiserror::Error;
/// 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>>>;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub(crate) 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)]
@@ -118,14 +115,6 @@ 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 {
@@ -204,15 +193,13 @@ impl Executor {
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
pub fn init_futures_executor() -> Result<(), ExecutorError> {
use futures::{
executor::{LocalPool, LocalSpawner, ThreadPool},
executor::{LocalPool, ThreadPool},
task::{LocalSpawnExt, SpawnExt},
};
use std::cell::RefCell;
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
thread_local! {
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
static LOCAL_POOL: LocalPool = LocalPool::new();
}
fn get_thread_pool() -> &'static ThreadPool {
@@ -231,97 +218,28 @@ impl Executor {
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
SPAWNER.with(|spawner| {
LOCAL_POOL.with(|pool| {
let spawner = pool.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(())
}
/// 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);
#[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 {});
}
}

View File

@@ -1,55 +0,0 @@
#[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);
}

View File

@@ -1,38 +0,0 @@
#[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);
}

View File

@@ -5,14 +5,13 @@ use leptos::prelude::*;
use leptos_router::{
components::{
Form, Outlet, ParentRoute, ProtectedRoute, Redirect, Route, Router,
Routes, RoutingProgress, A,
Routes, 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)]
@@ -27,14 +26,9 @@ 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 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>
<Router>
<nav>
// ordinary <a> elements can be used for client-side navigation
// using <A> has two effects:
@@ -70,7 +64,7 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component(transparent)]
#[component]
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>

View File

@@ -3,6 +3,7 @@ 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

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.0-gamma3"
version = "0.2.0-gamma"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1381,41 +1381,39 @@ where
),
)
} else {
router
.route(path, web::head().to(HttpResponse::Ok))
.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
};
}
}

View File

@@ -16,6 +16,8 @@ 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"] }
@@ -24,6 +26,7 @@ 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"

View File

@@ -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,7 +1342,8 @@ where
.with(|| {
// stub out a path for now
provide_context(RequestUrl::new(""));
let (mock_parts, _) = Request::new(Body::from("")).into_parts();
let (mock_parts, _) =
http::Request::new(Body::from("")).into_parts();
let (mock_meta, _) = ServerMetaContext::new();
provide_contexts("", &mock_meta, mock_parts, Default::default());
additional_context();
@@ -1401,8 +1402,8 @@ impl StaticRouteGenerator {
let add_context = additional_context.clone();
move || {
let full_path = format!("http://leptos.dev{path}");
let mock_req = Request::builder()
.method(Method::GET)
let mock_req = http::Request::builder()
.method(http::Method::GET)
.header("Accept", "text/html")
.body(Body::empty())
.unwrap();
@@ -1494,12 +1495,10 @@ impl StaticRouteGenerator {
_ = routes;
_ = app_fn;
_ = additional_context;
Self(Box::new(|_| {
panic!(
"Static routes are not currently supported on WASM32 \
server targets."
);
}))
panic!(
"Static routes are not currently supported on WASM32 server \
targets."
);
}
}
@@ -1934,7 +1933,7 @@ where
///
/// #[server]
/// pub async fn request_method() -> Result<String, ServerFnError> {
/// use axum::http::Method;
/// use http::Method;
/// use leptos_axum::extract;
///
/// // you can extract anything that a regular Axum extractor can extract

View File

@@ -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"

View File

@@ -0,0 +1,59 @@
#![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())
}
}

View File

@@ -1,15 +1,12 @@
use crate::{children::ChildrenFn, component, control_flow::Show, IntoView};
use crate::{ChildrenFn, Show};
use core::time::Duration;
use leptos_dom::helpers::TimeoutHandle;
use leptos::component;
use leptos_dom::{helpers::TimeoutHandle, IntoView};
use leptos_macro::view;
use reactive_graph::{
effect::RenderEffect,
owner::{on_cleanup, StoredValue},
signal::RwSignal,
traits::{Get, GetUntracked, GetValue, Set, SetValue},
wrappers::read::Signal,
use leptos_reactive::{
create_render_effect, on_cleanup, signal_prelude::*, store_value,
StoredValue,
};
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`,
@@ -19,10 +16,10 @@ use tachys::prelude::*;
///
/// ```rust
/// # use core::time::Duration;
/// # use leptos::prelude::*;
/// # use leptos::*;
/// # #[component]
/// # pub fn App() -> impl IntoView {
/// let show = RwSignal::new(false);
/// let show = create_rw_signal(false);
///
/// view! {
/// <div
@@ -53,7 +50,7 @@ pub fn AnimatedShow(
children: ChildrenFn,
/// If the component should show or not
#[prop(into)]
when: Signal<bool>,
when: MaybeSignal<bool>,
/// Optional CSS class to apply if `when == true`
#[prop(optional)]
show_class: &'static str,
@@ -63,15 +60,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>> = StoredValue::new(None);
let cls = RwSignal::new(if when.get_untracked() {
let handle: StoredValue<Option<TimeoutHandle>> = store_value(None);
let cls = create_rw_signal(if when.get_untracked() {
show_class
} else {
hide_class
});
let show = RwSignal::new(when.get_untracked());
let show = create_rw_signal(when.get_untracked());
let eff = RenderEffect::new(move |_| {
create_render_effect(move |_| {
if when.get() {
// clear any possibly active timer
if let Some(h) = handle.get_value() {
@@ -96,7 +93,6 @@ pub fn AnimatedShow(
if let Some(Some(h)) = handle.try_get_value() {
h.clear();
}
drop(eff);
});
view! {

View File

@@ -87,7 +87,7 @@ where
T: Send + Sync + Serialize + DeserializeOwned + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
Chil: FnOnce(&T) -> V + Send + 'static,
V: IntoView + 'static,
V: IntoView,
{
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
let ready = res.ready();

View File

@@ -174,9 +174,7 @@ pub mod prelude {
pub use server_fn::{self, ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
view::template::ViewTemplate,
};
}
pub use export_types::*;
@@ -204,9 +202,8 @@ pub mod error {
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
pub mod control_flow {
pub use crate::{animated_show::*, await_::*, for_loop::*, show::*};
pub use crate::{await_::*, for_loop::*, show::*};
}
mod animated_show;
mod await_;
mod for_loop;
mod show;
@@ -329,3 +326,233 @@ 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()
}
}*/

View File

@@ -35,7 +35,7 @@ pub fn Provider<T, Chil>(
) -> impl IntoView
where
T: Send + Sync + 'static,
Chil: IntoView + 'static,
Chil: IntoView,
{
let owner = Owner::current()
.expect("no current reactive Owner found")

30
leptos/src/view_fn.rs Normal file
View File

@@ -0,0 +1,30 @@
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)()
}
}

View File

@@ -1,7 +1,5 @@
#[cfg(feature = "ssr")]
use leptos::html::HtmlElement;
#[cfg(feature = "ssr")]
#[test]
fn simple_ssr_test() {
use leptos::prelude::*;
@@ -22,7 +20,6 @@ fn simple_ssr_test() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_components() {
use leptos::prelude::*;
@@ -54,7 +51,6 @@ fn ssr_test_with_components() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_snake_case_components() {
use leptos::prelude::*;
@@ -85,7 +81,6 @@ fn ssr_test_with_snake_case_components() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn test_classes() {
use leptos::prelude::*;
@@ -103,7 +98,6 @@ fn test_classes() {
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_with_styles() {
use leptos::prelude::*;
@@ -125,7 +119,6 @@ fn ssr_with_styles() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_option() {
use leptos::prelude::*;

View File

@@ -396,7 +396,8 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
feature = "tracing",

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.0-gamma3"
version = "0.7.0-gamma"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -51,27 +51,7 @@ 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",
],
]
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }

View File

@@ -18,7 +18,6 @@ use syn::{
};
pub struct Model {
is_transparent: bool,
island: Option<String>,
docs: Docs,
unknown_attrs: UnknownAttrs,
@@ -63,7 +62,6 @@ impl Parse for Model {
});
Ok(Self {
is_transparent: false,
island: None,
docs,
unknown_attrs,
@@ -104,7 +102,6 @@ pub fn convert_from_snake_case(name: &Ident) -> Ident {
impl ToTokens for Model {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
is_transparent,
island,
docs,
unknown_attrs,
@@ -119,22 +116,20 @@ impl ToTokens for Model {
let no_props = props.is_empty();
// check for components that end ;
if !is_transparent {
let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
_ => None,
});
if let Some(semi) = ends_semi {
proc_macro_error2::emit_error!(
semi.span(),
"A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually \
an accident, not intentional, so we prevent it. If youd \
like to return (), you can do it it explicitly by \
returning () as the last item from the component."
);
}
let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
_ => None,
});
if let Some(semi) = ends_semi {
proc_macro_error2::emit_error!(
semi.span(),
"A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually an \
accident, not intentional, so we prevent it. If youd like \
to return (), you can do it it explicitly by returning () as \
the last item from the component."
);
}
//body.sig.ident = format_ident!("__{}", body.sig.ident);
@@ -270,30 +265,14 @@ impl ToTokens for Model {
}
};
let component = if *is_transparent {
body_expr
} else if cfg!(erase_components) {
quote! {
::leptos::prelude::IntoAny::into_any(
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
)
}
} else {
quote! {
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
}
let component = quote! {
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
};
// add island wrapper if island
@@ -541,13 +520,6 @@ impl ToTokens for Model {
}
impl Model {
#[allow(clippy::wrong_self_convention)]
pub fn is_transparent(mut self, is_transparent: bool) -> Self {
self.is_transparent = is_transparent;
self
}
#[allow(clippy::wrong_self_convention)]
pub fn with_island(mut self, island: Option<String>) -> Self {
self.island = island;

View File

@@ -535,24 +535,11 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
if transparent != "transparent" {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[component(transparent)]` or `#[component]`"
);
}
true
} else {
false
};
component_macro(s, is_transparent, None)
pub fn component(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
component_macro(s, None)
}
/// Defines a component as an interactive island when you are using the
@@ -628,37 +615,17 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
if transparent != "transparent" {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[component(transparent)]` or `#[component]`"
);
}
true
} else {
false
};
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let island_src = s.to_string();
component_macro(s, is_transparent, Some(island_src))
component_macro(s, Some(island_src))
}
fn component_macro(
s: TokenStream,
is_transparent: bool,
island: Option<String>,
) -> TokenStream {
fn component_macro(s: TokenStream, island: Option<String>) -> TokenStream {
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(s);
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.is_transparent(is_transparent).with_island(island).into_token_stream();
let expanded = model.with_island(island).into_token_stream();
if !matches!(unexpanded.vis, Visibility::Public(_)) {
unexpanded.vis = Visibility::Public(Pub {
span: unexpanded.vis.span(),

View File

@@ -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) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
fn from_map(map: &::leptos_router::params::ParamsMap) -> Result<Self, ::leptos_router::params::ParamsError> {
Ok(Self {
#(#fields,)*
})

View File

@@ -179,7 +179,7 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
}
enum Item<'a, T> {
Node(&'a Node<T>, bool),
Node(&'a Node<T>),
ClosingTag(String),
}
@@ -290,11 +290,10 @@ impl<'a> InertElementBuilder<'a> {
fn inert_element_to_tokens(
node: &Node<impl CustomNode>,
escape_text: bool,
global_class: Option<&TokenTree>,
) -> Option<TokenStream> {
let mut html = InertElementBuilder::new(global_class);
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
let mut nodes = VecDeque::from([Item::Node(node)]);
while let Some(current) = nodes.pop_front() {
match current {
@@ -304,32 +303,21 @@ fn inert_element_to_tokens(
html.push_str(&tag);
html.push('>');
}
Item::Node(current, escape) => {
Item::Node(current) => {
match current {
Node::RawText(raw) => {
let text = raw.to_string_best();
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
let text = html_escape::encode_text(&text);
html.push_str(&text);
}
Node::Text(text) => {
let text = text.value_string();
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
let text = html_escape::encode_text(&text);
html.push_str(&text);
}
Node::Element(node) => {
let self_closing = is_self_closing(node);
let el_name = node.name().to_string();
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
// opening tag
html.push('<');
@@ -376,7 +364,7 @@ fn inert_element_to_tokens(
nodes.push_front(Item::ClosingTag(el_name));
let children = node.children.iter().rev();
for child in children {
nodes.push_front(Item::Node(child, escape));
nodes.push_front(Item::Node(child));
}
}
}
@@ -571,11 +559,7 @@ fn node_to_tokens(
}
Node::Element(el_node) => {
if !top_level && is_inert {
let el_name = el_node.name().to_string();
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
inert_element_to_tokens(node, escape, global_class)
inert_element_to_tokens(node, global_class)
} else {
element_to_tokens(
el_node,

View File

@@ -1,4 +1,3 @@
#[cfg(not(erase_components))]
#[test]
fn ui() {
let t = trybuild::TestCases::new();

View File

@@ -185,7 +185,7 @@ impl FromEncodedStr for [u8] {
mod view_implementations {
use crate::Resource;
use reactive_graph::traits::Read;
use std::future::Future;
use std::{future::Future, pin::Pin};
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
@@ -219,11 +219,16 @@ mod view_implementations {
{
type Output<SomeNewAttr: Attribute> = Box<
dyn FnMut() -> Suspend<
<T as AddAnyAttr>::Output<
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
>,
>
+ Send
Pin<
Box<
dyn Future<
Output = <T as AddAnyAttr>::Output<
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
>,
> + Send,
>,
>,
> + Send,
>;
fn add_any_attr<NewAttr: Attribute>(

View File

@@ -434,14 +434,6 @@ pub struct OnceResource<T, Ser = JsonSerdeCodec> {
defined_at: &'static Location<'static>,
}
impl<T, Ser> Clone for OnceResource<T, Ser> {
fn clone(&self) -> Self {
*self
}
}
impl<T, Ser> Copy for OnceResource<T, Ser> {}
impl<T, Ser> OnceResource<T, Ser>
where
T: Send + Sync + 'static,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.0-gamma3"
version = "0.7.0-gamma"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -1,6 +1,6 @@
[package]
name = "next_tuple"
version = "0.1.0-gamma3"
version = "0.1.0-gamma"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.1.0-gamma3"
version = "0.1.0-gamma"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,12 +1,9 @@
use crate::{
computed::{ArcMemo, Memo},
diagnostics::is_suppressing_resource_load,
owner::{
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
SyncStorage,
},
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{ArcRwSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update},
traits::{DefinedAt, Dispose, Get, GetUntracked, Update},
unwrap_signal,
};
use any_spawner::Executor;
@@ -96,7 +93,6 @@ pub struct ArcAction<I, O> {
input: ArcRwSignal<Option<I>>,
value: ArcRwSignal<Option<O>>,
version: ArcRwSignal<usize>,
dispatched: ArcStoredValue<usize>,
#[allow(clippy::complexity)]
action_fn: Arc<
dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>> + Send + Sync,
@@ -112,7 +108,6 @@ impl<I, O> Clone for ArcAction<I, O> {
input: self.input.clone(),
value: self.value.clone(),
version: self.version.clone(),
dispatched: self.dispatched.clone(),
action_fn: self.action_fn.clone(),
#[cfg(debug_assertions)]
defined_at: self.defined_at,
@@ -196,7 +191,6 @@ where
input: Default::default(),
value: ArcRwSignal::new(value),
version: Default::default(),
dispatched: Default::default(),
action_fn: Arc::new(move |input| Box::pin(action_fn(input))),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
@@ -236,14 +230,14 @@ where
// Update the state before loading
self.in_flight.update(|n| *n += 1);
let current_version = self.dispatched.get_value();
let current_version =
self.version.try_get_untracked().unwrap_or_default();
self.input.try_update(|inp| *inp = Some(input));
// Spawn the task
crate::spawn({
let input = self.input.clone();
let version = self.version.clone();
let dispatched = self.dispatched.clone();
let value = self.value.clone();
let in_flight = self.in_flight.clone();
async move {
@@ -255,7 +249,7 @@ where
// otherwise, update the value
result = fut => {
in_flight.update(|n| *n = n.saturating_sub(1));
let is_latest = dispatched.get_value() <= current_version;
let is_latest = version.get_untracked() <= current_version;
if is_latest {
version.update(|n| *n += 1);
value.update(|n| *n = Some(result));
@@ -288,7 +282,8 @@ where
// Update the state before loading
self.in_flight.update(|n| *n += 1);
let current_version = self.dispatched.get_value();
let current_version =
self.version.try_get_untracked().unwrap_or_default();
self.input.try_update(|inp| *inp = Some(input));
// Spawn the task
@@ -296,7 +291,6 @@ where
let input = self.input.clone();
let version = self.version.clone();
let value = self.value.clone();
let dispatched = self.dispatched.clone();
let in_flight = self.in_flight.clone();
async move {
select! {
@@ -307,7 +301,7 @@ where
// otherwise, update the value
result = fut => {
in_flight.update(|n| *n = n.saturating_sub(1));
let is_latest = dispatched.get_value() <= current_version;
let is_latest = version.get_untracked() <= current_version;
if is_latest {
version.update(|n| *n += 1);
value.update(|n| *n = Some(result));
@@ -357,7 +351,6 @@ where
input: Default::default(),
value: ArcRwSignal::new(value),
version: Default::default(),
dispatched: Default::default(),
action_fn: Arc::new(move |input| {
Box::pin(SendWrapper::new(action_fn(input)))
}),

View File

@@ -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: Clone + Send + Sync + Serialize,
T: Send + Sync + Serialize,
St: Storage<SignalTypes<T, St>> + Storage<T>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>

View File

@@ -623,46 +623,3 @@ 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)
}
}

View File

@@ -168,20 +168,11 @@ 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: DefinedAt {
pub trait Read {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
@@ -194,9 +185,7 @@ pub trait Read: DefinedAt {
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
fn read(&self) -> Self::Value;
}
impl<T> Read for T
@@ -206,18 +195,13 @@ where
type Value = T::Value;
fn try_read(&self) -> Option<Self::Value> {
// 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()
}
self.track();
self.try_read_untracked()
}
fn read(&self) -> Self::Value {
self.track();
self.read_untracked()
}
}
@@ -323,13 +307,14 @@ pub trait With: DefinedAt {
impl<T> With for T
where
T: Read,
T: WithUntracked + Track,
{
type Value = <<T as Read>::Value as Deref>::Target;
type Value = <T as WithUntracked>::Value;
#[track_caller]
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
self.try_read().map(|val| fun(&val))
self.track();
self.try_with_untracked(fun)
}
}

View File

@@ -15,6 +15,7 @@ pub mod read {
},
traits::{
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
With, WithValue,
},
unwrap_signal,
};
@@ -200,6 +201,29 @@ 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,
@@ -261,23 +285,22 @@ pub mod read {
}
}
impl<T, S> Track for ArcSignal<T, S>
impl<T, S> With for ArcSignal<T, S>
where
S: Storage<T>,
T: Clone,
{
fn track(&self) {
type Value = T;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
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),
}
}
}
@@ -305,27 +328,32 @@ pub mod read {
}
.map(ReadGuard::new)
}
}
/// 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)
}
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)
}
.map(ReadGuard::new),
)
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))
}
}
@@ -404,31 +432,27 @@ pub mod read {
}
}
impl<T, S> Track for Signal<T, S>
impl<T, S> With for Signal<T, S>
where
T: 'static,
S: Storage<T> + Storage<SignalTypes<T, S>>,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
fn track(&self) {
let inner = self
.inner
type Value = T;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
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(_) => {}
}
.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),
})
}
}
@@ -462,33 +486,41 @@ pub mod read {
.map(ReadGuard::new)
})
}
}
/// 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)
}
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)
}
.map(ReadGuard::new)
}),
)
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))
}
}
@@ -588,6 +620,36 @@ 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,
@@ -768,20 +830,6 @@ 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
@@ -855,14 +903,20 @@ pub mod read {
}
}
impl<T, S> Track for MaybeSignal<T, S>
impl<T, S> With for MaybeSignal<T, S>
where
S: Storage<T> + Storage<SignalTypes<T, S>>,
T: Send + Sync + 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
fn track(&self) {
type Value = T;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match self {
Self::Static(_) => {}
Self::Dynamic(signal) => signal.track(),
Self::Static(t) => Some(fun(t)),
Self::Dynamic(s) => s.try_with(fun),
}
}
}
@@ -882,13 +936,27 @@ pub mod read {
Self::Dynamic(s) => s.try_read_untracked(),
}
}
}
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
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> {
match self {
Self::Static(_) => None,
Self::Dynamic(s) => s.custom_try_read(),
Self::Static(t) => {
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
}
Self::Dynamic(s) => s.try_read(),
}
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> MaybeSignal<T>
@@ -912,6 +980,21 @@ 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>,
@@ -1109,14 +1192,20 @@ pub mod read {
}
}
impl<T, S> Track for MaybeProp<T, S>
impl<T, S> With for MaybeProp<T, S>
where
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
T: Send + Sync + 'static,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
fn track(&self) {
type Value = Option<T>;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match &self.0 {
None => {}
Some(signal) => signal.track(),
None => Some(fun(&None)),
Some(inner) => inner.try_with(fun),
}
}
}
@@ -1134,13 +1223,25 @@ pub mod read {
Some(inner) => inner.try_read_untracked(),
}
}
}
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
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> {
match &self.0 {
None => None,
Some(inner) => inner.custom_try_read(),
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
Some(inner) => inner.try_read(),
}
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> MaybeProp<T>
@@ -1156,6 +1257,21 @@ 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>>,

View File

@@ -1,11 +1,6 @@
[package]
name = "reactive_stores"
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."
version = "0.1.0-gamma"
rust-version.workspace = true
edition.workspace = true
@@ -16,10 +11,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"] }

View File

@@ -1,15 +0,0 @@
# 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.

View File

@@ -10,7 +10,6 @@ use reactive_graph::{
Write,
},
};
pub use reactive_stores_macro::*;
use rustc_hash::FxHashMap;
use std::{
any::Any,
@@ -172,26 +171,12 @@ impl KeyMap {
where
K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static,
{
// this incredibly defensive mechanism takes the guard twice
// on initialization. unfortunately, this is because `initialize`, on
// a nested keyed field can, when being initialized), can in fact try
// to take the lock again, as we try to insert the keys of the parent
// while inserting the keys on this child.
//
// see here https://github.com/leptos-rs/leptos/issues/3086
let mut guard = self.0.write().or_poisoned();
if guard.contains_key(&path) {
let entry = guard.get_mut(&path)?;
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
} else {
drop(guard);
let keys = Box::new(FieldKeys::new(initialize()));
let mut guard = self.0.write().or_poisoned();
let entry = guard.entry(path).or_insert(keys);
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
}
let entry = guard
.entry(path)
.or_insert_with(|| Box::new(FieldKeys::new(initialize())));
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
}
}
@@ -445,6 +430,7 @@ mod tests {
effect::Effect,
traits::{Read, ReadUntracked, Set, Update, Write},
};
use reactive_stores_macro::{Patch, Store};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,

View File

@@ -51,6 +51,7 @@ mod tests {
effect::Effect,
traits::{Get, Read, ReadUntracked, Set, Write},
};
use reactive_stores_macro::Store;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,

View File

@@ -1,11 +1,6 @@
[package]
name = "reactive_stores_macro"
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."
version = "0.1.0-gamma"
rust-version.workspace = true
edition.workspace = true

View File

@@ -1 +0,0 @@
This crate provides macro that are helpful or required when using the `reactive_stores` crate.

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.7.0-gamma3"
version = "0.7.0-gamma"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -22,11 +22,13 @@ 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"

View File

@@ -24,7 +24,6 @@ use reactive_graph::{
use std::{
borrow::Cow,
fmt::{Debug, Display},
mem,
sync::Arc,
time::Duration,
};
@@ -48,7 +47,7 @@ where
}
}
#[component(transparent)]
#[component]
pub fn Router<Chil>(
/// The base URL for the router. Defaults to `""`.
#[prop(optional, into)]
@@ -102,7 +101,6 @@ where
location,
state,
set_is_routing,
query_mutations: Default::default(),
});
let children = children.into_inner();
@@ -116,8 +114,6 @@ pub(crate) struct RouterContext {
pub location: Location,
pub state: ArcRwSignal<State>,
pub set_is_routing: Option<SignalSetter<bool>>,
pub query_mutations:
ArcStoredValue<Vec<(Oco<'static, str>, Option<String>)>>,
}
impl RouterContext {
@@ -134,7 +130,7 @@ impl RouterContext {
resolve_path("", path, None)
};
let mut url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
let url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
Some(Ok(url)) => url,
Some(Err(e)) => {
leptos::logging::error!("Error parsing URL: {e:?}");
@@ -145,22 +141,6 @@ impl RouterContext {
return;
}
};
let query_mutations =
mem::take(&mut *self.query_mutations.write_value());
if !query_mutations.is_empty() {
for (key, value) in query_mutations {
if let Some(value) = value {
url.search_params_mut().replace(key, value);
} else {
url.search_params_mut().remove(&key);
}
}
*url.search_mut() = url
.search_params()
.to_query_string()
.trim_start_matches('?')
.into()
}
if url.origin() != current.origin() {
window().location().set_href(path).unwrap();
@@ -173,14 +153,13 @@ impl RouterContext {
}
// update URL signal, if necessary
let value = url.to_full_path();
if current != url {
drop(current);
self.current_url.set(url);
}
BrowserUrl::complete_navigation(&LocationChange {
value,
value: path.to_string(),
replace: options.replace,
scroll: options.scroll,
state: options.state,
@@ -225,7 +204,7 @@ where
}
}*/
#[component(transparent)]
#[component]
pub fn Routes<Defs, FallbackFn, Fallback>(
fallback: FallbackFn,
children: RouteChildren<Defs>,
@@ -248,10 +227,7 @@ where
base.upgrade_inplace();
base
});
let routes = Routes::new_with_base(
children.into_inner(),
base.clone().unwrap_or_default(),
);
let routes = Routes::new(children.into_inner());
let outer_owner =
Owner::current().expect("creating Routes, but no Owner was found");
move || {
@@ -271,7 +247,7 @@ where
}
}
#[component(transparent)]
#[component]
pub fn FlatRoutes<Defs, FallbackFn, Fallback>(
fallback: FallbackFn,
children: RouteChildren<Defs>,
@@ -297,10 +273,7 @@ where
base.upgrade_inplace();
base
});
let routes = Routes::new_with_base(
children.into_inner(),
base.clone().unwrap_or_default(),
);
let routes = Routes::new(children.into_inner());
let outer_owner =
Owner::current().expect("creating Router, but no Owner was found");
@@ -321,7 +294,7 @@ where
}
}
#[component(transparent)]
#[component]
pub fn Route<Segments, View>(
path: Segments,
view: View,
@@ -333,7 +306,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr)
}
#[component(transparent)]
#[component]
pub fn ParentRoute<Segments, View, Children>(
path: Segments,
view: View,
@@ -347,7 +320,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr).child(children)
}
#[component(transparent)]
#[component]
pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
path: Segments,
view: ViewFn,
@@ -389,7 +362,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr)
}
#[component(transparent)]
#[component]
pub fn ProtectedParentRoute<Segments, ViewFn, View, C, PathFn, P, Children>(
path: Segments,
view: ViewFn,
@@ -451,7 +424,7 @@ where
///
/// [`leptos_actix`]: <https://docs.rs/leptos_actix/>
/// [`leptos_axum`]: <https://docs.rs/leptos_axum/>
#[component(transparent)]
#[component]
pub fn Redirect<P>(
/// The relative path to which the user should be redirected.
path: P,

View File

@@ -4,18 +4,15 @@ use crate::{
navigate::NavigateOptions,
params::{Params, ParamsError, ParamsMap},
};
use leptos::{leptos_dom::helpers::request_animation_frame, oco::Oco};
use leptos::oco::Oco;
use reactive_graph::{
computed::{ArcMemo, Memo},
owner::{expect_context, use_context},
owner::use_context,
signal::{ArcRwSignal, ReadSignal},
traits::{Get, GetUntracked, ReadUntracked, With, WriteValue},
traits::{Get, GetUntracked, With},
wrappers::write::SignalSetter,
};
use std::{
str::FromStr,
sync::atomic::{AtomicBool, Ordering},
};
use std::str::FromStr;
#[track_caller]
#[deprecated = "This has been renamed to `query_signal` to match Rust naming \
@@ -96,15 +93,10 @@ pub fn query_signal_with_options<T>(
where
T: FromStr + ToString + PartialEq + Send + Sync,
{
static IS_NAVIGATING: AtomicBool = AtomicBool::new(false);
let mut key: Oco<'static, str> = key.into();
let query_map = use_query_map();
let navigate = use_navigate();
let location = use_location();
let RouterContext {
query_mutations, ..
} = expect_context();
let get = Memo::new({
let key = key.clone_inplace();
@@ -116,25 +108,20 @@ where
});
let set = SignalSetter::map(move |value: Option<T>| {
let mut new_query_map = query_map.get();
match value {
Some(value) => {
new_query_map.insert(key.to_string(), value.to_string());
}
None => {
new_query_map.remove(&key);
}
}
let qs = new_query_map.to_query_string();
let path = location.pathname.get_untracked();
let hash = location.hash.get_untracked();
let qs = location.query.read_untracked().to_query_string();
let new_url = format!("{path}{qs}{hash}");
query_mutations
.write_value()
.push((key.clone(), value.as_ref().map(ToString::to_string)));
if !IS_NAVIGATING.load(Ordering::Relaxed) {
IS_NAVIGATING.store(true, Ordering::Relaxed);
request_animation_frame({
let navigate = navigate.clone();
let nav_options = nav_options.clone();
move || {
navigate(&new_url, nav_options.clone());
IS_NAVIGATING.store(false, Ordering::Relaxed)
}
})
}
navigate(&new_url, nav_options.clone());
});
(get, set)

View File

@@ -121,7 +121,7 @@ impl LocationProvider for BrowserUrl {
&& curr.path() == new_url.path()
};
url.set(new_url.clone());
url.set(new_url);
if same_path {
Self::complete_navigation(&loc);
}
@@ -130,19 +130,12 @@ impl LocationProvider for BrowserUrl {
if !same_path {
*pending.lock().or_poisoned() = Some(tx);
}
let url = url.clone();
async move {
if !same_path {
// if it has been canceled, ignore
// otherwise, complete navigation -- i.e., set URL in address bar
if rx.await.is_ok() {
// only update the URL in the browser if this is still the current URL
// if we've navigated to another page in the meantime, don't update the
// browser URL
let curr = url.read_untracked();
if curr == new_url {
Self::complete_navigation(&loc);
}
Self::complete_navigation(&loc);
}
}
}

View File

@@ -36,42 +36,22 @@ impl Url {
&self.origin
}
pub fn origin_mut(&mut self) -> &mut String {
&mut self.origin
}
pub fn path(&self) -> &str {
&self.path
}
pub fn path_mut(&mut self) -> &mut str {
&mut self.path
}
pub fn search(&self) -> &str {
&self.search
}
pub fn search_mut(&mut self) -> &mut String {
&mut self.search
}
pub fn search_params(&self) -> &ParamsMap {
&self.search_params
}
pub fn search_params_mut(&mut self) -> &mut ParamsMap {
&mut self.search_params
}
pub fn hash(&self) -> &str {
&self.hash
}
pub fn hash_mut(&mut self) -> &mut String {
&mut self.hash
}
pub fn provide_server_action_error(&self) {
let search_params = self.search_params();
if let (Some(err), Some(path)) = (
@@ -82,19 +62,6 @@ impl Url {
}
}
pub(crate) fn to_full_path(&self) -> String {
let mut path = self.path.to_string();
if !self.search.is_empty() {
path.push('?');
path.push_str(&self.search);
}
if !self.hash.is_empty() {
path.push('#');
path.push_str(&self.hash);
}
path
}
pub fn escape(s: &str) -> String {
#[cfg(not(feature = "ssr"))]
{

View File

@@ -8,7 +8,7 @@ use crate::{
};
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
use futures::{channel::oneshot, future::join_all, FutureExt};
use futures::{future::join_all, FutureExt};
use leptos::{component, oco::Oco};
use or_poisoned::OrPoisoned;
use reactive_graph::{
@@ -16,7 +16,6 @@ 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;
@@ -48,6 +47,7 @@ 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,37 +155,23 @@ where
state.outlets.clear();
}
Some(route) => {
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();
let mut loaders = Vec::new();
route.rebuild_nested_route(
&self.current_url.read_untracked(),
self.base,
&mut 0,
&mut preloaders,
&mut full_loaders,
&mut 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(preloaders).await;
let triggers = join_all(loaders).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();
}
@@ -440,7 +426,9 @@ where
}
}
type OutletViewFn = Box<dyn Fn() -> Suspend<AnyView> + Send>;
type OutletViewFn = Box<
dyn Fn() -> Suspend<Pin<Box<dyn Future<Output = AnyView> + Send>>> + Send,
>;
pub(crate) struct RouteContext {
id: RouteMatchId,
@@ -498,17 +486,14 @@ 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,
);
}
@@ -644,17 +629,14 @@ where
}
}
#[allow(clippy::too_many_arguments)]
fn rebuild_nested_route(
self,
url: &Url,
base: Option<Oco<'static, str>>,
items: &mut usize,
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
) {
let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets
.iter()
@@ -665,7 +647,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, preloaders, outlets, parent);
self.build_nested_route(url, base, loaders, outlets, parent);
}
Some(current) => {
// a unique ID for each route, which allows us to compare when we get new matches
@@ -734,14 +716,11 @@ 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
preloaders.push(Box::pin(owner.with(|| {
loaders.push(Box::pin(owner.with(|| {
ScopedFuture::new({
let owner = owner.clone();
let trigger = current.trigger.clone();
@@ -757,26 +736,15 @@ 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(
async move {
if set_is_routing {
AsyncTransition::run(|| view.choose()).await
} else {
view.choose().await
}
}
view.choose(),
)
}),
);
let view = view.await;
if let Some(tx) = full_tx {
_ = tx.send(());
}
owner.with(|| {
OwnedView::new(view).into_any()
})
@@ -798,7 +766,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, preloaders, outlets, &owner,
url, base, loaders, outlets, &owner,
);
}
@@ -814,14 +782,7 @@ where
let owner = current.owner.clone();
*items += 1;
child.rebuild_nested_route(
url,
base,
items,
preloaders,
full_loaders,
outlets,
&owner,
set_is_routing,
url, base, items, loaders, outlets, &owner,
);
}
}

View File

@@ -21,9 +21,6 @@ impl ParamsMap {
}
/// Inserts a value into the map.
///
/// If a value with that key already exists, the new value will be added to it.
/// To replace the value instead, see [`replace`].
pub fn insert(&mut self, key: impl Into<Cow<'static, str>>, value: String) {
let value = unescape(&value);
@@ -35,23 +32,6 @@ impl ParamsMap {
}
}
/// Inserts a value into the map, replacing any existing value for that key.
pub fn replace(
&mut self,
key: impl Into<Cow<'static, str>>,
value: String,
) {
let value = unescape(&value);
let key = key.into();
if let Some(prev) = self.0.iter_mut().find(|(k, _)| k == &key) {
prev.1.clear();
prev.1.push(value);
} else {
self.0.push((key, vec![value]));
}
}
/// Gets the most-recently-added value of this param from the map.
pub fn get(&self, key: &str) -> Option<String> {
self.get_str(key).map(ToOwned::to_owned)

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.7.0-gamma3"
version = "0.7.0-gamma"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.1.0-gamma3"
version = "0.1.0-gamma"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -180,4 +180,6 @@ tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]
denylist = ["tracing", "sledgehammer"]
skip_feature_sets = [["ssr", "hydrate"]]
skip_feature_sets = [
["ssr", "hydrate"],
]

View File

@@ -67,121 +67,107 @@ where
let value = Box::new(self) as Box<dyn Any + Send>;
match value.downcast::<AnyAttribute>() {
// if it's already an AnyAttribute, we don't need to double-wrap it
Ok(any_attribute) => *any_attribute,
Err(value) => {
#[cfg(feature = "ssr")]
let to_html =
|value: Box<dyn Any>,
buf: &mut String,
class: &mut String,
style: &mut String,
inner_html: &mut String| {
let value = value.downcast::<T>().expect(
"AnyAttribute::to_html could not be downcast",
);
value.to_html(buf, class, style, inner_html);
};
let build =
|value: Box<dyn Any>,
#[cfg(feature = "ssr")]
let to_html = |value: Box<dyn Any>,
buf: &mut String,
class: &mut String,
style: &mut String,
inner_html: &mut String| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::to_html could not be downcast");
value.to_html(buf, class, style, inner_html);
};
let build = |value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::build couldn't downcast");
let state = Box::new(value.build(el));
let value = value
.downcast::<T>()
.expect("AnyAttribute::build couldn't downcast");
let state = Box::new(value.build(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't \
downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_template =
|value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't \
downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
let rebuild =
|new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyAttributeState| {
let value = value.downcast::<T>().expect(
"AnyAttribute::rebuild couldn't downcast value",
);
if new_type_id == state.type_id {
let state = state.state.downcast_mut().expect(
"AnyAttribute::rebuild couldn't downcast state",
);
value.rebuild(state);
} else {
let new = value.into_any_attr().build(&state.el);
*state = new;
}
};
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(
async move { value.resolve().await.into_any_attr() },
)
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
};
AnyAttribute {
type_id: TypeId::of::<T>(),
html_len,
value,
#[cfg(feature = "ssr")]
to_html,
build,
rebuild,
#[cfg(feature = "hydrate")]
hydrate_from_server,
#[cfg(feature = "hydrate")]
hydrate_from_template,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
}
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_template =
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
let rebuild = |new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyAttributeState| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state
.state
.downcast_mut()
.expect("AnyAttribute::rebuild couldn't downcast state");
value.rebuild(state);
} else {
let new = value.into_any_attr().build(&state.el);
*state = new;
}
};
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any_attr() })
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
};
AnyAttribute {
type_id: TypeId::of::<T>(),
html_len,
value,
#[cfg(feature = "ssr")]
to_html,
build,
rebuild,
#[cfg(feature = "hydrate")]
hydrate_from_server,
#[cfg(feature = "hydrate")]
hydrate_from_template,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
}
}
}

View File

@@ -180,84 +180,6 @@ 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);

View File

@@ -1,7 +1,11 @@
use super::{ReactiveFunction, SharedReactiveFunction};
use super::{ReactiveFunction, SharedReactiveFunction, Suspend};
use crate::{html::class::IntoClass, renderer::Rndr};
use futures::FutureExt;
use reactive_graph::{effect::RenderEffect, signal::guards::ReadGuard};
use std::{borrow::Borrow, ops::Deref, sync::Arc};
use std::{
borrow::Borrow, cell::RefCell, future::Future, ops::Deref, rc::Rc,
sync::Arc,
};
impl<F, C> IntoClass for F
where
@@ -705,7 +709,6 @@ mod stable {
class_signal!(ArcSignal);
}
/*
impl<Fut> IntoClass for Suspend<Fut>
where
Fut: Clone + Future + Send + 'static,
@@ -786,4 +789,3 @@ where
self.inner.await
}
}
*/

View File

@@ -11,6 +11,7 @@ use crate::{
use reactive_graph::effect::RenderEffect;
use std::{
cell::RefCell,
future::Future,
rc::Rc,
sync::{Arc, Mutex},
};
@@ -360,8 +361,9 @@ where
}
}
impl<V> AttributeValue for Suspend<V>
impl<Fut, V> AttributeValue for Suspend<Fut>
where
Fut: Future<Output = V> + Send + 'static,
V: AttributeValue + 'static,
V::State: 'static,
{

View File

@@ -1,10 +1,7 @@
use crate::html::{element::ElementType, node_ref::NodeRefContainer};
use reactive_graph::{
signal::{
guards::{Derefable, ReadGuard},
RwSignal,
},
traits::{DefinedAt, ReadUntracked, Set, Track},
signal::RwSignal,
traits::{DefinedAt, Set, Track, WithUntracked},
};
use send_wrapper::SendWrapper;
use wasm_bindgen::JsCast;
@@ -78,17 +75,19 @@ where
}
}
impl<E> ReadUntracked for NodeRef<E>
impl<E> WithUntracked for NodeRef<E>
where
E: ElementType,
E::Output: JsCast + Clone + 'static,
{
type Value = ReadGuard<Option<E::Output>, Derefable<Option<E::Output>>>;
type Value = Option<E::Output>;
fn try_read_untracked(&self) -> Option<Self::Value> {
Some(ReadGuard::new(Derefable(
self.0.try_read_untracked()?.as_deref().cloned(),
)))
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()))
}
}

View File

@@ -1,7 +1,8 @@
use super::{ReactiveFunction, SharedReactiveFunction};
use super::{ReactiveFunction, SharedReactiveFunction, Suspend};
use crate::{html::style::IntoStyle, renderer::Rndr};
use futures::FutureExt;
use reactive_graph::effect::RenderEffect;
use std::borrow::Cow;
use std::{borrow::Cow, cell::RefCell, future::Future, rc::Rc};
impl<F, S> IntoStyle for (&'static str, F)
where
@@ -437,7 +438,6 @@ mod stable {
style_signal!(ArcSignal);
}
/*
impl<Fut> IntoStyle for Suspend<Fut>
where
Fut: Clone + Future + Send + 'static,
@@ -514,4 +514,3 @@ where
self.inner.await
}
}
*/

View File

@@ -36,9 +36,10 @@ use std::{
use throw_error::ErrorHook;
/// A suspended `Future`, which can be used in the view.
pub struct Suspend<T> {
#[derive(Clone)]
pub struct Suspend<Fut> {
pub(crate) subscriber: SuspendSubscriber,
pub(crate) inner: Pin<Box<dyn Future<Output = T> + Send>>,
pub(crate) inner: Pin<Box<ScopedFuture<Fut>>>,
}
#[derive(Debug, Clone)]
@@ -113,9 +114,9 @@ impl ToAnySubscriber for SuspendSubscriber {
}
}
impl<T> Suspend<T> {
impl<Fut> Suspend<Fut> {
/// Creates a new suspended view.
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
pub fn new(fut: Fut) -> Self {
let subscriber = SuspendSubscriber::new();
let any_subscriber = subscriber.to_any_subscriber();
let inner =
@@ -124,7 +125,7 @@ impl<T> Suspend<T> {
}
}
impl<T> Debug for Suspend<T> {
impl<Fut> Debug for Suspend<Fut> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Suspend").finish()
}
@@ -159,11 +160,12 @@ where
}
}
impl<T> Render for Suspend<T>
impl<Fut> Render for Suspend<Fut>
where
T: Render + 'static,
Fut: Future + 'static,
Fut::Output: Render,
{
type State = SuspendState<T>;
type State = SuspendState<Fut::Output>;
fn build(self) -> Self::State {
let Self { subscriber, inner } = self;
@@ -253,12 +255,22 @@ where
}
}
impl<T> AddAnyAttr for Suspend<T>
impl<Fut> AddAnyAttr for Suspend<Fut>
where
T: Send + AddAnyAttr + 'static,
Fut: Future + Send + 'static,
Fut::Output: AddAnyAttr,
{
type Output<SomeNewAttr: Attribute> =
Suspend<<T as AddAnyAttr>::Output<SomeNewAttr::CloneableOwned>>;
type Output<SomeNewAttr: Attribute> = Suspend<
Pin<
Box<
dyn Future<
Output = <Fut::Output as AddAnyAttr>::Output<
SomeNewAttr::CloneableOwned,
>,
> + Send,
>,
>,
>;
fn add_any_attr<NewAttr: Attribute>(
self,
@@ -268,20 +280,21 @@ where
Self::Output<NewAttr>: RenderHtml,
{
let attr = attr.into_cloneable_owned();
Suspend::new(async move {
Suspend::new(Box::pin(async move {
let this = self.inner.await;
this.add_any_attr(attr)
})
}))
}
}
impl<T> RenderHtml for Suspend<T>
impl<Fut> RenderHtml for Suspend<Fut>
where
T: RenderHtml + Sized + 'static,
Fut: Future + Send + 'static,
Fut::Output: RenderHtml,
{
type AsyncOutput = Option<T>;
type AsyncOutput = Option<Fut::Output>;
const MIN_LENGTH: usize = T::MIN_LENGTH;
const MIN_LENGTH: usize = Fut::Output::MIN_LENGTH;
fn to_html_with_buf(
self,
@@ -427,23 +440,6 @@ where
}
fn dry_resolve(&mut self) {
// 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>>;
}
self.inner.as_mut().now_or_never();
}
}

View File

@@ -27,8 +27,7 @@ use std::{future::Future, pin::Pin};
pub struct AnyView {
type_id: TypeId,
value: Box<dyn Any + Send>,
build: fn(Box<dyn Any>) -> AnyViewState,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
// The fields below are cfg-gated so they will not be included in WASM bundles if not needed.
// Ordinarily, the compiler can simply omit this dead code because the methods are not called.
// With this type-erased wrapper, however, the compiler is not *always* able to correctly
@@ -43,6 +42,8 @@ pub struct AnyView {
#[cfg(feature = "ssr")]
to_html_async_ooo:
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
build: fn(Box<dyn Any>) -> AnyViewState,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
#[cfg(feature = "ssr")]
#[allow(clippy::type_complexity)]
resolve: fn(Box<dyn Any>) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
@@ -137,172 +138,156 @@ where
let value = Box::new(self) as Box<dyn Any + Send>;
match value.downcast::<AnyView>() {
// if it's already an AnyView, we don't need to double-wrap it
Ok(any_view) => *any_view,
Err(value) => {
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any() })
as Pin<Box<dyn Future<Output = AnyView> + Send>>
};
#[cfg(feature = "ssr")]
let to_html =
|value: Box<dyn Any>,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_with_buf(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_async_with_buf::<false>(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async_ooo =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
);
};
let build = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::build couldn't downcast");
let state = Box::new(value.build());
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>,
cursor: &Cursor,
position: &PositionState| {
let value = value.downcast::<T>().expect(
"AnyView::hydrate_from_server couldn't downcast",
);
let state =
Box::new(value.hydrate::<true>(cursor, position));
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
let rebuild =
|new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyViewState| {
let value = value
.downcast::<T>()
.expect("AnyView::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state.state.downcast_mut().expect(
"AnyView::rebuild couldn't downcast state",
);
value.rebuild(state);
} else {
let mut new = value.into_any().build();
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
};
AnyView {
type_id: TypeId::of::<T>(),
value,
build,
rebuild,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
#[cfg(feature = "ssr")]
html_len,
#[cfg(feature = "ssr")]
to_html,
#[cfg(feature = "ssr")]
to_html_async,
#[cfg(feature = "ssr")]
to_html_async_ooo,
#[cfg(feature = "hydrate")]
hydrate_from_server,
}
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any() })
as Pin<Box<dyn Future<Output = AnyView> + Send>>
};
#[cfg(feature = "ssr")]
let to_html = |value: Box<dyn Any>,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_with_buf(buf, position, escape, mark_branches);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async = |value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_async_with_buf::<false>(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async_ooo =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
);
};
let build = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::build couldn't downcast");
let state = Box::new(value.build());
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>, cursor: &Cursor, position: &PositionState| {
let value = value
.downcast::<T>()
.expect("AnyView::hydrate_from_server couldn't downcast");
let state = Box::new(value.hydrate::<true>(cursor, position));
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
let rebuild = |new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyViewState| {
let value = value
.downcast::<T>()
.expect("AnyView::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state
.state
.downcast_mut()
.expect("AnyView::rebuild couldn't downcast state");
value.rebuild(state);
} else {
let mut new = value.into_any().build();
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
};
AnyView {
type_id: TypeId::of::<T>(),
value,
build,
rebuild,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
#[cfg(feature = "ssr")]
html_len,
#[cfg(feature = "ssr")]
to_html,
#[cfg(feature = "ssr")]
to_html_async,
#[cfg(feature = "ssr")]
to_html_async_ooo,
#[cfg(feature = "hydrate")]
hydrate_from_server,
}
}
}
@@ -329,7 +314,7 @@ impl AddAnyAttr for AnyView {
where
Self::Output<NewAttr>: RenderHtml,
{
self
todo!()
}
}

View File

@@ -62,7 +62,7 @@ impl<'a> RenderHtml for &'a str {
if matches!(position, Position::NextChildAfterText) {
buf.push_str("<!>")
}
if self.is_empty() && escape {
if self.is_empty() {
buf.push(' ');
} else if escape {
let escaped = html_escape::encode_text(self);