Compare commits

..

5 Commits

Author SHA1 Message Date
Greg Johnston
ca903c1053 fix: missing Copy/Clone implementations for OnceResource 2024-10-09 13:43:12 -04:00
Greg Johnston
eef88ac180 chore: update tests 2024-10-06 19:42:42 -04:00
Greg Johnston
c594c0e523 chore: clippy 2024-10-06 19:23:03 -04:00
Greg Johnston
b8f553c3a1 remove local prop to simplify 2024-10-06 13:04:50 -04:00
Greg Johnston
55873e9a10 feat: add an optimized OnceResource and use it to rebuild Await 2024-10-05 12:51:06 -04:00
73 changed files with 1367 additions and 2123 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

@@ -41,10 +41,7 @@
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::WithValue,
};
use reactive_graph::owner::{LocalStorage, StoredValue};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.

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));
}
}
}
@@ -560,9 +548,7 @@ fn node_to_tokens(
view_marker,
disable_inert_html,
),
Node::Block(block) => {
Some(quote! { ::leptos::prelude::IntoRender::into_render(#block) })
}
Node::Block(block) => Some(quote! { #block }),
Node::Text(text) => Some(text_to_tokens(&text.value)),
Node::RawText(raw) => {
let text = raw.to_string_best();
@@ -571,11 +557,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,
@@ -743,11 +725,6 @@ pub(crate) fn element_to_tokens(
quote! { ::leptos::tachys::html::element::#custom(#name) }
} else if is_svg_element(&tag) {
parent_type = TagType::Svg;
let name = if tag == "use" || tag == "use_" {
Ident::new_raw("use", name.span()).to_token_stream()
} else {
name.to_token_stream()
};
quote! { ::leptos::tachys::svg::#name() }
} else if is_math_ml_element(&tag) {
parent_type = TagType::Math;
@@ -901,7 +878,7 @@ fn attribute_to_tokens(
NodeName::Path(path) => path.path.get_ident(),
_ => unreachable!(),
};
let value = attribute_value(node, false);
let value = attribute_value(node);
quote! {
.#node_ref(#value)
}
@@ -951,13 +928,13 @@ fn attribute_to_tokens(
// we don't provide statically-checked methods for SVG attributes
|| (tag_type == TagType::Svg && name != "inner_html")
{
let value = attribute_value(node, true);
let value = attribute_value(node);
quote! {
.attr(#name, #value)
}
} else {
let key = attribute_name(&node.key);
let value = attribute_value(node, true);
let value = attribute_value(node);
// special case of global_class and class attribute
if &node.key.to_string() == "class"
@@ -994,11 +971,11 @@ pub(crate) fn attribute_absolute(
let id = &parts[0];
match id {
NodeNameFragment::Ident(id) => {
let value = attribute_value(node);
// ignore `let:` and `clone:`
if id == "let" || id == "clone" {
None
} else if id == "attr" {
let value = attribute_value(node, true);
let key = &parts[1];
let key_name = key.to_string();
if key_name == "class" || key_name == "style" {
@@ -1006,7 +983,6 @@ pub(crate) fn attribute_absolute(
quote! { ::leptos::tachys::html::#key::#key(#value) },
)
} else if key_name == "aria" {
let value = attribute_value(node, true);
let mut parts_iter = parts.iter();
parts_iter.next();
let fn_name = parts_iter.map(|p| p.to_string()).collect::<Vec<String>>().join("_");
@@ -1035,7 +1011,6 @@ pub(crate) fn attribute_absolute(
},
)
} else if id == "style" || id == "class" {
let value = attribute_value(node, false);
let key = &node.key.to_string();
let key = key
.replacen("style:", "", 1)
@@ -1044,7 +1019,6 @@ pub(crate) fn attribute_absolute(
quote! { ::leptos::tachys::html::#id::#id((#key, #value)) },
)
} else if id == "prop" {
let value = attribute_value(node, false);
let key = &node.key.to_string();
let key = key.replacen("prop:", "", 1);
Some(
@@ -1101,7 +1075,7 @@ pub(crate) fn two_way_binding_to_tokens(
name: &str,
node: &KeyedAttribute,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
let ident =
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
@@ -1126,7 +1100,7 @@ pub(crate) fn event_type_and_handler(
name: &str,
node: &KeyedAttribute,
) -> (TokenStream, TokenStream, TokenStream) {
let handler = attribute_value(node, false);
let handler = attribute_value(node);
let (event_type, is_custom, is_force_undelegated, is_targeted) =
parse_event_name(name);
@@ -1183,7 +1157,7 @@ fn class_to_tokens(
class: TokenStream,
class_name: Option<&str>,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
if let Some(class_name) = class_name {
quote! {
.#class((#class_name, #value))
@@ -1200,7 +1174,7 @@ fn style_to_tokens(
style: TokenStream,
style_name: Option<&str>,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
if let Some(style_name) = style_name {
quote! {
.#style((#style_name, #value))
@@ -1217,7 +1191,7 @@ fn prop_to_tokens(
prop: TokenStream,
key: &str,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
quote! {
.#prop(#key, #value)
}
@@ -1374,10 +1348,7 @@ fn attribute_name(name: &NodeName) -> TokenStream {
}
}
fn attribute_value(
attr: &KeyedAttribute,
is_attribute_proper: bool,
) -> TokenStream {
fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
match attr.possible_value.to_value() {
None => quote! { true },
Some(value) => match &value.value {
@@ -1392,26 +1363,14 @@ fn attribute_value(
}
}
if matches!(expr, Expr::Lit(_)) || !is_attribute_proper {
quote! {
#expr
}
} else {
quote! {
::leptos::prelude::IntoAttributeValue::into_attribute_value(#expr)
}
quote! {
{#expr}
}
}
// any value in braces: expand as-is to give proper r-a support
KVAttributeValue::InvalidBraced(block) => {
if is_attribute_proper {
quote! {
::leptos::prelude::IntoAttributeValue::into_attribute_value(#block)
}
} else {
quote! {
#block
}
quote! {
#block
}
}
},

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

@@ -35,10 +35,10 @@ impl<T> Clone for ArcLocalResource<T> {
impl<T> ArcLocalResource<T> {
#[track_caller]
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
pub fn new<Fut>(fetcher: impl Fn() -> Fut + Send + Sync + 'static) -> Self
where
T: 'static,
Fut: Future<Output = T> + 'static,
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
let fetcher = move || {
let fut = fetcher();
@@ -60,7 +60,7 @@ impl<T> ArcLocalResource<T> {
}
};
Self {
data: ArcAsyncDerived::new_unsync(fetcher),
data: ArcAsyncDerived::new(fetcher),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
@@ -103,7 +103,7 @@ impl<T> DefinedAt for ArcLocalResource<T> {
impl<T> ReadUntracked for ArcLocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
@@ -201,7 +201,7 @@ impl<T> LocalResource<T> {
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
where
T: 'static,
Fut: Future<Output = T> + 'static,
Fut: Future<Output = T> + Send + 'static,
{
let fetcher = move || {
let fut = fetcher();
@@ -230,7 +230,7 @@ impl<T> LocalResource<T> {
let fetcher = SendWrapper::new(fetcher);
AsyncDerived::new(move || {
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
async move { SendWrapper::new(fut.await) }
})
},
#[cfg(debug_assertions)]
@@ -280,7 +280,7 @@ impl<T> DefinedAt for LocalResource<T> {
impl<T> ReadUntracked for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
type Value =
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
@@ -307,7 +307,7 @@ impl<T: 'static> IsDisposed for LocalResource<T> {
impl<T: 'static> ToAnySource for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn to_any_source(&self) -> AnySource {
self.data.to_any_source()
@@ -316,7 +316,7 @@ where
impl<T: 'static> ToAnySubscriber for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn to_any_subscriber(&self) -> AnySubscriber {
self.data.to_any_subscriber()
@@ -325,7 +325,7 @@ where
impl<T> Source for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn add_subscriber(&self, subscriber: AnySubscriber) {
self.data.add_subscriber(subscriber)
@@ -342,7 +342,7 @@ where
impl<T> ReactiveNode for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn mark_dirty(&self) {
self.data.mark_dirty();
@@ -363,7 +363,7 @@ where
impl<T> Subscriber for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn add_source(&self, source: AnySource) {
self.data.add_source(source);

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

@@ -15,7 +15,6 @@ use crate::{
};
pub use arc_memo::*;
pub use async_derived::*;
pub(crate) use inner::MemoInner;
pub use memo::*;
pub use selector::*;

View File

@@ -12,14 +12,12 @@ use std::{
sync::{Arc, RwLock, Weak},
};
mod arc_stored_value;
mod arena;
mod arena_item;
mod context;
mod storage;
mod stored_value;
use self::arena::Arena;
pub use arc_stored_value::ArcStoredValue;
#[cfg(feature = "sandboxed-arenas")]
pub use arena::sandboxed::Sandboxed;
#[cfg(feature = "sandboxed-arenas")]

View File

@@ -1,126 +0,0 @@
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, IsDisposed, ReadValue, WriteValue},
};
use std::{
fmt::{Debug, Formatter},
hash::Hash,
panic::Location,
sync::{Arc, RwLock},
};
/// A reference-counted getter for any value non-reactively.
///
/// This is a reference-counted value, which is `Clone` but not `Copy`.
/// For arena-allocated `Copy` values, use [`StoredValue`](super::StoredValue).
///
/// This allows you to create a stable reference for any value by storing it within
/// the reactive system. Unlike e.g. [`ArcRwSignal`](crate::signal::ArcRwSignal), it is not reactive;
/// accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
pub struct ArcStoredValue<T> {
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
value: Arc<RwLock<T>>,
}
impl<T> Clone for ArcStoredValue<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
}
}
}
impl<T> Debug for ArcStoredValue<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ArcStoredValue")
.field("type", &std::any::type_name::<T>())
.field("value", &Arc::as_ptr(&self.value))
.finish()
}
}
impl<T: Default> Default for ArcStoredValue<T> {
#[track_caller]
fn default() -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(T::default())),
}
}
}
impl<T> PartialEq for ArcStoredValue<T> {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.value, &other.value)
}
}
impl<T> Eq for ArcStoredValue<T> {}
impl<T> Hash for ArcStoredValue<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(&Arc::as_ptr(&self.value), state);
}
}
impl<T> DefinedAt for ArcStoredValue<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<T> ArcStoredValue<T> {
/// Creates a new stored value, taking the initial value as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
#[track_caller]
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(value)),
}
}
}
impl<T> ReadValue for ArcStoredValue<T>
where
T: 'static,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
}
}
impl<T> WriteValue for ArcStoredValue<T>
where
T: 'static,
{
type Value = T;
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
UntrackedWriteGuard::try_new(self.value.clone())
}
}
impl<T> IsDisposed for ArcStoredValue<T> {
fn is_disposed(&self) -> bool {
false
}
}

View File

@@ -1,16 +1,14 @@
use super::{
arc_stored_value::ArcStoredValue, ArenaItem, LocalStorage, Storage,
SyncStorage,
};
use super::{ArenaItem, LocalStorage, Storage, SyncStorage};
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, Dispose, IsDisposed, ReadValue, WriteValue},
traits::{DefinedAt, Dispose, IsDisposed},
unwrap_signal,
};
use or_poisoned::OrPoisoned;
use std::{
fmt::{Debug, Formatter},
hash::Hash,
panic::Location,
sync::{Arc, RwLock},
};
/// A **non-reactive**, `Copy` handle for any value.
@@ -20,8 +18,9 @@ use std::{
/// and [`RwSignal`](crate::signal::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
#[derive(Debug)]
pub struct StoredValue<T, S = SyncStorage> {
value: ArenaItem<ArcStoredValue<T>, S>,
value: ArenaItem<Arc<RwLock<T>>, S>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -34,18 +33,6 @@ impl<T, S> Clone for StoredValue<T, S> {
}
}
impl<T, S> Debug for StoredValue<T, S>
where
S: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StoredValue")
.field("type", &std::any::type_name::<T>())
.field("value", &self.value)
.finish()
}
}
impl<T, S> PartialEq for StoredValue<T, S> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
@@ -76,13 +63,13 @@ impl<T, S> DefinedAt for StoredValue<T, S> {
impl<T, S> StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
S: Storage<Arc<RwLock<T>>>,
{
/// Stores the given value in the arena allocator.
#[track_caller]
pub fn new_with_storage(value: T) -> Self {
Self {
value: ArenaItem::new_with_storage(ArcStoredValue::new(value)),
value: ArenaItem::new_with_storage(Arc::new(RwLock::new(value))),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
@@ -92,7 +79,7 @@ where
impl<T, S> Default for StoredValue<T, S>
where
T: Default + 'static,
S: Storage<ArcStoredValue<T>>,
S: Storage<Arc<RwLock<T>>>,
{
#[track_caller] // Default trait is not annotated with #[track_caller]
fn default() -> Self {
@@ -122,31 +109,268 @@ where
}
}
impl<T, S> ReadValue for StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
impl<T, S: Storage<Arc<RwLock<T>>>> StoredValue<T, S> {
/// Returns an [`Option`] of applying a function to the value within the [`StoredValue`].
///
/// If the owner of the reactive node has not been disposed [`Some`] is returned. Calling this
/// function after the owner has been disposed will always return [`None`].
///
/// See [`StoredValue::with_value`] for a version that panics in the case of the owner being
/// disposed.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data {
/// rows: vec![0, 1, 2, 3, 4],
/// });
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let length_fn = move || data.try_with_value(|inner| inner.rows.len());
///
/// let sum = data.try_with_value(|inner| inner.rows.iter().sum::<u8>());
///
/// assert_eq!(sum, Some(10));
/// assert_eq!(length_fn(), Some(5));
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// let last = data.try_with_value(|inner| inner.rows.last().cloned());
///
/// assert_eq!(last, None);
/// assert_eq!(length_fn(), None);
/// ```
#[track_caller]
pub fn try_with_value<U>(&self, fun: impl FnOnce(&T) -> U) -> Option<U> {
self.value
.try_get_value()
.and_then(|inner| inner.try_read_value())
.map(|inner| fun(&*inner.read().or_poisoned()))
}
}
impl<T, S> WriteValue for StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
{
type Value = T;
/// Returns the output of applying a function to the value within the [`StoredValue`].
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_with_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data {
/// rows: vec![1, 2, 3],
/// });
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let length_fn = move || data.with_value(|inner| inner.rows.len());
///
/// let sum = data.with_value(|inner| inner.rows.iter().sum::<u8>());
///
/// assert_eq!(sum, 6);
/// assert_eq!(length_fn(), 3);
/// ```
#[track_caller]
pub fn with_value<U>(&self, fun: impl FnOnce(&T) -> U) -> U {
self.try_with_value(fun)
.unwrap_or_else(unwrap_signal!(self))
}
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
/// Returns a read guard to the stored data, or `None` if the owner of the reactive node has been disposed.
#[track_caller]
pub fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
self.value
.try_get_value()
.and_then(|inner| inner.try_write_value())
.and_then(|inner| Plain::try_new(inner).map(ReadGuard::new))
}
/// Returns a read guard to the stored data.
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_read_value`] for a version without panic.
#[track_caller]
pub fn read_value(&self) -> ReadGuard<T, Plain<T>> {
self.try_read_value().unwrap_or_else(unwrap_signal!(self))
}
/// Returns a write guard to the stored data, or `None` if the owner of the reactive node has been disposed.
#[track_caller]
pub fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
self.value
.try_get_value()
.and_then(|inner| UntrackedWriteGuard::try_new(inner))
}
/// Returns a write guard to the stored data.
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_write_value`] for a version without panic.
#[track_caller]
pub fn write_value(&self) -> UntrackedWriteGuard<T> {
self.try_write_value().unwrap_or_else(unwrap_signal!(self))
}
/// Updates the current value by applying the given closure, returning the return value of the
/// closure, or `None` if the value has already been disposed.
pub fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.value
.try_get_value()
.map(|inner| fun(&mut *inner.write().or_poisoned()))
}
/// Updates the value within [`StoredValue`] by applying a function to it.
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_update_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// #[derive(Default)] // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data::default());
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let push_next = move || {
/// data.update_value(|inner| match inner.rows.last().as_deref() {
/// Some(n) => inner.rows.push(n + 1),
/// None => inner.rows.push(0),
/// })
/// };
///
/// data.update_value(|inner| inner.rows = vec![5, 6, 7]);
/// data.with_value(|inner| assert_eq!(inner.rows.last(), Some(&7)));
///
/// push_next();
/// data.with_value(|inner| assert_eq!(inner.rows.last(), Some(&8)));
///
/// data.update_value(|inner| {
/// std::mem::take(inner) // sets Data back to default
/// });
/// data.with_value(|inner| assert!(inner.rows.is_empty()));
/// ```
pub fn update_value<U>(&self, fun: impl FnOnce(&mut T) -> U) {
self.try_update_value(fun);
}
/// Sets the value within [`StoredValue`].
///
/// Returns [`Some`] containing the passed value if the owner of the reactive node has been
/// disposed.
///
/// For types that do not implement [`Clone`], or in cases where allocating the entire object
/// would be too expensive, prefer [`StoredValue::try_update_value`].
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// let data = StoredValue::new(String::default());
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let say_hello = move || {
/// // Note that using the `update` methods would be more efficient here.
/// data.try_set_value("Hello, World!".into())
/// };
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let reset = move || {
/// // Note that using the `update` methods would be more efficient here.
/// data.try_set_value(Default::default())
/// };
/// assert_eq!(data.get_value(), "");
///
/// // None is returned because the value was able to be updated
/// assert_eq!(say_hello(), None);
///
/// assert_eq!(data.get_value(), "Hello, World!");
///
/// reset();
/// assert_eq!(data.get_value(), "");
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// // The reactive owner is disposed, so the value we intended to set is now
/// // returned as some.
/// assert_eq!(say_hello().as_deref(), Some("Hello, World!"));
/// assert_eq!(reset().as_deref(), Some(""));
/// ```
pub fn try_set_value(&self, value: T) -> Option<T> {
match self.value.try_get_value() {
Some(inner) => {
*inner.write().or_poisoned() = value;
None
}
None => Some(value),
}
}
/// Sets the value within [`StoredValue`].
///
/// For types that do not implement [`Clone`], or in cases where allocating the entire object
/// would be too expensive, prefer [`StoredValue::update_value`].
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_set_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// let data = StoredValue::new(10);
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.get_value(), u8::MAX);
///
/// zero();
/// assert_eq!(data.get_value(), u8::MIN);
/// ```
pub fn set_value(&self, value: T) {
self.update_value(|n| *n = value);
}
}
@@ -156,39 +380,90 @@ impl<T, S> IsDisposed for StoredValue<T, S> {
}
}
impl<T, S: Storage<Arc<RwLock<T>>>> StoredValue<T, S>
where
T: Clone + 'static,
{
/// Returns the value within [`StoredValue`] by cloning.
///
/// Returns [`Some`] containing the value if the owner of the reactive node has not been
/// disposed. When disposed, returns [`None`].
///
/// See [`StoredValue::try_with_value`] for a version that avoids cloning. See
/// [`StoredValue::get_value`] for a version that clones, but panics if the node is disposed.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// // u8 is practically free to clone.
/// let data: StoredValue<u8> = StoredValue::new(10);
///
/// // Larger data structures can become very expensive to clone.
/// // You may prefer to use StoredValue::try_with_value.
/// let _expensive: StoredValue<Vec<String>> = StoredValue::new(vec![]);
///
/// // Easy to move into closures because StoredValue is Copy + 'static
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.try_get_value(), Some(u8::MAX));
///
/// zero();
/// assert_eq!(data.try_get_value(), Some(u8::MIN));
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// assert_eq!(data.try_get_value(), None);
/// ```
pub fn try_get_value(&self) -> Option<T> {
self.try_with_value(T::clone)
}
/// Returns the value within [`StoredValue`] by cloning.
///
/// See [`StoredValue::with_value`] for a version that avoids cloning.
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_get_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// // u8 is practically free to clone.
/// let data: StoredValue<u8> = StoredValue::new(10);
///
/// // Larger data structures can become very expensive to clone.
/// // You may prefer to use StoredValue::try_with_value.
/// let _expensive: StoredValue<Vec<String>> = StoredValue::new(vec![]);
///
/// // Easy to move into closures because StoredValue is Copy + 'static
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.get_value(), u8::MAX);
///
/// zero();
/// assert_eq!(data.get_value(), u8::MIN);
/// ```
pub fn get_value(&self) -> T {
self.with_value(T::clone)
}
}
impl<T, S> Dispose for StoredValue<T, S> {
fn dispose(self) {
self.value.dispose();
}
}
impl<T> From<ArcStoredValue<T>> for StoredValue<T>
where
T: Send + Sync + 'static,
{
#[track_caller]
fn from(value: ArcStoredValue<T>) -> Self {
StoredValue {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: ArenaItem::new(value),
}
}
}
impl<T, S> From<StoredValue<T, S>> for ArcStoredValue<T>
where
S: Storage<ArcStoredValue<T>>,
{
#[track_caller]
fn from(value: StoredValue<T, S>) -> Self {
value
.value
.try_get_value()
.unwrap_or_else(unwrap_signal!(value))
}
}
/// Creates a new [`StoredValue`].
#[inline(always)]
#[track_caller]

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

@@ -52,7 +52,7 @@ use crate::{
effect::Effect,
graph::{Observer, Source, Subscriber, ToAnySource},
owner::Owner,
signal::{arc_signal, guards::UntrackedWriteGuard, ArcReadSignal},
signal::{arc_signal, ArcReadSignal},
};
use any_spawner::Executor;
use futures::{Stream, StreamExt};
@@ -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)
}
}
@@ -654,189 +639,3 @@ pub fn panic_getting_disposed_signal(
)
}
}
/// A variation of the [`Read`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait ReadValue: Sized + DefinedAt {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
/// Returns the non-reactive guard, or `None` if the value has already been disposed.
#[track_caller]
fn try_read_value(&self) -> Option<Self::Value>;
/// Returns the non-reactive guard.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn read_value(&self) -> Self::Value {
self.try_read_value().unwrap_or_else(unwrap_signal!(self))
}
}
/// A variation of the [`With`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait WithValue: DefinedAt {
/// The type of the value contained in the value.
type Value: ?Sized;
/// Applies the closure to the value, non-reactively, and returns the result,
/// or `None` if the value has already been disposed.
#[track_caller]
fn try_with_value<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U>;
/// Applies the closure to the value, non-reactively, and returns the result.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn with_value<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
self.try_with_value(fun)
.unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> WithValue for T
where
T: DefinedAt + ReadValue,
{
type Value = <<Self as ReadValue>::Value as Deref>::Target;
fn try_with_value<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
self.try_read_value().map(|value| fun(&value))
}
}
/// A variation of the [`Get`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait GetValue: DefinedAt {
/// The type of the value contained in the value.
type Value: Clone;
/// Clones and returns the value of the value, non-reactively,
/// or `None` if the value has already been disposed.
#[track_caller]
fn try_get_value(&self) -> Option<Self::Value>;
/// Clones and returns the value of the value, non-reactively.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn get_value(&self) -> Self::Value {
self.try_get_value().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> GetValue for T
where
T: WithValue,
T::Value: Clone,
{
type Value = <Self as WithValue>::Value;
fn try_get_value(&self) -> Option<Self::Value> {
self.try_with_value(Self::Value::clone)
}
}
/// A variation of the [`Write`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait WriteValue: Sized + DefinedAt {
/// The type of the value's value.
type Value: Sized + 'static;
/// Returns a non-reactive write guard, or `None` if the value has already been disposed.
#[track_caller]
fn try_write_value(&self) -> Option<UntrackedWriteGuard<Self::Value>>;
/// Returns a non-reactive write guard.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn write_value(&self) -> UntrackedWriteGuard<Self::Value> {
self.try_write_value().unwrap_or_else(unwrap_signal!(self))
}
}
/// A variation of the [`Update`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait UpdateValue: DefinedAt {
/// The type of the value contained in the value.
type Value;
/// Updates the value, returning the value that is
/// returned by the update function, or `None` if the value has already been disposed.
#[track_caller]
fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
) -> Option<U>;
/// Updates the value.
#[track_caller]
fn update_value(&self, fun: impl FnOnce(&mut Self::Value)) {
self.try_update_value(fun);
}
}
impl<T> UpdateValue for T
where
T: WriteValue,
{
type Value = <Self as WriteValue>::Value;
#[track_caller]
fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
) -> Option<U> {
let mut guard = self.try_write_value()?;
Some(fun(&mut *guard))
}
}
/// A variation of the [`Set`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait SetValue: DefinedAt {
/// The type of the value contained in the value.
type Value;
/// Updates the value by replacing it, non-reactively.
///
/// If the value has already been disposed, returns `Some(value)` with the value that was
/// passed in. Otherwise, returns `None`.
#[track_caller]
fn try_set_value(&self, value: Self::Value) -> Option<Self::Value>;
/// Updates the value by replacing it, non-reactively.
#[track_caller]
fn set_value(&self, value: Self::Value) {
self.try_set_value(value);
}
}
impl<T> SetValue for T
where
T: WriteValue,
{
type Value = <Self as WriteValue>::Value;
fn try_set_value(&self, value: Self::Value) -> Option<Self::Value> {
// Unlike most other traits, for these None actually means success:
if let Some(mut guard) = self.try_write_value() {
*guard = value;
None
} else {
Some(value)
}
}
}

View File

@@ -3,29 +3,15 @@
/// Types that abstract over signals with values that can be read.
pub mod read {
use crate::{
computed::{ArcMemo, Memo, MemoInner},
computed::{ArcMemo, Memo},
graph::untrack,
owner::{
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
SyncStorage,
},
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
},
traits::{
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
},
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, With, WithUntracked},
unwrap_signal,
};
use send_wrapper::SendWrapper;
use std::{
borrow::Borrow,
fmt::Display,
ops::Deref,
panic::Location,
sync::{Arc, RwLock},
};
use std::{panic::Location, sync::Arc};
/// Possibilities for the inner type of a [`Signal`].
pub enum SignalTypes<T, S = SyncStorage>
@@ -38,8 +24,6 @@ pub mod read {
Memo(ArcMemo<T, S>),
/// A derived signal.
DerivedSignal(Arc<dyn Fn() -> T + Send + Sync>),
/// A static, stored value.
Stored(ArcStoredValue<T>),
}
impl<T, S> Clone for SignalTypes<T, S>
@@ -51,7 +35,6 @@ pub mod read {
Self::ReadSignal(arg0) => Self::ReadSignal(arg0.clone()),
Self::Memo(arg0) => Self::Memo(arg0.clone()),
Self::DerivedSignal(arg0) => Self::DerivedSignal(arg0.clone()),
Self::Stored(arg0) => Self::Stored(arg0.clone()),
}
}
}
@@ -69,9 +52,6 @@ pub mod read {
Self::DerivedSignal(_) => {
f.debug_tuple("DerivedSignal").finish()
}
Self::Stored(arg0) => {
f.debug_tuple("Static").field(arg0).finish()
}
}
}
}
@@ -188,16 +168,6 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
#[track_caller]
pub fn stored(value: T) -> Self {
Self {
inner: SignalTypes::Stored(ArcStoredValue::new(value)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> Default for ArcSignal<T, SyncStorage>
@@ -205,7 +175,7 @@ pub mod read {
T: Default + Send + Sync + 'static,
{
fn default() -> Self {
Self::stored(Default::default())
Self::derive(|| Default::default())
}
}
@@ -261,71 +231,40 @@ pub mod read {
}
}
impl<T, S> Track for ArcSignal<T, S>
impl<T, S> WithUntracked for ArcSignal<T, S>
where
S: Storage<T>,
{
fn track(&self) {
type Value = T;
fn try_with_untracked<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_untracked(fun),
SignalTypes::Memo(i) => i.try_with_untracked(fun),
SignalTypes::DerivedSignal(i) => Some(untrack(|| fun(&i()))),
}
}
}
impl<T, S> ReadUntracked for ArcSignal<T, S>
impl<T, S> With for ArcSignal<T, S>
where
S: Storage<T>,
T: Clone,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
type Value = T;
fn try_read_untracked(&self) -> Option<Self::Value> {
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.try_read_untracked().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read_untracked().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(untrack(|| i())))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
SignalTypes::ReadSignal(i) => i.try_with(fun),
SignalTypes::Memo(i) => i.try_with(fun),
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
}
.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)
}
}
.map(ReadGuard::new),
)
}
}
@@ -404,91 +343,51 @@ pub mod read {
}
}
impl<T, S> Track for Signal<T, S>
where
T: 'static,
S: Storage<T> + Storage<SignalTypes<T, S>>,
{
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, S> ReadUntracked for Signal<T, S>
impl<T, S> WithUntracked for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
type Value = T;
fn try_read_untracked(&self) -> Option<Self::Value> {
fn try_with_untracked<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)
.and_then(|inner| {
match &inner {
SignalTypes::ReadSignal(i) => {
i.try_read_untracked().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read_untracked().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(untrack(|| i())))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
.and_then(|inner| match &inner {
SignalTypes::ReadSignal(i) => i.try_with_untracked(fun),
SignalTypes::Memo(i) => i.try_with_untracked(fun),
SignalTypes::DerivedSignal(i) => {
Some(untrack(|| fun(&i())))
}
.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)
}
}
.map(ReadGuard::new)
}),
)
impl<T, S> With for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
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)
.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())),
})
}
}
@@ -534,18 +433,6 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
#[track_caller]
pub fn stored(value: T) -> Self {
Self {
inner: ArenaItem::new_with_storage(SignalTypes::Stored(
ArcStoredValue::new(value),
)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> Signal<T, LocalStorage>
@@ -573,19 +460,6 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
/// Works like [`Signal::stored`] but uses [`LocalStorage`].
#[track_caller]
pub fn stored_local(value: T) -> Self {
Self {
inner: ArenaItem::new_local(SignalTypes::Stored(
ArcStoredValue::new(value),
)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> Default for Signal<T>
@@ -593,7 +467,7 @@ pub mod read {
T: Send + Sync + Default + 'static,
{
fn default() -> Self {
Self::stored(Default::default())
Self::derive(|| Default::default())
}
}
@@ -602,34 +476,34 @@ pub mod read {
T: Default + 'static,
{
fn default() -> Self {
Self::stored_local(Default::default())
Self::derive_local(|| Default::default())
}
}
impl<T: Send + Sync + 'static> From<T> for ArcSignal<T, SyncStorage> {
impl<T: Clone + Send + Sync + 'static> From<T> for ArcSignal<T, SyncStorage> {
#[track_caller]
fn from(value: T) -> Self {
ArcSignal::stored(value)
Self::derive(move || value.clone())
}
}
impl<T> From<T> for Signal<T>
where
T: Send + Sync + 'static,
T: Clone + Send + Sync + 'static,
{
#[track_caller]
fn from(value: T) -> Self {
Self::stored(value)
Self::derive(move || value.clone())
}
}
impl<T> From<T> for Signal<T, LocalStorage>
where
T: 'static,
T: Clone + 'static,
{
#[track_caller]
fn from(value: T) -> Self {
Self::stored_local(value)
Self::derive_local(move || value.clone())
}
}
@@ -768,20 +642,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,38 +715,37 @@ pub mod read {
}
}
impl<T, S> Track for MaybeSignal<T, S>
impl<T, S> WithUntracked for MaybeSignal<T, S>
where
S: Storage<T> + Storage<SignalTypes<T, S>>,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
fn track(&self) {
type Value = T;
fn try_with_untracked<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_untracked(fun),
}
}
}
impl<T, S> ReadUntracked for MaybeSignal<T, S>
impl<T, S> With for MaybeSignal<T, S>
where
T: Clone,
T: Send + Sync + 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
type Value = T;
fn try_read_untracked(&self) -> Option<Self::Value> {
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match self {
Self::Static(t) => {
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
}
Self::Dynamic(s) => s.try_read_untracked(),
}
}
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
match self {
Self::Static(_) => None,
Self::Dynamic(s) => s.custom_try_read(),
Self::Static(t) => Some(fun(t)),
Self::Dynamic(s) => s.try_with(fun),
}
}
}
@@ -1034,7 +893,7 @@ pub mod read {
impl<S> From<&str> for MaybeSignal<String, S>
where
S: Storage<String> + Storage<Arc<RwLock<String>>>,
S: Storage<String>,
{
fn from(value: &str) -> Self {
Self::Static(value.to_string())
@@ -1109,36 +968,37 @@ pub mod read {
}
}
impl<T, S> Track for MaybeProp<T, S>
impl<T, S> WithUntracked for MaybeProp<T, S>
where
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
fn track(&self) {
type Value = Option<T>;
fn try_with_untracked<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_untracked(fun),
}
}
}
impl<T, S> ReadUntracked for MaybeProp<T, S>
impl<T, S> With for MaybeProp<T, S>
where
T: Clone,
T: Send + Sync + 'static,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
type Value = Option<T>;
fn try_read_untracked(&self) -> Option<Self::Value> {
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match &self.0 {
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
Some(inner) => inner.try_read_untracked(),
}
}
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
match &self.0 {
None => None,
Some(inner) => inner.custom_try_read(),
None => Some(fun(&None)),
Some(inner) => inner.try_with(fun),
}
}
}
@@ -1382,76 +1242,6 @@ pub mod read {
Self(Some(MaybeSignal::from_local(Some(value.to_string()))))
}
}
/// The content of a [`Signal`] wrapper read guard, variable depending on the signal type.
#[derive(Debug)]
pub enum SignalReadGuard<T: 'static, S: Storage<T>> {
/// A read signal guard.
Read(ReadGuard<T, Plain<T>>),
/// A memo guard.
Memo(ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>),
/// A fake guard for derived signals, the content had to actually be cloned, so it's not a guard but we pretend it is.
Owned(T),
}
impl<T, S> Clone for SignalReadGuard<T, S>
where
S: Storage<T>,
T: Clone,
Plain<T>: Clone,
Mapped<Plain<MemoInner<T, S>>, T>: Clone,
{
fn clone(&self) -> Self {
match self {
SignalReadGuard::Read(i) => SignalReadGuard::Read(i.clone()),
SignalReadGuard::Memo(i) => SignalReadGuard::Memo(i.clone()),
SignalReadGuard::Owned(i) => SignalReadGuard::Owned(i.clone()),
}
}
}
impl<T, S> Deref for SignalReadGuard<T, S>
where
S: Storage<T>,
{
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
SignalReadGuard::Read(i) => i,
SignalReadGuard::Memo(i) => i,
SignalReadGuard::Owned(i) => i,
}
}
}
impl<T, S> Borrow<T> for SignalReadGuard<T, S>
where
S: Storage<T>,
{
fn borrow(&self) -> &T {
self.deref()
}
}
impl<T, S> PartialEq<T> for SignalReadGuard<T, S>
where
S: Storage<T>,
T: PartialEq,
{
fn eq(&self, other: &T) -> bool {
self.deref() == other
}
}
impl<T, S> Display for SignalReadGuard<T, S>
where
S: Storage<T>,
T: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&**self, f)
}
}
}
/// Types that abstract over the ability to update a signal.

View File

@@ -1,88 +0,0 @@
use reactive_graph::{
computed::Memo,
owner::{on_cleanup, Owner},
signal::{RwSignal, Trigger},
traits::{Dispose, GetUntracked, Track},
};
use std::sync::Arc;
#[test]
fn cleanup_on_dispose() {
let owner = Owner::new();
owner.set();
struct ExecuteOnDrop(Option<Box<dyn FnOnce() + Send + Sync>>);
impl ExecuteOnDrop {
fn new(f: impl FnOnce() + Send + Sync + 'static) -> Self {
Self(Some(Box::new(f)))
}
}
impl Drop for ExecuteOnDrop {
fn drop(&mut self) {
self.0.take().unwrap()();
}
}
let trigger = Trigger::new();
println!("STARTING");
let memo = Memo::new(move |_| {
trigger.track();
// An example of why you might want to do this is that
// when something goes out of reactive scope you want it to be cleaned up.
// The cleaning up might have side effects, and those side effects might cause
// re-renders where new `on_cleanup` are registered.
let on_drop = ExecuteOnDrop::new(|| {
on_cleanup(|| println!("Nested cleanup in progress."))
});
on_cleanup(move || {
println!("Cleanup in progress.");
drop(on_drop)
});
});
println!("Memo 1: {:?}", memo);
memo.get_untracked(); // First cleanup registered.
memo.dispose(); // Cleanup not run here.
println!("Cleanup should have been executed.");
let memo = Memo::new(move |_| {
// New cleanup registered. It'll panic here.
on_cleanup(move || println!("Test passed."));
});
println!("Memo 2: {:?}", memo);
println!("^ Note how the memos have the same key (different versions).");
memo.get_untracked(); // First cleanup registered.
println!("Test passed.");
memo.dispose();
}
#[test]
fn leak_on_dispose() {
let owner = Owner::new();
owner.set();
let trigger = Trigger::new();
let value = Arc::new(());
let weak = Arc::downgrade(&value);
let memo = Memo::new(move |_| {
trigger.track();
RwSignal::new(value.clone());
});
memo.get_untracked();
memo.dispose();
assert!(weak.upgrade().is_none()); // Should have been dropped.
}

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

@@ -11,26 +11,6 @@ use std::{
sync::Arc,
};
/// Declares that this type can be converted into some other type, which is a valid attribute value.
pub trait IntoAttributeValue {
/// The attribute value into which this type can be converted.
type Output;
/// Consumes this value, transforming it into an attribute value.
fn into_attribute_value(self) -> Self::Output;
}
impl<T> IntoAttributeValue for T
where
T: AttributeValue,
{
type Output = Self;
fn into_attribute_value(self) -> Self::Output {
self
}
}
/// A possible value for an HTML attribute.
pub trait AttributeValue: Send {
/// The state that should be retained between building and rebuilding.

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

@@ -4,8 +4,8 @@ use crate::{
renderer::{CastFrom, Rndr},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, IntoRender, Mountable, Position, PositionState,
Render, RenderHtml, ToTemplate,
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml, ToTemplate,
},
};
use const_str_slice_concat::{
@@ -65,13 +65,11 @@ impl<E, At, Ch, NewChild> ElementChild<NewChild> for HtmlElement<E, At, Ch>
where
E: ElementWithChildren,
Ch: Render + NextTuple,
<Ch as NextTuple>::Output<NewChild::Output>: Render,
<Ch as NextTuple>::Output<NewChild>: Render,
NewChild: IntoRender,
NewChild::Output: Render,
NewChild: Render,
{
type Output =
HtmlElement<E, At, <Ch as NextTuple>::Output<NewChild::Output>>;
type Output = HtmlElement<E, At, <Ch as NextTuple>::Output<NewChild>>;
fn child(self, child: NewChild) -> Self::Output {
let HtmlElement {
@@ -84,7 +82,7 @@ where
tag,
attributes,
children: children.next_tuple(child.into_render()),
children: children.next_tuple(child),
}
}
}
@@ -118,7 +116,7 @@ where
/// Adds a child to the element.
pub trait ElementChild<NewChild>
where
NewChild: IntoRender,
NewChild: Render,
{
/// The type of the element, with the child added.
type Output;

View File

@@ -19,7 +19,6 @@ pub mod prelude {
OnAttribute, OnTargetAttribute, PropAttribute,
StyleAttribute,
},
IntoAttributeValue,
},
directive::DirectiveAttribute,
element::{ElementChild, ElementExt, InnerHtmlAttribute},
@@ -27,8 +26,8 @@ pub mod prelude {
},
renderer::{dom::Dom, Renderer},
view::{
add_attr::AddAnyAttr, any_view::IntoAny, IntoRender, Mountable,
Render, RenderHtml,
add_attr::AddAnyAttr, any_view::IntoAny, Mountable, Render,
RenderHtml,
},
};
}

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

@@ -11,7 +11,7 @@ macro_rules! svg_elements {
($($tag:ident [$($attr:ty),*]),* $(,)?) => {
paste::paste! {
$(
/// An SVG element.
/// An SVG attribute.
// `tag()` function
#[allow(non_snake_case)]
pub fn $tag() -> HtmlElement<[<$tag:camel>], (), ()>
@@ -151,33 +151,4 @@ svg_elements![
view [],
];
/// An SVG element.
#[allow(non_snake_case)]
pub fn r#use() -> HtmlElement<Use, (), ()>
where {
HtmlElement {
tag: Use,
attributes: (),
children: (),
}
}
/// An SVG element.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Use;
impl ElementType for Use {
type Output = web_sys::SvgElement;
const TAG: &'static str = "use";
const SELF_CLOSING: bool = false;
const ESCAPE_CHILDREN: bool = true;
const NAMESPACE: Option<&'static str> = Some("http://www.w3.org/2000/svg");
#[inline(always)]
fn tag(&self) -> &str {
Self::TAG
}
}
impl ElementWithChildren for Use {}
// TODO <use>

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

@@ -432,23 +432,3 @@ pub enum Position {
/// This is the last child of its parent.
LastChild,
}
/// Declares that this type can be converted into some other type, which can be renderered.
pub trait IntoRender {
/// The renderable type into which this type can be converted.
type Output;
/// Consumes this value, transforming it into the renderable type.
fn into_render(self) -> Self::Output;
}
impl<T> IntoRender for T
where
T: Render,
{
type Output = Self;
fn into_render(self) -> Self::Output {
self
}
}

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);