Compare commits

..

22 Commits

Author SHA1 Message Date
Greg Johnston
faa73ff4db fmt 2024-10-18 16:21:02 -04:00
Penguinwithatie
7291efc077 add tachys::view::fragment::Fragment to prelude 2024-10-16 12:07:42 -06:00
stefnotch
ee66f6c395 Add support for user-supplied executors (#3091) 2024-10-16 06:24:07 -07:00
Greg Johnston
eba08ad592 fix: don't render empty string as a space in unescaped elements (closes #3120) (#3122) 2024-10-15 18:57:09 -04:00
Greg Johnston
4833b4e287 fix: avoid double-polling synchronously-available Suspend (closes #3113) (#3114) 2024-10-15 08:49:40 -04:00
Greg Johnston
9d1be64e4d chore: publish stores (#3110) 2024-10-14 10:18:38 -04:00
benwis
d6e6cd3be0 v0.7.0gamma3 2024-10-14 05:01:19 -07:00
stefnotch
70476f9277 feat: add support for async-executor from smol-rs (#3090) 2024-10-14 07:57:19 -04:00
zakstucke
d8ddfc26e9 perf: use the Track trait for the Signal wrapper. (#3076) 2024-10-12 20:29:03 -04:00
stefnotch
c8acc3e8bd fix: correctly support local pools for futures-executor (#3089) 2024-10-12 20:13:50 -04:00
zakstucke
547442243b impl IntoClass for Option<impl IntoClass> (#3104) 2024-10-12 05:03:53 -07:00
Greg Johnston
6e58266f54 feat: support set_is_routing/RoutingProgress for nested routes (#3101) 2024-10-11 19:05:33 -04:00
Greg Johnston
f0cd0fb41d feat: condense Router/Routes base prop into one (#3100) 2024-10-11 14:06:11 -04:00
Daniil Polyakov
7585faf57e fix: use full path to Result in Params derive (#3096) 2024-10-10 15:20:38 -04:00
zakstucke
da7f6a34e8 chore: expose AnyView in prelude (#3099) 2024-10-10 15:20:24 -04:00
Greg Johnston
4f7fa41262 fix: don't on WASM server targets unless you actually try to generate static routes (closes #3094) (#3097) 2024-10-10 15:20:04 -04:00
Greg Johnston
4becfa39ca correct version number 2024-10-10 09:13:39 -04:00
Greg Johnston
f8388b122d fix: avoid reentering lock when initializing nested keyed store fields (closes #3086) (#3087) 2024-10-10 08:53:28 -04:00
Greg Johnston
f57a57b92b feat: restore AnimatedShow for 0.7 (#3084) 2024-10-10 08:53:05 -04:00
vsuryamurthy
f0bcbd9cfe remove unused dependencies leptos_axum and leptos_router (#2960)
* remove unused dependencies leptos_axum and leptos_router

* cargo fmt

* Restore http::Uri under default feature

* use axum re-exported headers instead of http directly

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2024-10-10 04:29:11 -07:00
Greg Johnston
115477ef1d chore: remove unused code from leptos package (#3085) 2024-10-10 04:23:37 -07:00
Greg Johnston
832b9cb321 chore: pin wasm-bindgen to 0.2.93 to fix example builds (#3088) 2024-10-09 22:56:05 -04:00
44 changed files with 701 additions and 737 deletions

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.0-gamma"
version = "0.7.0-gamma3"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-gamma" }
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma" }
leptos = { path = "./leptos", version = "0.7.0-gamma" }
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma" }
leptos_router = { path = "./router", version = "0.7.0-gamma" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma" }
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma" }
leptos_meta = { path = "./meta", version = "0.7.0-gamma" }
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma" }
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma3" }
leptos = { path = "./leptos", version = "0.7.0-gamma3" }
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma3" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma3" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma3" }
leptos_router = { path = "./router", version = "0.7.0-gamma3" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma3" }
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma3" }
leptos_meta = { path = "./meta", version = "0.7.0-gamma3" }
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma3" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma" }
server_fn = { path = "./server_fn", version = "0.7.0-gamma" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma" }
tachys = { path = "./tachys", version = "0.1.0-gamma" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma3" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma3" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma3" }
server_fn = { path = "./server_fn", version = "0.7.0-gamma3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma3" }
tachys = { path = "./tachys", version = "0.1.0-gamma3" }
[profile.release]
codegen-units = 1

View File

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

View File

@@ -9,6 +9,7 @@ description = "Spawn asynchronous tasks in an executor-independent way."
edition.workspace = true
[dependencies]
async-executor = { version = "1.13.1", optional = true }
futures = "0.3.30"
glib = { version = "0.20.0", optional = true }
thiserror = "1.0"
@@ -19,12 +20,14 @@ tracing = { version = "0.1.40", optional = true }
wasm-bindgen-futures = { version = "0.4.42", optional = true }
[features]
async-executor = ["dep:async-executor"]
tracing = ["dep:tracing"]
tokio = ["dep:tokio"]
glib = ["dep:glib"]
wasm-bindgen = ["dep:wasm-bindgen-futures"]
futures-executor = ["futures/thread-pool", "futures/executor"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View File

@@ -32,11 +32,14 @@
use std::{future::Future, pin::Pin, sync::OnceLock};
use thiserror::Error;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub(crate) type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
/// A future that has been pinned.
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
/// A future that has been pinned.
pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
static SPAWN: OnceLock<fn(PinnedFuture<()>)> = OnceLock::new();
static SPAWN_LOCAL: OnceLock<fn(PinnedLocalFuture<()>)> = OnceLock::new();
static POLL_LOCAL: OnceLock<fn()> = OnceLock::new();
/// Errors that can occur when using the executor.
#[derive(Error, Debug)]
@@ -115,6 +118,14 @@ impl Executor {
});
_ = rx.await;
}
/// Polls the current async executor.
/// Not all async executors support polling, so this function may not do anything.
pub fn poll_local() {
if let Some(poller) = POLL_LOCAL.get() {
poller()
}
}
}
impl Executor {
@@ -193,13 +204,15 @@ impl Executor {
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
pub fn init_futures_executor() -> Result<(), ExecutorError> {
use futures::{
executor::{LocalPool, ThreadPool},
executor::{LocalPool, LocalSpawner, ThreadPool},
task::{LocalSpawnExt, SpawnExt},
};
use std::cell::RefCell;
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
thread_local! {
static LOCAL_POOL: LocalPool = LocalPool::new();
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
}
fn get_thread_pool() -> &'static ThreadPool {
@@ -218,28 +231,97 @@ impl Executor {
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
LOCAL_POOL.with(|pool| {
let spawner = pool.spawner();
SPAWNER.with(|spawner| {
spawner.spawn_local(fut).expect("failed to spawn future");
});
})
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| {
LOCAL_POOL.with(|pool| {
if let Ok(mut pool) = pool.try_borrow_mut() {
pool.run_until_stalled();
}
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
});
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "futures-executor")]
#[test]
fn can_spawn_local_future() {
use crate::Executor;
use std::rc::Rc;
Executor::init_futures_executor().expect("couldn't set executor");
let rc = Rc::new(());
Executor::spawn_local(async {
_ = rc;
});
Executor::spawn(async {});
/// Globally sets the [`async_executor`] executor as the executor used to spawn tasks,
/// lazily creating a thread pool to spawn tasks into.
///
/// Returns `Err(_)` if an executor has already been set.
///
/// Requires the `async-executor` feature to be activated on this crate.
#[cfg(feature = "async-executor")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-executor")))]
pub fn init_async_executor() -> Result<(), ExecutorError> {
use async_executor::{Executor, LocalExecutor};
static THREAD_POOL: OnceLock<Executor> = OnceLock::new();
thread_local! {
static LOCAL_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
}
fn get_thread_pool() -> &'static Executor<'static> {
THREAD_POOL.get_or_init(Executor::new)
}
SPAWN
.set(|fut| {
get_thread_pool().spawn(fut).detach();
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
LOCAL_POOL.with(|pool| pool.spawn(fut).detach());
})
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| {
LOCAL_POOL.with(|pool| pool.try_tick());
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
/// Globally sets a custom executor as the executor used to spawn tasks.
///
/// Returns `Err(_)` if an executor has already been set.
pub fn init_custom_executor(
custom_executor: impl CustomExecutor + 'static,
) -> Result<(), ExecutorError> {
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
EXECUTOR
.set(Box::new(custom_executor))
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN
.set(|fut| {
EXECUTOR.get().unwrap().spawn(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| EXECUTOR.get().unwrap().spawn_local(fut))
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| EXECUTOR.get().unwrap().poll_local())
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
}
/// A trait for custom executors.
/// Custom executors can be used to integrate with any executor that supports spawning futures.
///
/// All methods can be called recursively.
pub trait CustomExecutor: Send + Sync {
/// Spawns a future, usually on a thread pool.
fn spawn(&self, fut: PinnedFuture<()>);
/// Spawns a local future. May require calling `poll_local` to make progress.
fn spawn_local(&self, fut: PinnedLocalFuture<()>);
/// Polls the executor, if it supports polling.
fn poll_local(&self);
}

View File

@@ -0,0 +1,55 @@
#[cfg(feature = "futures-executor")]
use any_spawner::{CustomExecutor, Executor, PinnedFuture, PinnedLocalFuture};
#[cfg(feature = "futures-executor")]
#[test]
fn can_create_custom_executor() {
use futures::{
executor::{LocalPool, LocalSpawner},
task::LocalSpawnExt,
};
use std::{
cell::RefCell,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
thread_local! {
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
}
struct CustomFutureExecutor;
impl CustomExecutor for CustomFutureExecutor {
fn spawn(&self, _fut: PinnedFuture<()>) {
panic!("not supported in this test");
}
fn spawn_local(&self, fut: PinnedLocalFuture<()>) {
SPAWNER.with(|spawner| {
spawner.spawn_local(fut).expect("failed to spawn future");
});
}
fn poll_local(&self) {
LOCAL_POOL.with(|pool| {
if let Ok(mut pool) = pool.try_borrow_mut() {
pool.run_until_stalled();
}
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
});
}
}
Executor::init_custom_executor(CustomFutureExecutor)
.expect("couldn't set executor");
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
Executor::spawn_local(async move {
counter_clone.store(1, Ordering::Release);
});
Executor::poll_local();
assert_eq!(counter.load(Ordering::Acquire), 1);
}

View File

@@ -0,0 +1,38 @@
#[cfg(feature = "futures-executor")]
use any_spawner::Executor;
// All tests in this file use the same executor.
#[cfg(feature = "futures-executor")]
#[test]
fn can_spawn_local_future() {
use std::rc::Rc;
let _ = Executor::init_futures_executor();
let rc = Rc::new(());
Executor::spawn_local(async {
_ = rc;
});
Executor::spawn(async {});
}
#[cfg(feature = "futures-executor")]
#[test]
fn can_make_local_progress() {
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
let _ = Executor::init_futures_executor();
let counter = Arc::new(AtomicUsize::new(0));
Executor::spawn_local({
let counter = Arc::clone(&counter);
async move {
assert_eq!(counter.fetch_add(1, Ordering::AcqRel), 0);
Executor::spawn_local(async {
// Should not crash
});
}
});
Executor::poll_local();
assert_eq!(counter.load(Ordering::Acquire), 1);
}

View File

@@ -5,13 +5,14 @@ use leptos::prelude::*;
use leptos_router::{
components::{
Form, Outlet, ParentRoute, ProtectedRoute, Redirect, Route, Router,
Routes, A,
Routes, RoutingProgress, A,
},
hooks::{use_navigate, use_params, use_query_map},
params::Params,
MatchNestedRoutes,
};
use leptos_router_macro::path;
use std::time::Duration;
use tracing::info;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -26,9 +27,14 @@ pub fn RouterExample() -> impl IntoView {
// this signal will be ued to set whether we are allowed to access a protected route
let (logged_in, set_logged_in) = signal(true);
let (is_routing, set_is_routing) = signal(false);
view! {
<Router>
<Router set_is_routing>
// shows a progress bar while async data are loading
<div class="routing-progress">
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
</div>
<nav>
// ordinary <a> elements can be used for client-side navigation
// using <A> has two effects:

View File

@@ -3,7 +3,6 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use chrono::{Local, NaiveDate};
use leptos::prelude::*;
use reactive_stores::{Field, Patch, Store};
use reactive_stores_macro::{Patch, Store};
use serde::{Deserialize, Serialize};
// ID starts higher than 0 because we have a few starting todos by default

View File

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

View File

@@ -16,8 +16,6 @@ axum = { version = "0.7.5", default-features = false, features = [
] }
dashmap = "6"
futures = "0.3.30"
http = "1.1"
http-body-util = "0.1.2"
leptos = { workspace = true, features = ["nonce", "ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
@@ -26,7 +24,6 @@ leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
serde_json = "1.0"
tokio = { version = "1.39", default-features = false }
tower = { version = "0.4.13", features = ["util"] }
tower-http = "0.5.2"

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

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

@@ -1,59 +0,0 @@
#![allow(deprecated)]
use crate::TextProp;
use std::rc::Rc;
/// A collection of additional HTML attributes to be applied to an element,
/// each of which may or may not be reactive.
#[derive(Clone)]
#[repr(transparent)]
#[deprecated = "Most uses of `AdditionalAttributes` can be replaced with `#[prop(attrs)]` \
and the `attr:` syntax. If you have a use case that still requires `AdditionalAttributes`, please \
open a GitHub issue here and share it: https://github.com/leptos-rs/leptos"]
pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>);
impl<I, T, U> From<I> for AdditionalAttributes
where
I: IntoIterator<Item = (T, U)>,
T: Into<String>,
U: Into<TextProp>,
{
fn from(value: I) -> Self {
Self(
value
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}
impl Default for AdditionalAttributes {
fn default() -> Self {
Self([].into_iter().collect())
}
}
/// Iterator over additional HTML attributes.
#[repr(transparent)]
pub struct AdditionalAttributesIter<'a>(
std::slice::Iter<'a, (String, TextProp)>,
);
impl<'a> Iterator for AdditionalAttributesIter<'a> {
type Item = &'a (String, TextProp);
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl<'a> IntoIterator for &'a AdditionalAttributes {
type Item = &'a (String, TextProp);
type IntoIter = AdditionalAttributesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
AdditionalAttributesIter(self.0.iter())
}
}

View File

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

View File

@@ -174,7 +174,9 @@ pub mod prelude {
pub use server_fn::{self, ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::template::ViewTemplate,
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
};
}
pub use export_types::*;
@@ -202,8 +204,9 @@ pub mod error {
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
pub mod control_flow {
pub use crate::{await_::*, for_loop::*, show::*};
pub use crate::{animated_show::*, await_::*, for_loop::*, show::*};
}
mod animated_show;
mod await_;
mod for_loop;
mod show;
@@ -326,233 +329,3 @@ pub use tracing;
pub use wasm_bindgen;
#[doc(hidden)]
pub use web_sys;
/*mod additional_attributes;
pub use additional_attributes::*;
pub use await_::*;
pub use leptos_config::{self, get_configuration, LeptosOptions};
#[cfg(not(all(
target_arch = "wasm32",
any(feature = "csr", feature = "hydrate")
)))]
/// Utilities for server-side rendering HTML.
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
pub use leptos_dom::{
self, create_node_ref, document, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener, window_event_listener_untyped,
},
html,
html::Binding,
math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class,
CollectView, Errors, EventHandlerFn, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
};
/// Types to make it easier to handle errors in your application.
pub mod error {
pub use server_fn::error::{Error, Result};
}
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
pub use leptos_macro::template;
#[cfg(not(all(target_arch = "wasm32", feature = "template_macro")))]
pub use leptos_macro::view as template;
pub use leptos_macro::{component, island, slice, slot, view, Params};
cfg_if::cfg_if!(
if #[cfg(feature="spin")] {
pub use leptos_spin_macro::server;
} else {
pub use leptos_macro::server;
}
);
pub use leptos_reactive::*;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,
create_server_multi_action, Action, MultiAction, ServerFnError,
ServerFnErrorErr,
};
pub use server_fn::{self, ServerFn as _};
mod error_boundary;
pub use error_boundary::*;
mod animated_show;
mod for_loop;
mod provider;
mod show;
pub use animated_show::*;
pub use for_loop::*;
pub use provider::*;
#[cfg(feature = "experimental-islands")]
pub use serde;
#[cfg(feature = "experimental-islands")]
pub use serde_json;
pub use show::*;
//pub use suspense_component::*;
mod suspense_component;
//mod transition;
#[cfg(feature = "tracing")]
#[doc(hidden)]
pub use tracing;
pub use transition::*;
#[doc(hidden)]
pub use typed_builder;
#[doc(hidden)]
pub use typed_builder::Optional;
#[doc(hidden)]
pub use typed_builder_macro;
#[doc(hidden)]
#[cfg(any(
feature = "csr",
feature = "hydrate",
feature = "template_macro"
))]
pub use wasm_bindgen; // used in islands
#[doc(hidden)]
#[cfg(any(
feature = "csr",
feature = "hydrate",
feature = "template_macro"
))]
pub use web_sys; // used in islands
mod children;
mod portal;
mod view_fn;
pub use children::*;
pub use portal::*;
pub use view_fn::*;
extern crate self as leptos;
/// A type for taking anything that implements [`IntoAttribute`].
///
/// ```rust
/// use leptos::*;
///
/// #[component]
/// pub fn MyHeading(
/// text: String,
/// #[prop(optional, into)] class: Option<AttributeValue>,
/// ) -> impl IntoView {
/// view! {
/// <h1 class=class>{text}</h1>
/// }
/// }
/// ```
pub type AttributeValue = Box<dyn IntoAttribute>;
#[doc(hidden)]
pub trait Component<P> {}
#[doc(hidden)]
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}
#[doc(hidden)]
pub trait DynAttrs {
fn dyn_attrs(self, _args: Vec<(&'static str, Attribute)>) -> Self
where
Self: Sized,
{
self
}
}
impl DynAttrs for () {}
#[doc(hidden)]
pub trait DynBindings {
fn dyn_bindings<B: Into<Binding>>(
self,
_args: impl IntoIterator<Item = B>,
) -> Self
where
Self: Sized,
{
self
}
}
impl DynBindings for () {}
#[doc(hidden)]
pub trait PropsOrNoPropsBuilder {
type Builder;
fn builder_or_not() -> Self::Builder;
}
#[doc(hidden)]
#[derive(Copy, Clone, Debug, Default)]
pub struct EmptyPropsBuilder {}
impl EmptyPropsBuilder {
pub fn build(self) {}
}
impl<P: Props> PropsOrNoPropsBuilder for P {
type Builder = <P as Props>::Builder;
fn builder_or_not() -> Self::Builder {
Self::builder()
}
}
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
type Builder = EmptyPropsBuilder;
fn builder_or_not() -> Self::Builder {
EmptyPropsBuilder {}
}
}
impl<F, R> Component<EmptyPropsBuilder> for F where F: FnOnce() -> R {}
impl<P, F, R> Component<P> for F
where
F: FnOnce(P) -> R,
P: Props,
{
}
#[doc(hidden)]
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
_f: &impl Component<P>,
) -> <P as PropsOrNoPropsBuilder>::Builder {
<P as PropsOrNoPropsBuilder>::builder_or_not()
}
#[doc(hidden)]
pub fn component_view<P>(f: impl ComponentConstructor<P>, props: P) -> View {
f.construct(props)
}
#[doc(hidden)]
pub trait ComponentConstructor<P> {
fn construct(self, props: P) -> View;
}
impl<Func, V> ComponentConstructor<()> for Func
where
Func: FnOnce() -> V,
V: IntoView,
{
fn construct(self, (): ()) -> View {
(self)().into_view()
}
}
impl<Func, V, P> ComponentConstructor<P> for Func
where
Func: FnOnce(P) -> V,
V: IntoView,
P: PropsOrNoPropsBuilder,
{
fn construct(self, props: P) -> View {
(self)(props).into_view()
}
}*/

View File

@@ -1,30 +0,0 @@
use leptos_dom::{IntoView, View};
use std::rc::Rc;
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
pub struct ViewFn(Rc<dyn Fn() -> View>);
impl Default for ViewFn {
fn default() -> Self {
Self(Rc::new(|| ().into_view()))
}
}
impl<F, IV> From<F> for ViewFn
where
F: Fn() -> IV + 'static,
IV: IntoView,
{
fn from(value: F) -> Self {
Self(Rc::new(move || value().into_view()))
}
}
impl ViewFn {
/// Execute the wrapped function
pub fn run(&self) -> View {
(self.0)()
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.0-gamma"
version = "0.7.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -52,10 +52,22 @@ axum = ["server_fn_macro/axum"]
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing", "trace-component-props"]
skip_feature_sets = [
["csr", "hydrate"],
["hydrate", "csr"],
["hydrate", "ssr"],
["actix", "axum"]
[
"csr",
"hydrate",
],
[
"hydrate",
"csr",
],
[
"hydrate",
"ssr",
],
[
"actix",
"axum",
],
]
[package.metadata.docs.rs]

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

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, pin::Pin};
use std::future::Future;
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
@@ -219,16 +219,11 @@ mod view_implementations {
{
type Output<SomeNewAttr: Attribute> = Box<
dyn FnMut() -> Suspend<
Pin<
Box<
dyn Future<
Output = <T as AddAnyAttr>::Output<
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
>,
> + Send,
>,
>,
> + Send,
<T as AddAnyAttr>::Output<
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
>,
>
+ Send
>;
fn add_any_attr<NewAttr: Attribute>(

View File

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

View File

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

View File

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

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

View File

@@ -623,3 +623,46 @@ where
Display::fmt(&**self, f)
}
}
/// A wrapper that implements [`Deref`] and [`Borrow`] for itself.
pub struct Derefable<T>(pub T);
impl<T> Clone for Derefable<T>
where
T: Clone,
{
fn clone(&self) -> Self {
Derefable(self.0.clone())
}
}
impl<T> std::ops::Deref for Derefable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> Borrow<T> for Derefable<T> {
fn borrow(&self) -> &T {
self.deref()
}
}
impl<T> PartialEq<T> for Derefable<T>
where
T: PartialEq,
{
fn eq(&self, other: &T) -> bool {
self.deref() == other
}
}
impl<T> Display for Derefable<T>
where
T: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&**self, f)
}
}

View File

@@ -168,11 +168,20 @@ pub trait ReadUntracked: Sized + DefinedAt {
self.try_read_untracked()
.unwrap_or_else(unwrap_signal!(self))
}
/// This is a backdoor to allow overriding the [`Read::try_read`] implementation despite it being auto implemented.
///
/// If your type contains a [`Signal`](crate::wrappers::read::Signal),
/// call it's [`ReadUntracked::custom_try_read`] here, else return `None`.
#[track_caller]
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
None
}
}
/// Give read-only access to a signal's value by reference through a guard type,
/// and subscribes the active reactive observer (an effect or computed) to changes in its value.
pub trait Read {
pub trait Read: DefinedAt {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
@@ -185,7 +194,9 @@ pub trait Read {
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn read(&self) -> Self::Value;
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> Read for T
@@ -195,13 +206,18 @@ where
type Value = T::Value;
fn try_read(&self) -> Option<Self::Value> {
self.track();
self.try_read_untracked()
}
fn read(&self) -> Self::Value {
self.track();
self.read_untracked()
// The [`Read`] trait is auto implemented for types that implement [`ReadUntracked`] + [`Track`]. The [`Read`] trait then auto implements the [`With`] and [`Get`] traits too.
//
// This is a problem for e.g. the [`Signal`](crate::wrappers::read::Signal) type,
// this type must use a custom [`Read::try_read`] implementation to avoid an unnecessary clone.
//
// This is a backdoor to allow overriding the [`Read::try_read`] implementation despite it being auto implemented.
if let Some(custom) = self.custom_try_read() {
custom
} else {
self.track();
self.try_read_untracked()
}
}
}
@@ -307,14 +323,13 @@ pub trait With: DefinedAt {
impl<T> With for T
where
T: WithUntracked + Track,
T: Read,
{
type Value = <T as WithUntracked>::Value;
type Value = <<T as Read>::Value as Deref>::Target;
#[track_caller]
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
self.track();
self.try_with_untracked(fun)
self.try_read().map(|val| fun(&val))
}
}

View File

@@ -15,7 +15,6 @@ pub mod read {
},
traits::{
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
With, WithValue,
},
unwrap_signal,
};
@@ -201,29 +200,6 @@ pub mod read {
}
}
impl<T, S> ArcSignal<T, S>
where
S: Storage<T>,
{
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
pub fn track(&self) {
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
}
}
}
impl<T> Default for ArcSignal<T, SyncStorage>
where
T: Default + Send + Sync + 'static,
@@ -285,22 +261,23 @@ pub mod read {
}
}
impl<T, S> With for ArcSignal<T, S>
impl<T, S> Track for ArcSignal<T, S>
where
S: Storage<T>,
T: Clone,
{
type Value = T;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn track(&self) {
match &self.inner {
SignalTypes::ReadSignal(i) => i.try_with(fun),
SignalTypes::Memo(i) => i.try_with(fun),
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
SignalTypes::Stored(i) => i.try_with_value(fun),
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
}
}
}
@@ -328,32 +305,27 @@ pub mod read {
}
.map(ReadGuard::new)
}
}
impl<T, S> Read for ArcSignal<T, S>
where
S: Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read(&self) -> Option<Self::Value> {
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.try_read().map(SignalReadGuard::Read)
/// Overriding the default auto implemented [`Read::try_read`] to combine read and track,
/// to avoid 2 clones and just have 1 in the [`SignalTypes::DerivedSignal`].
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
Some(
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.try_read().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(i()))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
SignalTypes::Memo(i) => i.try_read().map(SignalReadGuard::Memo),
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(i()))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
.map(ReadGuard::new),
)
}
}
@@ -432,27 +404,31 @@ pub mod read {
}
}
impl<T, S> With for Signal<T, S>
impl<T, S> Track for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
S: Storage<T> + Storage<SignalTypes<T, S>>,
{
type Value = T;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
self.inner
fn track(&self) {
let inner = self
.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| match &inner {
SignalTypes::ReadSignal(i) => i.try_with(fun),
SignalTypes::Memo(i) => i.try_with(fun),
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
SignalTypes::Stored(i) => i.try_with_value(fun),
})
.unwrap_or_else(unwrap_signal!(self));
match inner {
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
}
}
}
@@ -486,41 +462,33 @@ pub mod read {
.map(ReadGuard::new)
})
}
}
impl<T, S> Read for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read(&self) -> Option<Self::Value> {
self.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| {
match &inner {
SignalTypes::ReadSignal(i) => {
i.try_read().map(SignalReadGuard::Read)
/// Overriding the default auto implemented [`Read::try_read`] to combine read and track,
/// to avoid 2 clones and just have 1 in the [`SignalTypes::DerivedSignal`].
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
Some(
self.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| {
match &inner {
SignalTypes::ReadSignal(i) => {
i.try_read().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(i()))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
SignalTypes::Memo(i) => {
i.try_read().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(i()))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
})
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
.map(ReadGuard::new)
}),
)
}
}
@@ -620,36 +588,6 @@ pub mod read {
}
}
impl<T, S> Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
pub fn track(&self) {
let inner = self
.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.unwrap_or_else(unwrap_signal!(self));
match inner {
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
}
}
}
impl<T> Default for Signal<T>
where
T: Send + Sync + Default + 'static,
@@ -830,6 +768,20 @@ pub mod read {
}
}
impl From<&str> for Signal<String> {
#[track_caller]
fn from(value: &str) -> Self {
Signal::stored(value.to_string())
}
}
impl From<&str> for Signal<String, LocalStorage> {
#[track_caller]
fn from(value: &str) -> Self {
Signal::stored_local(value.to_string())
}
}
/// A wrapper for a value that is *either* `T` or [`Signal<T>`].
///
/// This allows you to create APIs that take either a reactive or a non-reactive value
@@ -903,20 +855,14 @@ pub mod read {
}
}
impl<T, S> With for MaybeSignal<T, S>
impl<T, S> Track for MaybeSignal<T, S>
where
T: Send + Sync + 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
S: Storage<T> + Storage<SignalTypes<T, S>>,
{
type Value = T;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn track(&self) {
match self {
Self::Static(t) => Some(fun(t)),
Self::Dynamic(s) => s.try_with(fun),
Self::Static(_) => {}
Self::Dynamic(signal) => signal.track(),
}
}
}
@@ -936,27 +882,13 @@ pub mod read {
Self::Dynamic(s) => s.try_read_untracked(),
}
}
}
impl<T, S> Read for MaybeSignal<T, S>
where
T: Clone,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read(&self) -> Option<Self::Value> {
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
match self {
Self::Static(t) => {
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
}
Self::Dynamic(s) => s.try_read(),
Self::Static(_) => None,
Self::Dynamic(s) => s.custom_try_read(),
}
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> MaybeSignal<T>
@@ -980,21 +912,6 @@ pub mod read {
}
}
impl<T, S> MaybeSignal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
pub fn track(&self) {
match self {
Self::Static(_) => {}
Self::Dynamic(signal) => signal.track(),
}
}
}
impl<T> From<T> for MaybeSignal<T, SyncStorage>
where
SyncStorage: Storage<T>,
@@ -1192,20 +1109,14 @@ pub mod read {
}
}
impl<T, S> With for MaybeProp<T, S>
impl<T, S> Track for MaybeProp<T, S>
where
T: Send + Sync + 'static,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
{
type Value = Option<T>;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn track(&self) {
match &self.0 {
None => Some(fun(&None)),
Some(inner) => inner.try_with(fun),
None => {}
Some(signal) => signal.track(),
}
}
}
@@ -1223,25 +1134,13 @@ pub mod read {
Some(inner) => inner.try_read_untracked(),
}
}
}
impl<T, S> Read for MaybeProp<T, S>
where
T: Clone,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
fn try_read(&self) -> Option<Self::Value> {
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
match &self.0 {
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
Some(inner) => inner.try_read(),
None => None,
Some(inner) => inner.custom_try_read(),
}
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> MaybeProp<T>
@@ -1257,21 +1156,6 @@ pub mod read {
}
}
impl<T, S> MaybeProp<T, S>
where
T: 'static,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
pub fn track(&self) {
match &self.0 {
None => {}
Some(signal) => signal.track(),
}
}
}
impl<T> From<T> for MaybeProp<T>
where
SyncStorage: Storage<Option<T>>,

View File

@@ -1,6 +1,11 @@
[package]
name = "reactive_stores"
version = "0.1.0-gamma"
version = "0.1.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Stores for holding deeply-nested reactive state while maintaining fine-grained reactive tracking."
rust-version.workspace = true
edition.workspace = true
@@ -11,10 +16,10 @@ or_poisoned = { workspace = true }
paste = "1.0"
reactive_graph = { workspace = true }
rustc-hash = "2.0"
reactive_stores_macro = { workspace = true }
[dev-dependencies]
tokio = { version = "1.39", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4.4" }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
reactive_stores_macro = { workspace = true }
reactive_graph = { workspace = true, features = ["effects"] }

15
reactive_stores/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Stores
Stores are a data structure for nested reactivity.
The [`reactive_graph`](https://crates.io/crates/reactive_graph) crate provides primitives for fine-grained reactivity
via signals, memos, and effects.
This crate extends that reactivity to support reactive access to nested dested, without the need to create nested signals.
Using the `#[derive(Store)]` macro on a struct creates a series of getters that allow accessing each field. Individual fields
can then be read as if they were signals. Changes to parents will notify their children, but changing one sibling field will
not notify any of the others, nor will it require diffing those sibling fields (unlike earlier solutions using memoized “slices”).
This is published for use with the Leptos framework but can be used in any scenario where `reactive_graph` is being used
for reactivity.

View File

@@ -10,6 +10,7 @@ use reactive_graph::{
Write,
},
};
pub use reactive_stores_macro::*;
use rustc_hash::FxHashMap;
use std::{
any::Any,
@@ -444,7 +445,6 @@ mod tests {
effect::Effect,
traits::{Read, ReadUntracked, Set, Update, Write},
};
use reactive_stores_macro::{Patch, Store};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,

View File

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

View File

@@ -1,6 +1,11 @@
[package]
name = "reactive_stores_macro"
version = "0.1.0-gamma"
version = "0.1.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Stores for holding deeply-nested reactive state while maintaining fine-grained reactive tracking."
rust-version.workspace = true
edition.workspace = true

View File

@@ -0,0 +1 @@
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-gamma"
version = "0.7.0-gamma3"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -22,13 +22,11 @@ url = "2.5"
js-sys = { version = "0.3.69" }
wasm-bindgen = { version = "0.2.93" }
tracing = { version = "0.1.40", optional = true }
paste = "1.0"
once_cell = "1.19"
send_wrapper = "0.6.0"
thiserror = "1.0"
percent-encoding = { version = "2.3", optional = true }
gloo-net = "0.6.0"
serde = { version = "1", features = ["derive"] }
[dependencies.web-sys]
version = "0.3.70"

View File

@@ -248,7 +248,10 @@ where
base.upgrade_inplace();
base
});
let routes = Routes::new(children.into_inner());
let routes = Routes::new_with_base(
children.into_inner(),
base.clone().unwrap_or_default(),
);
let outer_owner =
Owner::current().expect("creating Routes, but no Owner was found");
move || {
@@ -294,7 +297,10 @@ where
base.upgrade_inplace();
base
});
let routes = Routes::new(children.into_inner());
let routes = Routes::new_with_base(
children.into_inner(),
base.clone().unwrap_or_default(),
);
let outer_owner =
Owner::current().expect("creating Router, but no Owner was found");

View File

@@ -8,7 +8,7 @@ use crate::{
};
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
use futures::{future::join_all, FutureExt};
use futures::{channel::oneshot, future::join_all, FutureExt};
use leptos::{component, oco::Oco};
use or_poisoned::OrPoisoned;
use reactive_graph::{
@@ -16,6 +16,7 @@ use reactive_graph::{
owner::{provide_context, use_context, Owner},
signal::{ArcRwSignal, ArcTrigger},
traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track},
transition::AsyncTransition,
wrappers::write::SignalSetter,
};
use send_wrapper::SendWrapper;
@@ -47,7 +48,6 @@ pub(crate) struct NestedRoutesView<Loc, Defs, FalFn> {
pub current_url: ArcRwSignal<Url>,
pub base: Option<Oco<'static, str>>,
pub fallback: FalFn,
#[allow(unused)] // TODO
pub set_is_routing: Option<SignalSetter<bool>>,
}
@@ -155,23 +155,37 @@ where
state.outlets.clear();
}
Some(route) => {
let mut loaders = Vec::new();
if let Some(set_is_routing) = self.set_is_routing {
set_is_routing.set(true);
}
let mut preloaders = Vec::new();
let mut full_loaders = Vec::new();
route.rebuild_nested_route(
&self.current_url.read_untracked(),
self.base,
&mut 0,
&mut loaders,
&mut preloaders,
&mut full_loaders,
&mut state.outlets,
&self.outer_owner,
self.set_is_routing.is_some(),
);
let location = self.location.clone();
Executor::spawn_local(async move {
let triggers = join_all(loaders).await;
let triggers = join_all(preloaders).await;
// tell each one of the outlet triggers that it's ready
for trigger in triggers {
trigger.notify();
}
});
Executor::spawn_local(async move {
join_all(full_loaders).await;
if let Some(set_is_routing) = self.set_is_routing {
set_is_routing.set(false);
}
if let Some(loc) = location {
loc.ready_to_complete();
}
@@ -426,9 +440,7 @@ where
}
}
type OutletViewFn = Box<
dyn Fn() -> Suspend<Pin<Box<dyn Future<Output = AnyView> + Send>>> + Send,
>;
type OutletViewFn = Box<dyn Fn() -> Suspend<AnyView> + Send>;
pub(crate) struct RouteContext {
id: RouteMatchId,
@@ -486,14 +498,17 @@ trait AddNestedRoute {
parent: &Owner,
);
#[allow(clippy::too_many_arguments)]
fn rebuild_nested_route(
self,
url: &Url,
base: Option<Oco<'static, str>>,
items: &mut usize,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
);
}
@@ -629,14 +644,17 @@ where
}
}
#[allow(clippy::too_many_arguments)]
fn rebuild_nested_route(
self,
url: &Url,
base: Option<Oco<'static, str>>,
items: &mut usize,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
) {
let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets
.iter()
@@ -647,7 +665,7 @@ where
match current {
// if there's nothing currently in the routes at this point, build from here
None => {
self.build_nested_route(url, base, loaders, outlets, parent);
self.build_nested_route(url, base, preloaders, outlets, parent);
}
Some(current) => {
// a unique ID for each route, which allows us to compare when we get new matches
@@ -716,11 +734,14 @@ where
let old_owner =
mem::replace(&mut current.owner, parent.child());
let owner = current.owner.clone();
let (full_tx, full_rx) = oneshot::channel();
let full_tx = Mutex::new(Some(full_tx));
full_loaders.push(full_rx);
// send the new view, with the new owner, through the channel to the Outlet,
// and notify the trigger so that the reactive view inside the Outlet tracking
// the trigger runs again
loaders.push(Box::pin(owner.with(|| {
preloaders.push(Box::pin(owner.with(|| {
ScopedFuture::new({
let owner = owner.clone();
let trigger = current.trigger.clone();
@@ -736,15 +757,26 @@ where
Box::new(move || {
let owner = owner.clone();
let view = view.clone();
let full_tx =
full_tx.lock().or_poisoned().take();
Suspend::new(Box::pin(async move {
let view = SendWrapper::new(
owner.with(|| {
ScopedFuture::new(
view.choose(),
async move {
if set_is_routing {
AsyncTransition::run(|| view.choose()).await
} else {
view.choose().await
}
}
)
}),
);
let view = view.await;
if let Some(tx) = full_tx {
_ = tx.send(());
}
owner.with(|| {
OwnedView::new(view).into_any()
})
@@ -766,7 +798,7 @@ where
// if this children has matches, then rebuild the lower section of the tree
if let Some(child) = child {
child.build_nested_route(
url, base, loaders, outlets, &owner,
url, base, preloaders, outlets, &owner,
);
}
@@ -782,7 +814,14 @@ where
let owner = current.owner.clone();
*items += 1;
child.rebuild_nested_route(
url, base, items, loaders, outlets, &owner,
url,
base,
items,
preloaders,
full_loaders,
outlets,
&owner,
set_is_routing,
);
}
}

View File

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

View File

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

View File

@@ -180,6 +180,84 @@ pub trait IntoClass: Send {
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send;
}
impl<T: IntoClass> IntoClass for Option<T> {
type AsyncOutput = Option<T::AsyncOutput>;
type State = (crate::renderer::types::Element, Option<T::State>);
type Cloneable = Option<T::Cloneable>;
type CloneableOwned = Option<T::CloneableOwned>;
fn html_len(&self) -> usize {
self.as_ref().map_or(0, IntoClass::html_len)
}
fn to_html(self, class: &mut String) {
if let Some(t) = self {
t.to_html(class);
}
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
) -> Self::State {
if let Some(t) = self {
(el.clone(), Some(t.hydrate::<FROM_SERVER>(el)))
} else {
(el.clone(), None)
}
}
fn build(self, el: &crate::renderer::types::Element) -> Self::State {
if let Some(t) = self {
(el.clone(), Some(t.build(el)))
} else {
(el.clone(), None)
}
}
fn rebuild(self, state: &mut Self::State) {
let el = &state.0;
let prev_state = &mut state.1;
let maybe_next_t_state = match (prev_state, self) {
(Some(_prev_t_state), None) => {
Rndr::remove_attribute(el, "class");
Some(None)
}
(None, Some(t)) => Some(Some(t.build(el))),
(Some(prev_t_state), Some(t)) => {
t.rebuild(prev_t_state);
None
}
(None, None) => Some(None),
};
if let Some(next_t_state) = maybe_next_t_state {
state.1 = next_t_state;
}
}
fn into_cloneable(self) -> Self::Cloneable {
self.map(|t| t.into_cloneable())
}
fn into_cloneable_owned(self) -> Self::CloneableOwned {
self.map(|t| t.into_cloneable_owned())
}
fn dry_resolve(&mut self) {
if let Some(t) = self {
t.dry_resolve();
}
}
async fn resolve(self) -> Self::AsyncOutput {
if let Some(t) = self {
Some(t.resolve().await)
} else {
None
}
}
}
impl<'a> IntoClass for &'a str {
type AsyncOutput = Self;
type State = (crate::renderer::types::Element, Self);

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,10 +36,9 @@ use std::{
use throw_error::ErrorHook;
/// A suspended `Future`, which can be used in the view.
#[derive(Clone)]
pub struct Suspend<Fut> {
pub struct Suspend<T> {
pub(crate) subscriber: SuspendSubscriber,
pub(crate) inner: Pin<Box<ScopedFuture<Fut>>>,
pub(crate) inner: Pin<Box<dyn Future<Output = T> + Send>>,
}
#[derive(Debug, Clone)]
@@ -114,9 +113,9 @@ impl ToAnySubscriber for SuspendSubscriber {
}
}
impl<Fut> Suspend<Fut> {
impl<T> Suspend<T> {
/// Creates a new suspended view.
pub fn new(fut: Fut) -> Self {
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
let subscriber = SuspendSubscriber::new();
let any_subscriber = subscriber.to_any_subscriber();
let inner =
@@ -125,7 +124,7 @@ impl<Fut> Suspend<Fut> {
}
}
impl<Fut> Debug for Suspend<Fut> {
impl<T> Debug for Suspend<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Suspend").finish()
}
@@ -160,12 +159,11 @@ where
}
}
impl<Fut> Render for Suspend<Fut>
impl<T> Render for Suspend<T>
where
Fut: Future + 'static,
Fut::Output: Render,
T: Render + 'static,
{
type State = SuspendState<Fut::Output>;
type State = SuspendState<T>;
fn build(self) -> Self::State {
let Self { subscriber, inner } = self;
@@ -255,22 +253,12 @@ where
}
}
impl<Fut> AddAnyAttr for Suspend<Fut>
impl<T> AddAnyAttr for Suspend<T>
where
Fut: Future + Send + 'static,
Fut::Output: AddAnyAttr,
T: Send + AddAnyAttr + 'static,
{
type Output<SomeNewAttr: Attribute> = Suspend<
Pin<
Box<
dyn Future<
Output = <Fut::Output as AddAnyAttr>::Output<
SomeNewAttr::CloneableOwned,
>,
> + Send,
>,
>,
>;
type Output<SomeNewAttr: Attribute> =
Suspend<<T as AddAnyAttr>::Output<SomeNewAttr::CloneableOwned>>;
fn add_any_attr<NewAttr: Attribute>(
self,
@@ -280,21 +268,20 @@ where
Self::Output<NewAttr>: RenderHtml,
{
let attr = attr.into_cloneable_owned();
Suspend::new(Box::pin(async move {
Suspend::new(async move {
let this = self.inner.await;
this.add_any_attr(attr)
}))
})
}
}
impl<Fut> RenderHtml for Suspend<Fut>
impl<T> RenderHtml for Suspend<T>
where
Fut: Future + Send + 'static,
Fut::Output: RenderHtml,
T: RenderHtml + Sized + 'static,
{
type AsyncOutput = Option<Fut::Output>;
type AsyncOutput = Option<T>;
const MIN_LENGTH: usize = Fut::Output::MIN_LENGTH;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn to_html_with_buf(
self,
@@ -440,6 +427,23 @@ where
}
fn dry_resolve(&mut self) {
self.inner.as_mut().now_or_never();
// this is a little crazy, but if a Suspend is immediately available, we end up
// with a situation where polling it multiple times (here in dry_resolve and then in
// resolve) causes a runtime panic
// (see https://github.com/leptos-rs/leptos/issues/3113)
//
// at the same time, we do need to dry_resolve Suspend so that we can register synchronous
// resource reads that happen inside them
// (see https://github.com/leptos-rs/leptos/issues/2917)
//
// fuse()-ing the Future doesn't work, because that will cause the subsequent resolve()
// simply to be pending forever
//
// in this case, though, we can simply... discover that the data are already here, and then
// stuff them back into a new Future, which can safely be polled after its completion
if let Some(inner) = self.inner.as_mut().now_or_never() {
self.inner = Box::pin(async move { inner })
as Pin<Box<dyn Future<Output = T> + Send>>;
}
}
}

View File

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