mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 17:11:55 -05:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebea3ec14d | ||
|
|
ed3ca9eae1 | ||
|
|
ed8abcfbcd | ||
|
|
29a56af38c | ||
|
|
f9d6f7fd64 | ||
|
|
251a472cd5 | ||
|
|
3c76cb24d3 | ||
|
|
67c3bf2478 | ||
|
|
f3aaae857a | ||
|
|
d727e53dd6 | ||
|
|
e4543ab5df | ||
|
|
1ca0f4430c | ||
|
|
b59fa11853 | ||
|
|
e55f08e017 | ||
|
|
fa1939e5b2 | ||
|
|
8b2f0eaf44 | ||
|
|
b118d69281 | ||
|
|
ee66f6c395 | ||
|
|
eba08ad592 | ||
|
|
4833b4e287 | ||
|
|
9d1be64e4d | ||
|
|
d6e6cd3be0 | ||
|
|
70476f9277 | ||
|
|
d8ddfc26e9 | ||
|
|
c8acc3e8bd | ||
|
|
547442243b | ||
|
|
6e58266f54 | ||
|
|
f0cd0fb41d | ||
|
|
7585faf57e | ||
|
|
da7f6a34e8 | ||
|
|
4f7fa41262 | ||
|
|
4becfa39ca | ||
|
|
f8388b122d | ||
|
|
f57a57b92b | ||
|
|
f0bcbd9cfe | ||
|
|
115477ef1d | ||
|
|
832b9cb321 | ||
|
|
b0150ceeec | ||
|
|
af8df34360 | ||
|
|
b2e6185b22 | ||
|
|
d2bfb3080b | ||
|
|
72ebd17042 | ||
|
|
e2f0b4deeb | ||
|
|
57c07e9aec | ||
|
|
0835066bc0 | ||
|
|
656e83fe24 | ||
|
|
ad0252ecfd | ||
|
|
77f05c6f4e | ||
|
|
a4ea491dc0 | ||
|
|
3c89b9c930 | ||
|
|
93d7ba0d5f | ||
|
|
e188993800 | ||
|
|
c1dc8c7629 | ||
|
|
ab9de1b8c0 | ||
|
|
b39985d9b8 | ||
|
|
5e8e93001d | ||
|
|
a4ed0cbe5b | ||
|
|
422fe9f43b | ||
|
|
36df36e16c | ||
|
|
95fc79034b | ||
|
|
7403e4084f | ||
|
|
8feee5e5d7 | ||
|
|
e6da266b4f | ||
|
|
0798e0812d | ||
|
|
03514e68be | ||
|
|
4092368581 | ||
|
|
dcc7865989 | ||
|
|
896f7de8e1 | ||
|
|
29b2d3024a | ||
|
|
c47893ad60 | ||
|
|
0d4f3b51e9 | ||
|
|
2431b19cdf | ||
|
|
4801e1ec6d | ||
|
|
e206b93ba5 | ||
|
|
f0675446d8 | ||
|
|
2ac7eaad15 | ||
|
|
8a040fda69 | ||
|
|
2f5c966cf4 | ||
|
|
45e2629f0e | ||
|
|
517566d085 | ||
|
|
6df8657700 | ||
|
|
2a4063a259 | ||
|
|
013ec4a09d | ||
|
|
d10fec08e2 | ||
|
|
94f4328586 | ||
|
|
2b70961110 | ||
|
|
699c54e16c | ||
|
|
1217ef4d8e | ||
|
|
9aba3efbe4 | ||
|
|
1b48a2f8d5 | ||
|
|
31cb766206 | ||
|
|
4c3bcaa68d | ||
|
|
fe060617d2 | ||
|
|
de28a317f0 | ||
|
|
d29433b98d | ||
|
|
f2582b6ac9 | ||
|
|
67845be161 | ||
|
|
2bf1f46b88 | ||
|
|
70ae3a0abb | ||
|
|
96e2b5cba1 | ||
|
|
ef27a198d9 | ||
|
|
47299926bb | ||
|
|
bdc2285658 | ||
|
|
9d4ce6e526 | ||
|
|
3d2cdc21a1 | ||
|
|
93d939aef8 | ||
|
|
fb04750607 | ||
|
|
a080496e7e | ||
|
|
9fc1002167 | ||
|
|
bc5c766530 | ||
|
|
17821f863a | ||
|
|
1ca4f34ef3 | ||
|
|
8f0a1554b1 | ||
|
|
38d4f26d03 | ||
|
|
2b04c2710d | ||
|
|
a4937a1236 | ||
|
|
f6f2c39686 | ||
|
|
d7eacf1ab5 | ||
|
|
d1a4bbe28e | ||
|
|
412ecd6b1b | ||
|
|
9bc0152121 | ||
|
|
4b05cada8f | ||
|
|
a818862704 | ||
|
|
173487debc | ||
|
|
449d96cc9a | ||
|
|
f9bf6a95ed |
2
.github/workflows/run-cargo-make-task.yml
vendored
2
.github/workflows/run-cargo-make-task.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
fi
|
||||
done
|
||||
- name: Install Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: Maybe install gtk-rs dependencies
|
||||
|
||||
42
Cargo.toml
42
Cargo.toml
@@ -40,36 +40,36 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0-beta5"
|
||||
version = "0.7.0-gamma3"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-beta5" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-beta5" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-beta5" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-beta5" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta5" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta5" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-beta5" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta5" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-beta5" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-beta5" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-beta5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma3" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-gamma3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma3" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-gamma3" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma3" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-gamma3" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma3" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta5" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta5" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta5" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-beta5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta5" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-beta5" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma3" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma3" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-gamma3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma3" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-gamma3" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0-beta5"
|
||||
version = "0.2.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -9,6 +9,7 @@ description = "Spawn asynchronous tasks in an executor-independent way."
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.30"
|
||||
glib = { version = "0.20.0", optional = true }
|
||||
thiserror = "1.0"
|
||||
@@ -19,12 +20,14 @@ tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.42", optional = true }
|
||||
|
||||
[features]
|
||||
async-executor = ["dep:async-executor"]
|
||||
tracing = ["dep:tracing"]
|
||||
tokio = ["dep:tokio"]
|
||||
glib = ["dep:glib"]
|
||||
wasm-bindgen = ["dep:wasm-bindgen-futures"]
|
||||
futures-executor = ["futures/thread-pool", "futures/executor"]
|
||||
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
@@ -32,11 +32,14 @@
|
||||
use std::{future::Future, pin::Pin, sync::OnceLock};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
pub(crate) type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
/// A future that has been pinned.
|
||||
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
/// A future that has been pinned.
|
||||
pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
static SPAWN: OnceLock<fn(PinnedFuture<()>)> = OnceLock::new();
|
||||
static SPAWN_LOCAL: OnceLock<fn(PinnedLocalFuture<()>)> = OnceLock::new();
|
||||
static POLL_LOCAL: OnceLock<fn()> = OnceLock::new();
|
||||
|
||||
/// Errors that can occur when using the executor.
|
||||
#[derive(Error, Debug)]
|
||||
@@ -115,6 +118,14 @@ impl Executor {
|
||||
});
|
||||
_ = rx.await;
|
||||
}
|
||||
|
||||
/// Polls the current async executor.
|
||||
/// Not all async executors support polling, so this function may not do anything.
|
||||
pub fn poll_local() {
|
||||
if let Some(poller) = POLL_LOCAL.get() {
|
||||
poller()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Executor {
|
||||
@@ -193,13 +204,15 @@ impl Executor {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
|
||||
pub fn init_futures_executor() -> Result<(), ExecutorError> {
|
||||
use futures::{
|
||||
executor::{LocalPool, ThreadPool},
|
||||
executor::{LocalPool, LocalSpawner, ThreadPool},
|
||||
task::{LocalSpawnExt, SpawnExt},
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
|
||||
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_POOL: LocalPool = LocalPool::new();
|
||||
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
|
||||
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
|
||||
}
|
||||
|
||||
fn get_thread_pool() -> &'static ThreadPool {
|
||||
@@ -218,28 +231,97 @@ impl Executor {
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
let spawner = pool.spawner();
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
if let Ok(mut pool) = pool.try_borrow_mut() {
|
||||
pool.run_until_stalled();
|
||||
}
|
||||
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
|
||||
});
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use crate::Executor;
|
||||
use std::rc::Rc;
|
||||
Executor::init_futures_executor().expect("couldn't set executor");
|
||||
let rc = Rc::new(());
|
||||
Executor::spawn_local(async {
|
||||
_ = rc;
|
||||
});
|
||||
Executor::spawn(async {});
|
||||
/// Globally sets the [`async_executor`] executor as the executor used to spawn tasks,
|
||||
/// lazily creating a thread pool to spawn tasks into.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
///
|
||||
/// Requires the `async-executor` feature to be activated on this crate.
|
||||
#[cfg(feature = "async-executor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-executor")))]
|
||||
pub fn init_async_executor() -> Result<(), ExecutorError> {
|
||||
use async_executor::{Executor, LocalExecutor};
|
||||
|
||||
static THREAD_POOL: OnceLock<Executor> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
|
||||
}
|
||||
|
||||
fn get_thread_pool() -> &'static Executor<'static> {
|
||||
THREAD_POOL.get_or_init(Executor::new)
|
||||
}
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
get_thread_pool().spawn(fut).detach();
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
LOCAL_POOL.with(|pool| pool.spawn(fut).detach());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| {
|
||||
LOCAL_POOL.with(|pool| pool.try_tick());
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets a custom executor as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
pub fn init_custom_executor(
|
||||
custom_executor: impl CustomExecutor + 'static,
|
||||
) -> Result<(), ExecutorError> {
|
||||
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
|
||||
EXECUTOR
|
||||
.set(Box::new(custom_executor))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
EXECUTOR.get().unwrap().spawn(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| EXECUTOR.get().unwrap().spawn_local(fut))
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
POLL_LOCAL
|
||||
.set(|| EXECUTOR.get().unwrap().poll_local())
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for custom executors.
|
||||
/// Custom executors can be used to integrate with any executor that supports spawning futures.
|
||||
///
|
||||
/// All methods can be called recursively.
|
||||
pub trait CustomExecutor: Send + Sync {
|
||||
/// Spawns a future, usually on a thread pool.
|
||||
fn spawn(&self, fut: PinnedFuture<()>);
|
||||
/// Spawns a local future. May require calling `poll_local` to make progress.
|
||||
fn spawn_local(&self, fut: PinnedLocalFuture<()>);
|
||||
/// Polls the executor, if it supports polling.
|
||||
fn poll_local(&self);
|
||||
}
|
||||
|
||||
55
any_spawner/tests/custom_runtime.rs
Normal file
55
any_spawner/tests/custom_runtime.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
#[cfg(feature = "futures-executor")]
|
||||
use any_spawner::{CustomExecutor, Executor, PinnedFuture, PinnedLocalFuture};
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_create_custom_executor() {
|
||||
use futures::{
|
||||
executor::{LocalPool, LocalSpawner},
|
||||
task::LocalSpawnExt,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
|
||||
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
|
||||
}
|
||||
|
||||
struct CustomFutureExecutor;
|
||||
impl CustomExecutor for CustomFutureExecutor {
|
||||
fn spawn(&self, _fut: PinnedFuture<()>) {
|
||||
panic!("not supported in this test");
|
||||
}
|
||||
|
||||
fn spawn_local(&self, fut: PinnedLocalFuture<()>) {
|
||||
SPAWNER.with(|spawner| {
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
}
|
||||
|
||||
fn poll_local(&self) {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
if let Ok(mut pool) = pool.try_borrow_mut() {
|
||||
pool.run_until_stalled();
|
||||
}
|
||||
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Executor::init_custom_executor(CustomFutureExecutor)
|
||||
.expect("couldn't set executor");
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let counter_clone = Arc::clone(&counter);
|
||||
Executor::spawn_local(async move {
|
||||
counter_clone.store(1, Ordering::Release);
|
||||
});
|
||||
Executor::poll_local();
|
||||
assert_eq!(counter.load(Ordering::Acquire), 1);
|
||||
}
|
||||
38
any_spawner/tests/futures_runtime.rs
Normal file
38
any_spawner/tests/futures_runtime.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
#[cfg(feature = "futures-executor")]
|
||||
use any_spawner::Executor;
|
||||
// All tests in this file use the same executor.
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use std::rc::Rc;
|
||||
|
||||
let _ = Executor::init_futures_executor();
|
||||
let rc = Rc::new(());
|
||||
Executor::spawn_local(async {
|
||||
_ = rc;
|
||||
});
|
||||
Executor::spawn(async {});
|
||||
}
|
||||
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_make_local_progress() {
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
let _ = Executor::init_futures_executor();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
Executor::spawn_local({
|
||||
let counter = Arc::clone(&counter);
|
||||
async move {
|
||||
assert_eq!(counter.fetch_add(1, Ordering::AcqRel), 0);
|
||||
Executor::spawn_local(async {
|
||||
// Should not crash
|
||||
});
|
||||
}
|
||||
});
|
||||
Executor::poll_local();
|
||||
assert_eq!(counter.load(Ordering::Acquire), 1);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use counter::*;
|
||||
use leptos::mount::mount_to;
|
||||
use leptos::prelude::*;
|
||||
use leptos::spawn::tick;
|
||||
use leptos::task::tick;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{prelude::*, reactive_graph::actions::Action};
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, A},
|
||||
StaticSegment,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use counter_without_macros::counter;
|
||||
use leptos::{prelude::*, spawn::tick};
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use pretty_assertions::assert_eq;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
@@ -4,7 +4,7 @@ use wasm_bindgen_test::*;
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counters::Counters;
|
||||
use leptos::prelude::*;
|
||||
use leptos::spawn::tick;
|
||||
use leptos::task::tick;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use directives::App;
|
||||
use leptos::{prelude::*, spawn::tick};
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "gtk"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
throw_error = { path = "../../any_error/" }
|
||||
|
||||
# these are used to build the integration
|
||||
gtk = { version = "0.9.0", package = "gtk4" }
|
||||
next_tuple = { path = "../../next_tuple/" }
|
||||
paste = "1.0"
|
||||
|
||||
# we want to support using glib for the reactive runtime event loop
|
||||
any_spawner = { path = "../../any_spawner/", features = ["glib"] }
|
||||
# yes, we want effects to run: this is a "frontend," not a backend
|
||||
reactive_graph = { path = "../../reactive_graph", features = ["effects"] }
|
||||
@@ -1 +0,0 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
@@ -1,8 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="color-scheme" content="dark">
|
||||
<link rel="css" href="style.css" data-trunk>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,645 +0,0 @@
|
||||
use self::properties::Connect;
|
||||
use gtk::{
|
||||
glib::{
|
||||
object::{IsA, IsClass, ObjectExt},
|
||||
Object, Value,
|
||||
},
|
||||
prelude::{Cast, WidgetExt},
|
||||
Label, Orientation, Widget,
|
||||
};
|
||||
use leptos::{
|
||||
reactive_graph::effect::RenderEffect,
|
||||
tachys::{
|
||||
renderer::{CastFrom, Renderer},
|
||||
view::{Mountable, Render},
|
||||
},
|
||||
};
|
||||
use next_tuple::NextTuple;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LeptosGtk;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Element(pub Widget);
|
||||
|
||||
impl Element {
|
||||
pub fn remove(&self) {
|
||||
self.0.unparent();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Text(pub Element);
|
||||
|
||||
impl<T> From<T> for Element
|
||||
where
|
||||
T: Into<Widget>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Element(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<LeptosGtk> for Element {
|
||||
fn unmount(&mut self) {
|
||||
self.remove()
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
if let Some(parent) = self.0.parent() {
|
||||
child.mount(&Element(parent), Some(self));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<LeptosGtk> for Text {
|
||||
fn unmount(&mut self) {
|
||||
self.0.remove()
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.0
|
||||
.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
self.0.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastFrom<Element> for Element {
|
||||
fn cast_from(source: Element) -> Option<Self> {
|
||||
Some(source)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastFrom<Element> for Text {
|
||||
fn cast_from(source: Element) -> Option<Self> {
|
||||
source
|
||||
.0
|
||||
.downcast::<Label>()
|
||||
.ok()
|
||||
.map(|n| Text(Element::from(n)))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Element> for Element {
|
||||
fn as_ref(&self) -> &Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Element> for Text {
|
||||
fn as_ref(&self) -> &Element {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for LeptosGtk {
|
||||
type Node = Element;
|
||||
type Element = Element;
|
||||
type Text = Text;
|
||||
type Placeholder = Element;
|
||||
|
||||
fn intern(text: &str) -> &str {
|
||||
text
|
||||
}
|
||||
|
||||
fn create_text_node(text: &str) -> Self::Text {
|
||||
Text(Element::from(Label::new(Some(text))))
|
||||
}
|
||||
|
||||
fn create_placeholder() -> Self::Placeholder {
|
||||
let label = Label::new(None);
|
||||
label.set_visible(false);
|
||||
Element::from(label)
|
||||
}
|
||||
|
||||
fn set_text(node: &Self::Text, text: &str) {
|
||||
let node_as_text = node.0 .0.downcast_ref::<Label>().unwrap();
|
||||
node_as_text.set_label(text);
|
||||
}
|
||||
|
||||
fn set_attribute(node: &Self::Element, name: &str, value: &str) {
|
||||
node.0.set_property(name, value);
|
||||
}
|
||||
|
||||
fn remove_attribute(node: &Self::Element, name: &str) {
|
||||
node.0.set_property(name, None::<&str>);
|
||||
}
|
||||
|
||||
fn insert_node(
|
||||
parent: &Self::Element,
|
||||
new_child: &Self::Node,
|
||||
marker: Option<&Self::Node>,
|
||||
) {
|
||||
new_child
|
||||
.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|n| &n.0));
|
||||
}
|
||||
|
||||
fn remove_node(
|
||||
_parent: &Self::Element,
|
||||
_child: &Self::Node,
|
||||
) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn remove(_node: &Self::Node) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_parent(node: &Self::Node) -> Option<Self::Node> {
|
||||
node.0.parent().map(Element::from)
|
||||
}
|
||||
|
||||
fn first_child(_node: &Self::Node) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn next_sibling(_node: &Self::Node) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn log_node(node: &Self::Node) {
|
||||
println!("{node:?}");
|
||||
}
|
||||
|
||||
fn clear_children(_parent: &Self::Element) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root<Chil>(children: Chil) -> (Widget, impl Mountable<LeptosGtk>)
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
let state = r#box()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
.build();
|
||||
(state.as_widget().clone(), state)
|
||||
}
|
||||
|
||||
pub trait WidgetClass {
|
||||
type Widget: Into<Widget> + IsA<Object> + IsClass;
|
||||
}
|
||||
|
||||
pub struct LGtkWidget<Widg, Props, Chil> {
|
||||
widget: PhantomData<Widg>,
|
||||
properties: Props,
|
||||
children: Chil,
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Chil: NextTuple,
|
||||
{
|
||||
pub fn child<T>(
|
||||
self,
|
||||
child: T,
|
||||
) -> LGtkWidget<Widg, Props, Chil::Output<T>> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children: children.next_tuple(child),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn connect<F>(
|
||||
self,
|
||||
signal_name: &'static str,
|
||||
callback: F,
|
||||
) -> LGtkWidget<Widg, Props::Output<Connect<F>>, Chil>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Connect {
|
||||
signal_name,
|
||||
callback,
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
Props: Property,
|
||||
Widg: WidgetClass,
|
||||
{
|
||||
ty: PhantomData<Widg>,
|
||||
widget: Element,
|
||||
properties: Props::State,
|
||||
children: Chil::State,
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
Props: Property,
|
||||
Widg: WidgetClass,
|
||||
{
|
||||
pub fn as_widget(&self) -> &Widget {
|
||||
&self.widget.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> Render<LeptosGtk> for LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: Property,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
type State = LGtkWidgetState<Widg, Props, Chil>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let widget = Object::new::<Widg::Widget>();
|
||||
let widget = Element::from(widget);
|
||||
let properties = self.properties.build(&widget);
|
||||
let mut children = self.children.build();
|
||||
children.mount(&widget, None);
|
||||
LGtkWidgetState {
|
||||
ty: PhantomData,
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.properties
|
||||
.rebuild(&state.widget, &mut state.properties);
|
||||
self.children.rebuild(&mut state.children);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> Mountable<LeptosGtk>
|
||||
for LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: Property,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
self.children.unmount();
|
||||
self.widget.remove();
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.children.mount(&self.widget, None);
|
||||
LeptosGtk::insert_node(parent, &self.widget, marker);
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
self.widget.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Property {
|
||||
type State;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State;
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State);
|
||||
}
|
||||
|
||||
impl<T, F> Property for F
|
||||
where
|
||||
T: Property,
|
||||
T::State: 'static,
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
type State = RenderEffect<T::State>;
|
||||
|
||||
fn build(self, widget: &Element) -> Self::State {
|
||||
let widget = widget.clone();
|
||||
RenderEffect::new(move |prev| {
|
||||
let value = self();
|
||||
if let Some(mut prev) = prev {
|
||||
value.rebuild(&widget, &mut prev);
|
||||
prev
|
||||
} else {
|
||||
value.build(&widget)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn rebuild(self, widget: &Element, state: &mut Self::State) {
|
||||
let prev_value = state.take_value();
|
||||
let widget = widget.to_owned();
|
||||
*state = RenderEffect::new_with_value(
|
||||
move |prev| {
|
||||
let value = self();
|
||||
if let Some(mut state) = prev {
|
||||
value.rebuild(&widget, &mut state);
|
||||
state
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
},
|
||||
prev_value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn button() -> LGtkWidget<gtk::Button, (), ()> {
|
||||
LGtkWidget {
|
||||
widget: PhantomData,
|
||||
properties: (),
|
||||
children: (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn r#box() -> LGtkWidget<gtk::Box, (), ()> {
|
||||
LGtkWidget {
|
||||
widget: PhantomData,
|
||||
properties: (),
|
||||
children: (),
|
||||
}
|
||||
}
|
||||
|
||||
mod widgets {
|
||||
use super::WidgetClass;
|
||||
|
||||
impl WidgetClass for gtk::Button {
|
||||
type Widget = Self;
|
||||
}
|
||||
|
||||
impl WidgetClass for gtk::Box {
|
||||
type Widget = Self;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod properties {
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::{Element, LGtkWidget, LeptosGtk, Property, WidgetClass};
|
||||
use gtk::glib::{object::ObjectExt, Value};
|
||||
use leptos::tachys::{renderer::Renderer, view::Render};
|
||||
use next_tuple::NextTuple;
|
||||
|
||||
pub struct Connect<F>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
pub signal_name: &'static str,
|
||||
pub callback: F,
|
||||
}
|
||||
|
||||
impl<F> Property for Connect<F>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
type State = ();
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.connect(self.signal_name, false, self.callback);
|
||||
}
|
||||
|
||||
fn rebuild(self, _element: &Element, _state: &mut Self::State) {
|
||||
// TODO we want to *remove* the previous listener, and reconnect with this new one
|
||||
}
|
||||
}
|
||||
|
||||
/* examples for macro */
|
||||
pub struct Orientation {
|
||||
value: gtk::Orientation,
|
||||
}
|
||||
|
||||
pub struct OrientationState {
|
||||
value: gtk::Orientation,
|
||||
}
|
||||
|
||||
impl Property for Orientation {
|
||||
type State = OrientationState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.set_property("orientation", self.value);
|
||||
OrientationState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
if self.value != state.value {
|
||||
element.0.set_property("orientation", self.value);
|
||||
state.value = self.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn orientation(
|
||||
self,
|
||||
value: impl Into<gtk::Orientation>,
|
||||
) -> LGtkWidget<Widg, Props::Output<Orientation>, Chil> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Orientation {
|
||||
value: value.into(),
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Spacing {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
pub struct SpacingState {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
impl Property for Spacing {
|
||||
type State = SpacingState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.set_property("spacing", self.value);
|
||||
SpacingState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
if self.value != state.value {
|
||||
element.0.set_property("spacing", self.value);
|
||||
state.value = self.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn spacing(
|
||||
self,
|
||||
value: impl Into<i32>,
|
||||
) -> LGtkWidget<Widg, Props::Output<Spacing>, Chil> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Spacing {
|
||||
value: value.into(),
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* end examples for properties macro */
|
||||
#[derive(Debug)]
|
||||
pub struct Label {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LabelState {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Property for Label {
|
||||
type State = LabelState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
LeptosGtk::set_attribute(element, "label", &self.value);
|
||||
LabelState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
if self.value != state.value {
|
||||
LeptosGtk::set_attribute(element, "label", &self.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Property for () {
|
||||
type State = ();
|
||||
|
||||
fn build(self, _element: &Element) -> Self::State {}
|
||||
|
||||
fn rebuild(self, _element: &Element, _state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
macro_rules! tuples {
|
||||
($($ty:ident),* $(,)?) => {
|
||||
impl<$($ty,)*> Property for ($($ty,)*)
|
||||
where $($ty: Property,)*
|
||||
{
|
||||
type State = ($($ty::State,)*);
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($ty,)*) = self;
|
||||
($($ty.build(element),)*)
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
paste::paste! {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($ty,)*) = self;
|
||||
#[allow(non_snake_case)]
|
||||
let ($([<state_ $ty:lower>],)*) = state;
|
||||
$($ty.rebuild(element, [<state_ $ty:lower>]));*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tuples!(A);
|
||||
tuples!(A, B);
|
||||
tuples!(A, B, C);
|
||||
tuples!(A, B, C, D);
|
||||
tuples!(A, B, C, D, E);
|
||||
tuples!(A, B, C, D, E, F);
|
||||
tuples!(A, B, C, D, E, F, G);
|
||||
tuples!(A, B, C, D, E, F, G, H);
|
||||
tuples!(A, B, C, D, E, F, G, H, I);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W
|
||||
);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X
|
||||
);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X,
|
||||
Y
|
||||
);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
use any_spawner::Executor;
|
||||
use gtk::{prelude::*, Application, ApplicationWindow, Orientation};
|
||||
use leptos::prelude::*;
|
||||
use leptos_gtk::LeptosGtk;
|
||||
use std::{mem, thread, time::Duration};
|
||||
mod leptos_gtk;
|
||||
|
||||
const APP_ID: &str = "dev.leptos.Counter";
|
||||
|
||||
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
|
||||
fn main() {
|
||||
// use the glib event loop to power the reactive system
|
||||
_ = Executor::init_glib();
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
app.connect_startup(|_| load_css());
|
||||
|
||||
app.connect_activate(|app| {
|
||||
// Connect to "activate" signal of `app`
|
||||
let owner = Owner::new();
|
||||
let view = owner.with(ui);
|
||||
let (root, state) = leptos_gtk::root(view);
|
||||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("TachyGTK")
|
||||
.child(&root)
|
||||
.build();
|
||||
// Present window
|
||||
window.present();
|
||||
mem::forget((owner, state));
|
||||
});
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
fn ui() -> impl Render<LeptosGtk> {
|
||||
let value = RwSignal::new(0);
|
||||
let rows = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
|
||||
Effect::new(move |_| {
|
||||
println!("value = {}", value.get());
|
||||
});
|
||||
|
||||
// just an example of multithreaded reactivity
|
||||
thread::spawn(move || loop {
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
value.update(|n| *n += 1);
|
||||
});
|
||||
|
||||
vstack((
|
||||
hstack((
|
||||
button("-1", move || {
|
||||
println!("clicked -1");
|
||||
value.update(|n| *n -= 1);
|
||||
}),
|
||||
move || value.get().to_string(),
|
||||
button("+1", move || value.update(|n| *n += 1)),
|
||||
)),
|
||||
button("Swap", move || {
|
||||
rows.update(|items| {
|
||||
items.swap(1, 3);
|
||||
})
|
||||
}),
|
||||
hstack(rows),
|
||||
))
|
||||
}
|
||||
|
||||
fn button(
|
||||
label: impl Render<LeptosGtk>,
|
||||
callback: impl Fn() + Send + Sync + 'static,
|
||||
) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::button()
|
||||
.child(label)
|
||||
.connect("clicked", move |_| {
|
||||
callback();
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn vstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::r#box()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
}
|
||||
|
||||
fn hstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::r#box()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
}
|
||||
|
||||
fn load_css() {
|
||||
use gtk::{gdk::Display, CssProvider};
|
||||
|
||||
let provider = CssProvider::new();
|
||||
provider.load_from_path("style.css");
|
||||
|
||||
// Add the provider to the default screen
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&Display::default().expect("Could not connect to a display."),
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
}
|
||||
@@ -21,10 +21,16 @@ pub fn Nav() -> impl IntoView {
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
|
||||
<a
|
||||
class="github"
|
||||
href="http://github.com/leptos-rs/leptos"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -50,30 +50,42 @@ pub fn Stories() -> impl IntoView {
|
||||
<div class="news-view">
|
||||
<div class="news-list-nav">
|
||||
<span>
|
||||
{move || if page() > 1 {
|
||||
Either::Left(view! {
|
||||
<a class="page-link"
|
||||
href=move || format!("/{}?page={}", story_type(), page() - 1)
|
||||
aria-label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
})
|
||||
} else {
|
||||
Either::Right(view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
})
|
||||
{move || {
|
||||
if page() > 1 {
|
||||
Either::Left(
|
||||
view! {
|
||||
<a
|
||||
class="page-link"
|
||||
href=move || {
|
||||
format!("/{}?page={}", story_type(), page() - 1)
|
||||
}
|
||||
aria-label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Either::Right(
|
||||
view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
},
|
||||
)
|
||||
}
|
||||
}}
|
||||
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Suspense>
|
||||
<span class="page-link"
|
||||
<span
|
||||
class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
<a
|
||||
href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
@@ -83,14 +95,10 @@ pub fn Stories() -> impl IntoView {
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
<Transition
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
set_pending
|
||||
>
|
||||
<Show when=move || stories.read().as_ref().map(Option::is_none).unwrap_or(false)>
|
||||
>
|
||||
<p>"Error loading stories."</p>
|
||||
</Show>
|
||||
<Transition fallback=move || view! { <p>"Loading..."</p> } set_pending>
|
||||
<Show when=move || {
|
||||
stories.read().as_ref().map(Option::is_none).unwrap_or(false)
|
||||
}>> <p>"Error loading stories."</p></Show>
|
||||
<ul>
|
||||
<For
|
||||
each=move || stories.get().unwrap_or_default().unwrap_or_default()
|
||||
@@ -105,54 +113,78 @@ pub fn Stories() -> impl IntoView {
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Story(story: api::Story) -> impl IntoView {
|
||||
view! {
|
||||
<li class="news-item">
|
||||
<li class="news-item">
|
||||
<span class="score">{story.points}</span>
|
||||
<span class="title">
|
||||
{if !story.url.starts_with("item?id=") {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
</span>
|
||||
})
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"(" {story.domain} ")"</span>
|
||||
</span>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
Either::Right(view! { <A href=format!("/stories/{}", story.id)>{title}</A> })
|
||||
}}
|
||||
|
||||
</span>
|
||||
<br />
|
||||
<br/>
|
||||
<span class="meta">
|
||||
{if story.story_type != "job" {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
</A>
|
||||
</span>
|
||||
})
|
||||
Either::Left(
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
}
|
||||
})} {format!(" {} | ", story.time_ago)}
|
||||
<A href=format!(
|
||||
"/stories/{}",
|
||||
story.id,
|
||||
)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!(
|
||||
"{} comments",
|
||||
story.comments_count.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
|
||||
</A>
|
||||
</span>
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
Either::Right(view! { <A href=format!("/item/{}", story.id)>{title}</A> })
|
||||
}}
|
||||
|
||||
</span>
|
||||
{(story.story_type != "link").then(|| view! {
|
||||
" "
|
||||
<span class="label">{story.story_type}</span>
|
||||
})}
|
||||
{(story.story_type != "link")
|
||||
.then(|| {
|
||||
view! {
|
||||
" "
|
||||
<span class="label">{story.story_type}</span>
|
||||
}
|
||||
})}
|
||||
|
||||
</li>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -28,18 +28,21 @@ pub fn Story() -> impl IntoView {
|
||||
<Meta name="description" content=story.title.clone()/>
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">"(" {story.domain} ")"</span>
|
||||
{story
|
||||
.user
|
||||
.map(|user| {
|
||||
view! {
|
||||
<p class="meta">
|
||||
{story.points} " points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
@@ -48,6 +51,7 @@ pub fn Story() -> impl IntoView {
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
@@ -55,7 +59,7 @@ pub fn Story() -> impl IntoView {
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment />
|
||||
<Comment comment/>
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -64,6 +68,7 @@ pub fn Story() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
}))).build())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -72,43 +77,65 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<li class="comment">
|
||||
<div class="by">
|
||||
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html=comment.content></div>
|
||||
{(!comment.comments.is_empty()).then(|| {
|
||||
view! {
|
||||
<div>
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| set_open.update(|n| *n = !*n)>
|
||||
{
|
||||
let comments_len = comment.comments.len();
|
||||
move || if open.get() {
|
||||
"[-]".into()
|
||||
} else {
|
||||
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
|
||||
}
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
{move || open.get().then({
|
||||
let comments = comment.comments.clone();
|
||||
move || view! {
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment />
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
<div class="by">
|
||||
<A href=format!(
|
||||
"/users/{}",
|
||||
comment.user.clone().unwrap_or_default(),
|
||||
)>{comment.user.clone()}</A>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html=comment.content></div>
|
||||
{(!comment.comments.is_empty())
|
||||
.then(|| {
|
||||
view! {
|
||||
<div>
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| {
|
||||
set_open.update(|n| *n = !*n)
|
||||
}>
|
||||
|
||||
{
|
||||
let comments_len = comment.comments.len();
|
||||
move || {
|
||||
if open.get() {
|
||||
"[-]".into()
|
||||
} else {
|
||||
format!(
|
||||
"[+] {}{} collapsed",
|
||||
comments_len,
|
||||
pluralize(comments_len),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</a>
|
||||
</div>
|
||||
{move || {
|
||||
open
|
||||
.get()
|
||||
.then({
|
||||
let comments = comment.comments.clone();
|
||||
move || {
|
||||
view! {
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || comments.clone()
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment/>
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
|
||||
</li>
|
||||
}.into_any()
|
||||
}
|
||||
|
||||
@@ -18,30 +18,48 @@ pub fn User() -> impl IntoView {
|
||||
);
|
||||
view! {
|
||||
<div class="user-view">
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
{move || Suspend::new(async move { match user.await.clone() {
|
||||
None => Either::Left(view! { <h1>"User not found."</h1> }),
|
||||
Some(user) => Either::Right(view! {
|
||||
<div>
|
||||
<h1>"User: " {user.id.clone()}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
<li inner_html={user.about} class="about"></li>
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
})
|
||||
}})}
|
||||
<Suspense fallback=|| {
|
||||
view! { "Loading..." }
|
||||
}>
|
||||
{move || Suspend::new(async move {
|
||||
match user.await.clone() {
|
||||
None => Either::Left(view! { <h1>"User not found."</h1> }),
|
||||
Some(user) => {
|
||||
Either::Right(
|
||||
view! {
|
||||
<div>
|
||||
<h1>"User: " {user.id.clone()}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span>
|
||||
{user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span>
|
||||
{user.karma}
|
||||
</li>
|
||||
<li inner_html=user.about class="about"></li>
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!(
|
||||
"https://news.ycombinator.com/submitted?id={}",
|
||||
user.id,
|
||||
)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!(
|
||||
"https://news.ycombinator.com/threads?id={}",
|
||||
user.id,
|
||||
)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ leptos = { path = "../../leptos", features = [
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
"islands-router",
|
||||
"dont-use-islands-router",
|
||||
], optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
# Leptos Todo App Sqlite with Axum
|
||||
# Work in Progress
|
||||
|
||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
This example is something I wrote on a long layover in the Orlando airport in July. (It was really hot!)
|
||||
|
||||
## Getting Started
|
||||
It is the culmination of a couple years of thinking and working toward being able to do this, which you can see
|
||||
described pretty well in the pinned roadmap issue (#1830) and its discussion of different modes of client-side
|
||||
routing when you use islands.
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
|
||||
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
|
||||
new HTML from the client, with extremely minimal diffing.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
|
||||
|
||||
## Rendering
|
||||
|
||||
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `cargo leptos watch` to run this example.
|
||||
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
|
||||
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use js_framework_benchmark_leptos::*;
|
||||
use leptos::{prelude::*, spawn::tick};
|
||||
use leptos::{prelude::*, task::tick};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use leptos::spawn::tick;
|
||||
use leptos::task::tick;
|
||||
use leptos::{leptos_dom::helpers::document, mount::mount_to};
|
||||
use web_sys::HtmlButtonElement;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<link data-trunk rel="css" href="style.css"/>
|
||||
<link data-trunk rel="css" href="style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
@@ -5,13 +5,14 @@ use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{
|
||||
Form, Outlet, ParentRoute, ProtectedRoute, Redirect, Route, Router,
|
||||
Routes, A,
|
||||
Routes, RoutingProgress, A,
|
||||
},
|
||||
hooks::{use_navigate, use_params, use_query_map},
|
||||
params::Params,
|
||||
MatchNestedRoutes,
|
||||
};
|
||||
use leptos_router_macro::path;
|
||||
use std::time::Duration;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -26,9 +27,14 @@ pub fn RouterExample() -> impl IntoView {
|
||||
|
||||
// this signal will be ued to set whether we are allowed to access a protected route
|
||||
let (logged_in, set_logged_in) = signal(true);
|
||||
let (is_routing, set_is_routing) = signal(false);
|
||||
|
||||
view! {
|
||||
<Router>
|
||||
<Router set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<div class="routing-progress">
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
|
||||
</div>
|
||||
<nav>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
// using <A> has two effects:
|
||||
@@ -44,7 +50,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
}>{move || if logged_in.get() { "Log Out" } else { "Log In" }}</button>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes fallback=|| "This page could not be found.">
|
||||
<Routes transition=true fallback=|| "This page could not be found.">
|
||||
// paths can be created using the path!() macro, or provided as types like
|
||||
// StaticSegment("about")
|
||||
<Route path=path!("about") view=About/>
|
||||
@@ -64,8 +70,8 @@ pub fn RouterExample() -> impl IntoView {
|
||||
|
||||
// You can define other routes in their own component.
|
||||
// Routes implement the MatchNestedRoutes
|
||||
#[component]
|
||||
pub fn ContactRoutes() -> impl MatchNestedRoutes<Dom> + Clone {
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("") view=ContactList>
|
||||
<Route path=path!("/") view=|| "Select a contact."/>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
.routing-progress {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
a[aria-current] {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -12,12 +17,8 @@ a[aria-current] {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
animation: 0.5s fadeIn forwards;
|
||||
}
|
||||
|
||||
.fadeOut {
|
||||
animation: 0.5s fadeOut forwards;
|
||||
.contact {
|
||||
view-transition-name: contact;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@@ -40,12 +41,44 @@ a[aria-current] {
|
||||
}
|
||||
}
|
||||
|
||||
.slideIn {
|
||||
animation: 0.25s slideIn forwards;
|
||||
.router-outlet-0 main {
|
||||
view-transition-name: main;
|
||||
}
|
||||
|
||||
.slideOut {
|
||||
animation: 0.25s slideOut forwards;
|
||||
.router-back main {
|
||||
view-transition-name: main-back;
|
||||
}
|
||||
|
||||
.router-outlet-1 .contact-list {
|
||||
view-transition-name: contact;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
::view-transition-old(contact) {
|
||||
animation: 0.5s fadeOut;
|
||||
}
|
||||
|
||||
::view-transition-new(contact) {
|
||||
animation: 0.5s fadeIn;
|
||||
}
|
||||
|
||||
::view-transition-old(main) {
|
||||
animation: 0.5s slideOut;
|
||||
}
|
||||
|
||||
::view-transition-new(main) {
|
||||
animation: 0.5s slideIn;
|
||||
}
|
||||
|
||||
::view-transition-old(main-back) {
|
||||
color: red;
|
||||
animation: 0.5s slideOutBack;
|
||||
}
|
||||
|
||||
::view-transition-new(main-back) {
|
||||
color: blue;
|
||||
animation: 0.5s slideInBack;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@@ -66,14 +99,6 @@ a[aria-current] {
|
||||
}
|
||||
}
|
||||
|
||||
.slideInBack {
|
||||
animation: 0.25s slideInBack forwards;
|
||||
}
|
||||
|
||||
.slideOutBack {
|
||||
animation: 0.25s slideOutBack forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInBack {
|
||||
from {
|
||||
transform: translate(-100vw, 0);
|
||||
|
||||
@@ -40,6 +40,8 @@ pin-project-lite = "0.2.14"
|
||||
dashmap = { version = "6.0", optional = true }
|
||||
once_cell = { version = "1.19", optional = true }
|
||||
async-broadcast = { version = "0.7.1", optional = true }
|
||||
bytecheck = "0.8.0"
|
||||
rkyv = { version = "0.8.8" }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use futures::StreamExt;
|
||||
use http::Method;
|
||||
use leptos::{html::Input, prelude::*, spawn::spawn_local};
|
||||
use leptos::{html::Input, prelude::*, task::spawn_local};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use server_fn::{
|
||||
client::{browser::BrowserClient, Client},
|
||||
@@ -417,7 +417,6 @@ pub fn FileUploadWithProgress() -> impl IntoView {
|
||||
/// This requires us to store some global state of all the uploads. In a real app, you probably
|
||||
/// shouldn't do exactly what I'm doing here in the demo. For example, this map just
|
||||
/// distinguishes between files by filename, not by user.
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod progress {
|
||||
use async_broadcast::{broadcast, Receiver, Sender};
|
||||
|
||||
@@ -4,6 +4,8 @@ use leptos::{config::get_configuration, logging};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use server_fns_axum::*;
|
||||
|
||||
// cargo make cli: error: unneeded `return` statement
|
||||
#[allow(clippy::needless_return)]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Error)
|
||||
|
||||
@@ -2,8 +2,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use chrono::{Local, NaiveDate};
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Field, Store};
|
||||
use reactive_stores_macro::Store;
|
||||
use reactive_stores::{Field, Patch, Store};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ID starts higher than 0 because we have a few starting todos by default
|
||||
@@ -11,11 +10,17 @@ static NEXT_ID: AtomicUsize = AtomicUsize::new(3);
|
||||
|
||||
#[derive(Debug, Store, Serialize, Deserialize)]
|
||||
struct Todos {
|
||||
user: String,
|
||||
user: User,
|
||||
#[store(key: usize = |todo| todo.id)]
|
||||
todos: Vec<Todo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Store, Patch, Serialize, Deserialize)]
|
||||
struct User {
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Store, Serialize, Deserialize)]
|
||||
struct Todo {
|
||||
id: usize,
|
||||
@@ -58,7 +63,10 @@ impl Todo {
|
||||
|
||||
fn data() -> Todos {
|
||||
Todos {
|
||||
user: "Bob".to_string(),
|
||||
user: User {
|
||||
name: "Bob".to_string(),
|
||||
email: "lawblog@bobloblaw.com".into(),
|
||||
},
|
||||
todos: vec![
|
||||
Todo {
|
||||
id: 0,
|
||||
@@ -86,7 +94,9 @@ pub fn App() -> impl IntoView {
|
||||
let input_ref = NodeRef::new();
|
||||
|
||||
view! {
|
||||
<p>"Hello, " {move || store.user().get()}</p>
|
||||
<p>"Hello, " {move || store.user().name().get()}</p>
|
||||
<UserForm user=store.user()/>
|
||||
<hr/>
|
||||
<form on:submit=move |ev| {
|
||||
ev.prevent_default();
|
||||
store.todos().write().push(Todo::new(input_ref.get().unwrap().value()));
|
||||
@@ -98,7 +108,15 @@ pub fn App() -> impl IntoView {
|
||||
// because `todos` is a keyed field, `store.todos()` returns a struct that
|
||||
// directly implements IntoIterator, so we can use it in <For/> and
|
||||
// it will manage reactivity for the store fields correctly
|
||||
<For each=move || store.todos() key=|row| row.id().get() let:todo>
|
||||
<For
|
||||
each=move || {
|
||||
leptos::logging::log!("RERUNNING FOR CALCULATION");
|
||||
store.todos()
|
||||
}
|
||||
|
||||
key=|row| row.id().get()
|
||||
let:todo
|
||||
>
|
||||
<TodoRow store todo/>
|
||||
</For>
|
||||
|
||||
@@ -107,6 +125,33 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn UserForm(#[prop(into)] user: Field<User>) -> impl IntoView {
|
||||
let error = RwSignal::new(None);
|
||||
|
||||
view! {
|
||||
{move || error.get().map(|n| view! { <p>{n}</p> })}
|
||||
<form on:submit:target=move |ev| {
|
||||
ev.prevent_default();
|
||||
match User::from_event(&ev) {
|
||||
Ok(new_user) => {
|
||||
error.set(None);
|
||||
user.patch(new_user);
|
||||
}
|
||||
Err(e) => error.set(Some(e.to_string())),
|
||||
}
|
||||
}>
|
||||
<label>
|
||||
"Name" <input type="text" name="name" prop:value=move || user.name().get()/>
|
||||
</label>
|
||||
<label>
|
||||
"Email" <input type="email" name="email" prop:value=move || user.email().get()/>
|
||||
</label>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TodoRow(
|
||||
store: Store<Todos>,
|
||||
|
||||
@@ -319,10 +319,7 @@ pub fn Todo(todo: Todo) -> impl IntoView {
|
||||
node_ref=todo_input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked=move || todo.completed.get()
|
||||
on:input:target=move |ev| {
|
||||
todo.completed.set(ev.target().checked());
|
||||
}
|
||||
bind:checked=todo.completed
|
||||
/>
|
||||
|
||||
<label on:dblclick=move |_| {
|
||||
|
||||
58
flake.lock
generated
58
flake.lock
generated
@@ -5,29 +5,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -38,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1703961334,
|
||||
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
|
||||
"lastModified": 1727634051,
|
||||
"narHash": "sha256-S5kVU7U82LfpEukbn/ihcyNt2+EvG7Z5unsKW9H/yFA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
|
||||
"rev": "06cf0e1da4208d3766d898b7fdab6513366d45b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -54,11 +36,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681358109,
|
||||
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
||||
"lastModified": 1718428119,
|
||||
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
||||
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -77,15 +59,14 @@
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1704075545,
|
||||
"narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
|
||||
"lastModified": 1727749966,
|
||||
"narHash": "sha256-DUS8ehzqB1DQzfZ4bRXVSollJhu+y7cvh1DJ9mbWebE=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
|
||||
"rev": "00decf1b4f9886d25030b9ee4aed7bfddccb5f66",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -108,21 +89,6 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.2.0-beta5"
|
||||
version = "0.2.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -33,7 +33,7 @@ once_cell = "1"
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
islands-router = []
|
||||
dont-use-islands-router = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -24,7 +24,7 @@ use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
prelude::expect_context,
|
||||
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
||||
reactive::{computed::ScopedFuture, owner::Owner},
|
||||
IntoView,
|
||||
};
|
||||
use leptos_integration_utils::{
|
||||
@@ -749,7 +749,7 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -1381,39 +1381,41 @@ where
|
||||
),
|
||||
)
|
||||
} else {
|
||||
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!()
|
||||
},
|
||||
)
|
||||
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!()
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ axum = { version = "0.7.5", default-features = false, features = [
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
http-body-util = "0.1.2"
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||
leptos_macro = { workspace = true, features = ["axum"] }
|
||||
@@ -26,9 +24,8 @@ 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 = "0.4.13"
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
tower-http = "0.5.2"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
|
||||
@@ -39,7 +36,7 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
|
||||
[features]
|
||||
wasm = []
|
||||
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
|
||||
islands-router = []
|
||||
dont-use-islands-router = []
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -53,7 +53,7 @@ use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
prelude::*,
|
||||
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
||||
reactive::{computed::ScopedFuture, owner::Owner},
|
||||
IntoView,
|
||||
};
|
||||
use leptos_integration_utils::{
|
||||
@@ -76,7 +76,7 @@ use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
||||
use std::path::Path;
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
#[cfg(feature = "default")]
|
||||
use tower::ServiceExt;
|
||||
use tower::util::ServiceExt;
|
||||
#[cfg(feature = "default")]
|
||||
use tower_http::services::ServeDir;
|
||||
// use tracing::Instrument; // TODO check tracing span -- was this used in 0.6 for a missing link?
|
||||
@@ -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(
|
||||
@@ -784,7 +784,7 @@ where
|
||||
_ = replace_blocks; // TODO
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_out_of_order()
|
||||
@@ -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(
|
||||
@@ -849,7 +849,7 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -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(
|
||||
@@ -1069,7 +1069,7 @@ where
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -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(
|
||||
@@ -1146,7 +1146,7 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -1342,8 +1342,7 @@ where
|
||||
.with(|| {
|
||||
// stub out a path for now
|
||||
provide_context(RequestUrl::new(""));
|
||||
let (mock_parts, _) =
|
||||
http::Request::new(Body::from("")).into_parts();
|
||||
let (mock_parts, _) = Request::new(Body::from("")).into_parts();
|
||||
let (mock_meta, _) = ServerMetaContext::new();
|
||||
provide_contexts("", &mock_meta, mock_parts, Default::default());
|
||||
additional_context();
|
||||
@@ -1402,8 +1401,8 @@ impl StaticRouteGenerator {
|
||||
let add_context = additional_context.clone();
|
||||
move || {
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
let mock_req = http::Request::builder()
|
||||
.method(http::Method::GET)
|
||||
let mock_req = Request::builder()
|
||||
.method(Method::GET)
|
||||
.header("Accept", "text/html")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
@@ -1495,10 +1494,12 @@ impl StaticRouteGenerator {
|
||||
_ = routes;
|
||||
_ = app_fn;
|
||||
_ = additional_context;
|
||||
panic!(
|
||||
"Static routes are not currently supported on WASM32 server \
|
||||
targets."
|
||||
);
|
||||
Self(Box::new(|_| {
|
||||
panic!(
|
||||
"Static routes are not currently supported on WASM32 \
|
||||
server targets."
|
||||
);
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1933,7 +1934,7 @@ where
|
||||
///
|
||||
/// #[server]
|
||||
/// pub async fn request_method() -> Result<String, ServerFnError> {
|
||||
/// use http::Method;
|
||||
/// use axum::http::Method;
|
||||
/// use leptos_axum::extract;
|
||||
///
|
||||
/// // you can extract anything that a regular Axum extractor can extract
|
||||
@@ -1992,7 +1993,7 @@ where
|
||||
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
|
||||
Box::pin(async move {
|
||||
let options = LeptosOptions::from_ref(&options);
|
||||
let res = get_static_file(uri, &options.site_root);
|
||||
let res = get_static_file(uri, &options.site_root, req.headers());
|
||||
let res = res.await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
@@ -2026,14 +2027,26 @@ where
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
headers: &HeaderMap<HeaderValue>,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
use axum::http::header::ACCEPT_ENCODING;
|
||||
|
||||
let req = Request::builder().uri(uri);
|
||||
|
||||
let req = match headers.get(ACCEPT_ENCODING) {
|
||||
Some(value) => req.header(ACCEPT_ENCODING, value),
|
||||
None => req,
|
||||
};
|
||||
|
||||
let req = req.body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
match ServeDir::new(root)
|
||||
.precompressed_gzip()
|
||||
.precompressed_br()
|
||||
.oneshot(req)
|
||||
.await
|
||||
{
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -2,7 +2,7 @@ use futures::{stream::once, Stream, StreamExt};
|
||||
use hydration_context::{SharedContext, SsrSharedContext};
|
||||
use leptos::{
|
||||
nonce::use_nonce,
|
||||
reactive_graph::owner::{Owner, Sandboxed},
|
||||
reactive::owner::{Owner, Sandboxed},
|
||||
IntoView,
|
||||
};
|
||||
use leptos_config::LeptosOptions;
|
||||
|
||||
@@ -28,7 +28,7 @@ paste = "1.0"
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
reactive_graph = { workspace = true, features = ["serde"] }
|
||||
rustc-hash = "2.0"
|
||||
tachys = { workspace = true, features = ["reactive_graph", "oco"] }
|
||||
tachys = { workspace = true, features = ["reactive_graph", "reactive_stores", "oco"] }
|
||||
thiserror = "1.0"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
typed-builder = "0.19.1"
|
||||
@@ -45,7 +45,7 @@ web-sys = { version = "0.3.70", features = [
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "=0.2.93"
|
||||
serde_qs = "0.13.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.30"
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use crate::TextProp;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A collection of additional HTML attributes to be applied to an element,
|
||||
/// each of which may or may not be reactive.
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
#[deprecated = "Most uses of `AdditionalAttributes` can be replaced with `#[prop(attrs)]` \
|
||||
and the `attr:` syntax. If you have a use case that still requires `AdditionalAttributes`, please \
|
||||
open a GitHub issue here and share it: https://github.com/leptos-rs/leptos"]
|
||||
pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>);
|
||||
|
||||
impl<I, T, U> From<I> for AdditionalAttributes
|
||||
where
|
||||
I: IntoIterator<Item = (T, U)>,
|
||||
T: Into<String>,
|
||||
U: Into<TextProp>,
|
||||
{
|
||||
fn from(value: I) -> Self {
|
||||
Self(
|
||||
value
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.into(), v.into()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AdditionalAttributes {
|
||||
fn default() -> Self {
|
||||
Self([].into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over additional HTML attributes.
|
||||
#[repr(transparent)]
|
||||
pub struct AdditionalAttributesIter<'a>(
|
||||
std::slice::Iter<'a, (String, TextProp)>,
|
||||
);
|
||||
|
||||
impl<'a> Iterator for AdditionalAttributesIter<'a> {
|
||||
type Item = &'a (String, TextProp);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a AdditionalAttributes {
|
||||
type Item = &'a (String, TextProp);
|
||||
type IntoIter = AdditionalAttributesIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
AdditionalAttributesIter(self.0.iter())
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
use crate::{ChildrenFn, Show};
|
||||
use crate::{children::ChildrenFn, component, control_flow::Show, IntoView};
|
||||
use core::time::Duration;
|
||||
use leptos::component;
|
||||
use leptos_dom::{helpers::TimeoutHandle, IntoView};
|
||||
use leptos_dom::helpers::TimeoutHandle;
|
||||
use leptos_macro::view;
|
||||
use leptos_reactive::{
|
||||
create_render_effect, on_cleanup, signal_prelude::*, store_value,
|
||||
StoredValue,
|
||||
use reactive_graph::{
|
||||
effect::RenderEffect,
|
||||
owner::{on_cleanup, StoredValue},
|
||||
signal::RwSignal,
|
||||
traits::{Get, GetUntracked, GetValue, Set, SetValue},
|
||||
wrappers::read::Signal,
|
||||
};
|
||||
use tachys::prelude::*;
|
||||
|
||||
/// A component that will show its children when the `when` condition is `true`.
|
||||
/// Additionally, you need to specify a `hide_delay`. If the `when` condition changes to `false`,
|
||||
@@ -16,10 +19,10 @@ use leptos_reactive::{
|
||||
///
|
||||
/// ```rust
|
||||
/// # use core::time::Duration;
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// # #[component]
|
||||
/// # pub fn App() -> impl IntoView {
|
||||
/// let show = create_rw_signal(false);
|
||||
/// let show = RwSignal::new(false);
|
||||
///
|
||||
/// view! {
|
||||
/// <div
|
||||
@@ -50,7 +53,7 @@ pub fn AnimatedShow(
|
||||
children: ChildrenFn,
|
||||
/// If the component should show or not
|
||||
#[prop(into)]
|
||||
when: MaybeSignal<bool>,
|
||||
when: Signal<bool>,
|
||||
/// Optional CSS class to apply if `when == true`
|
||||
#[prop(optional)]
|
||||
show_class: &'static str,
|
||||
@@ -60,15 +63,15 @@ pub fn AnimatedShow(
|
||||
/// The timeout after which the component will be unmounted if `when == false`
|
||||
hide_delay: Duration,
|
||||
) -> impl IntoView {
|
||||
let handle: StoredValue<Option<TimeoutHandle>> = store_value(None);
|
||||
let cls = create_rw_signal(if when.get_untracked() {
|
||||
let handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
|
||||
let cls = RwSignal::new(if when.get_untracked() {
|
||||
show_class
|
||||
} else {
|
||||
hide_class
|
||||
});
|
||||
let show = create_rw_signal(when.get_untracked());
|
||||
let show = RwSignal::new(when.get_untracked());
|
||||
|
||||
create_render_effect(move |_| {
|
||||
let eff = RenderEffect::new(move |_| {
|
||||
if when.get() {
|
||||
// clear any possibly active timer
|
||||
if let Some(h) = handle.get_value() {
|
||||
@@ -93,6 +96,7 @@ pub fn AnimatedShow(
|
||||
if let Some(Some(h)) = handle.try_get_value() {
|
||||
h.clear();
|
||||
}
|
||||
drop(eff);
|
||||
});
|
||||
|
||||
view! {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use crate::Suspense;
|
||||
use leptos_dom::IntoView;
|
||||
use crate::{prelude::Suspend, suspense_component::Suspense, IntoView};
|
||||
use leptos_macro::{component, view};
|
||||
use leptos_reactive::{
|
||||
create_blocking_resource, create_local_resource, create_resource,
|
||||
store_value, Serializable,
|
||||
};
|
||||
use leptos_server::ArcOnceResource;
|
||||
use reactive_graph::prelude::ReadUntracked;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
#[component]
|
||||
/// Allows you to inline the data loading for an `async` block or
|
||||
@@ -15,11 +13,8 @@ use leptos_reactive::{
|
||||
/// Adding `let:{variable name}` to the props makes the data available in the children
|
||||
/// that variable name, when resolved.
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_macro::*;
|
||||
/// # use leptos_dom::*; use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// # if false {
|
||||
/// # let runtime = create_runtime();
|
||||
/// async fn fetch_monkeys(monkey: i32) -> i32 {
|
||||
/// // do some expensive work
|
||||
/// 3
|
||||
@@ -27,29 +22,23 @@ use leptos_reactive::{
|
||||
///
|
||||
/// view! {
|
||||
/// <Await
|
||||
/// future=|| fetch_monkeys(3)
|
||||
/// future=fetch_monkeys(3)
|
||||
/// let:data
|
||||
/// >
|
||||
/// <p>{*data} " little monkeys, jumping on the bed."</p>
|
||||
/// </Await>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn Await<T, Fut, FF, VF, V>(
|
||||
/// A function that returns the [`Future`](std::future::Future) that
|
||||
/// will the component will `.await` before rendering.
|
||||
future: FF,
|
||||
/// If `true`, the component will use [`create_blocking_resource`], preventing
|
||||
pub fn Await<T, Fut, Chil, V>(
|
||||
/// A [`Future`](std::future::Future) that will the component will `.await`
|
||||
/// before rendering.
|
||||
future: Fut,
|
||||
/// If `true`, the component will create a blocking resource, preventing
|
||||
/// the HTML stream from returning anything before `future` has resolved.
|
||||
#[prop(optional)]
|
||||
blocking: bool,
|
||||
/// If `true`, the component will use [`create_local_resource`], this will
|
||||
/// always run on the local system and therefore its result type does not
|
||||
/// need to be `Serializable`.
|
||||
#[prop(optional)]
|
||||
local: bool,
|
||||
/// A function that takes a reference to the resolved data from the `future`
|
||||
/// renders a view.
|
||||
///
|
||||
@@ -58,65 +47,58 @@ pub fn Await<T, Fut, FF, VF, V>(
|
||||
/// `let:` syntax to specify the name for the data variable.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// # if false {
|
||||
/// # let runtime = create_runtime();
|
||||
/// # async fn fetch_monkeys(monkey: i32) -> i32 {
|
||||
/// # 3
|
||||
/// # }
|
||||
/// view! {
|
||||
/// <Await
|
||||
/// future=|| fetch_monkeys(3)
|
||||
/// future=fetch_monkeys(3)
|
||||
/// let:data
|
||||
/// >
|
||||
/// <p>{*data} " little monkeys, jumping on the bed."</p>
|
||||
/// </Await>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
/// is the same as
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// # if false {
|
||||
/// # let runtime = create_runtime();
|
||||
/// # async fn fetch_monkeys(monkey: i32) -> i32 {
|
||||
/// # 3
|
||||
/// # }
|
||||
/// view! {
|
||||
/// <Await
|
||||
/// future=|| fetch_monkeys(3)
|
||||
/// future=fetch_monkeys(3)
|
||||
/// children=|data| view! {
|
||||
/// <p>{*data} " little monkeys, jumping on the bed."</p>
|
||||
/// }
|
||||
/// />
|
||||
/// }
|
||||
/// # ;
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
children: VF,
|
||||
children: Chil,
|
||||
) -> impl IntoView
|
||||
where
|
||||
Fut: std::future::Future<Output = T> + 'static,
|
||||
FF: Fn() -> Fut + 'static,
|
||||
V: IntoView,
|
||||
VF: Fn(&T) -> V + 'static,
|
||||
T: Serializable + 'static,
|
||||
T: Send + Sync + Serialize + DeserializeOwned + 'static,
|
||||
Fut: std::future::Future<Output = T> + Send + 'static,
|
||||
Chil: FnOnce(&T) -> V + Send + 'static,
|
||||
V: IntoView + 'static,
|
||||
{
|
||||
let res = if blocking {
|
||||
create_blocking_resource(|| (), move |_| future())
|
||||
} else if local {
|
||||
create_local_resource(|| (), move |_| future())
|
||||
} else {
|
||||
create_resource(|| (), move |_| future())
|
||||
};
|
||||
let view = store_value(children);
|
||||
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
|
||||
let ready = res.ready();
|
||||
|
||||
view! {
|
||||
<Suspense fallback=|| ()>
|
||||
{move || res.map(|data| view.with_value(|view| view(data)))}
|
||||
{Suspend::new(async move {
|
||||
ready.await;
|
||||
children(res.read_untracked().as_ref().unwrap())
|
||||
})}
|
||||
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@
|
||||
//!
|
||||
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
|
||||
|
||||
use reactive_graph::owner::{LocalStorage, StoredValue};
|
||||
use reactive_graph::{
|
||||
owner::{LocalStorage, StoredValue},
|
||||
traits::WithValue,
|
||||
};
|
||||
use std::{fmt, rc::Rc, sync::Arc};
|
||||
|
||||
/// A wrapper trait for calling callbacks.
|
||||
|
||||
@@ -3,13 +3,10 @@ use std::{
|
||||
fmt::{self, Debug},
|
||||
sync::Arc,
|
||||
};
|
||||
use tachys::{
|
||||
renderer::dom::Dom,
|
||||
view::{
|
||||
any_view::{AnyView, IntoAny},
|
||||
fragment::{Fragment, IntoFragment},
|
||||
RenderHtml,
|
||||
},
|
||||
use tachys::view::{
|
||||
any_view::{AnyView, IntoAny},
|
||||
fragment::{Fragment, IntoFragment},
|
||||
RenderHtml,
|
||||
};
|
||||
|
||||
/// The most common type for the `children` property on components,
|
||||
@@ -17,31 +14,31 @@ use tachys::{
|
||||
///
|
||||
/// This does not support iterating over individual nodes within the children.
|
||||
/// To iterate over children, use [`ChildrenFragment`].
|
||||
pub type Children = Box<dyn FnOnce() -> AnyView<Dom> + Send>;
|
||||
pub type Children = Box<dyn FnOnce() -> AnyView + Send>;
|
||||
|
||||
/// A type for the `children` property on components that can be called only once,
|
||||
/// and provides a collection of all the children passed to this component.
|
||||
pub type ChildrenFragment = Box<dyn FnOnce() -> Fragment<Dom> + Send>;
|
||||
pub type ChildrenFragment = Box<dyn FnOnce() -> Fragment + Send>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once.
|
||||
pub type ChildrenFn = Arc<dyn Fn() -> AnyView<Dom> + Send + Sync>;
|
||||
pub type ChildrenFn = Arc<dyn Fn() -> AnyView + Send + Sync>;
|
||||
|
||||
/// A type for the `children` property on components that can be called more than once,
|
||||
/// and provides a collection of all the children passed to this component.
|
||||
pub type ChildrenFragmentFn = Arc<dyn Fn() -> Fragment<Dom> + Send>;
|
||||
pub type ChildrenFragmentFn = Arc<dyn Fn() -> Fragment + Send>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once, but may mutate the children.
|
||||
pub type ChildrenFnMut = Box<dyn FnMut() -> AnyView<Dom> + Send>;
|
||||
pub type ChildrenFnMut = Box<dyn FnMut() -> AnyView + Send>;
|
||||
|
||||
/// A type for the `children` property on components that can be called more than once,
|
||||
/// but may mutate the children, and provides a collection of all the children
|
||||
/// passed to this component.
|
||||
pub type ChildrenFragmentMut = Box<dyn FnMut() -> Fragment<Dom> + Send>;
|
||||
pub type ChildrenFragmentMut = Box<dyn FnMut() -> Fragment + Send>;
|
||||
|
||||
// This is to still support components that accept `Box<dyn Fn() -> AnyView>` as a children.
|
||||
type BoxedChildrenFn = Box<dyn Fn() -> AnyView<Dom> + Send>;
|
||||
type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
|
||||
|
||||
/// This trait can be used when constructing a component that takes children without needing
|
||||
/// to know exactly what children type the component expects. This is used internally by the
|
||||
@@ -97,7 +94,7 @@ pub trait ToChildren<F> {
|
||||
impl<F, C> ToChildren<F> for Children
|
||||
where
|
||||
F: FnOnce() -> C + Send + 'static,
|
||||
C: RenderHtml<Dom> + Send + 'static,
|
||||
C: RenderHtml + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
@@ -108,7 +105,7 @@ where
|
||||
impl<F, C> ToChildren<F> for ChildrenFn
|
||||
where
|
||||
F: Fn() -> C + Send + Sync + 'static,
|
||||
C: RenderHtml<Dom> + Send + 'static,
|
||||
C: RenderHtml + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
@@ -119,7 +116,7 @@ where
|
||||
impl<F, C> ToChildren<F> for ChildrenFnMut
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
C: RenderHtml<Dom> + Send + 'static,
|
||||
C: RenderHtml + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
@@ -130,7 +127,7 @@ where
|
||||
impl<F, C> ToChildren<F> for BoxedChildrenFn
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
C: RenderHtml<Dom> + Send + 'static,
|
||||
C: RenderHtml + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
@@ -141,7 +138,7 @@ where
|
||||
impl<F, C> ToChildren<F> for ChildrenFragment
|
||||
where
|
||||
F: FnOnce() -> C + Send + 'static,
|
||||
C: IntoFragment<Dom>,
|
||||
C: IntoFragment,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
@@ -152,7 +149,7 @@ where
|
||||
impl<F, C> ToChildren<F> for ChildrenFragmentFn
|
||||
where
|
||||
F: Fn() -> C + Send + 'static,
|
||||
C: IntoFragment<Dom>,
|
||||
C: IntoFragment,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(f: F) -> Self {
|
||||
@@ -163,7 +160,7 @@ where
|
||||
impl<F, C> ToChildren<F> for ChildrenFragmentMut
|
||||
where
|
||||
F: FnMut() -> C + Send + 'static,
|
||||
C: IntoFragment<Dom>,
|
||||
C: IntoFragment,
|
||||
{
|
||||
#[inline]
|
||||
fn to_children(mut f: F) -> Self {
|
||||
@@ -174,7 +171,7 @@ where
|
||||
/// New-type wrapper for 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(Arc<dyn Fn() -> AnyView<Dom> + Send + Sync + 'static>);
|
||||
pub struct ViewFn(Arc<dyn Fn() -> AnyView + Send + Sync + 'static>);
|
||||
|
||||
impl Default for ViewFn {
|
||||
fn default() -> Self {
|
||||
@@ -185,7 +182,7 @@ impl Default for ViewFn {
|
||||
impl<F, C> From<F> for ViewFn
|
||||
where
|
||||
F: Fn() -> C + Send + Sync + 'static,
|
||||
C: RenderHtml<Dom> + Send + 'static,
|
||||
C: RenderHtml + Send + 'static,
|
||||
{
|
||||
fn from(value: F) -> Self {
|
||||
Self(Arc::new(move || value().into_any()))
|
||||
@@ -194,14 +191,14 @@ where
|
||||
|
||||
impl ViewFn {
|
||||
/// Execute the wrapped function
|
||||
pub fn run(&self) -> AnyView<Dom> {
|
||||
pub fn run(&self) -> AnyView {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
|
||||
/// New-type wrapper for a function, which will only be called once and returns a view with `From` and
|
||||
/// `Default` traits implemented to enable optional props in for example `<Show>` and `<Suspense>`.
|
||||
pub struct ViewFnOnce(Box<dyn FnOnce() -> AnyView<Dom> + Send + 'static>);
|
||||
pub struct ViewFnOnce(Box<dyn FnOnce() -> AnyView + Send + 'static>);
|
||||
|
||||
impl Default for ViewFnOnce {
|
||||
fn default() -> Self {
|
||||
@@ -212,7 +209,7 @@ impl Default for ViewFnOnce {
|
||||
impl<F, C> From<F> for ViewFnOnce
|
||||
where
|
||||
F: FnOnce() -> C + Send + 'static,
|
||||
C: RenderHtml<Dom> + Send + 'static,
|
||||
C: RenderHtml + Send + 'static,
|
||||
{
|
||||
fn from(value: F) -> Self {
|
||||
Self(Box::new(move || value().into_any()))
|
||||
@@ -221,7 +218,7 @@ where
|
||||
|
||||
impl ViewFnOnce {
|
||||
/// Execute the wrapped function
|
||||
pub fn run(self) -> AnyView<Dom> {
|
||||
pub fn run(self) -> AnyView {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ use reactive_graph::{
|
||||
traits::{Get, Update, With, WithUntracked},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
reactive_graph::OwnedView,
|
||||
renderer::Renderer,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
@@ -112,20 +111,18 @@ where
|
||||
children,
|
||||
errors,
|
||||
fallback,
|
||||
rndr: PhantomData,
|
||||
},
|
||||
owner,
|
||||
)
|
||||
}
|
||||
|
||||
struct ErrorBoundaryView<Chil, FalFn, Rndr> {
|
||||
struct ErrorBoundaryView<Chil, FalFn> {
|
||||
hook: Arc<dyn ErrorHook>,
|
||||
boundary_id: SerializedDataId,
|
||||
errors_empty: ArcMemo<bool>,
|
||||
children: Chil,
|
||||
fallback: FalFn,
|
||||
errors: ArcRwSignal<Errors>,
|
||||
rndr: PhantomData<Rndr>,
|
||||
}
|
||||
|
||||
struct ErrorBoundaryViewState<Chil, Fal> {
|
||||
@@ -134,11 +131,10 @@ struct ErrorBoundaryViewState<Chil, Fal> {
|
||||
fallback: Option<Fal>,
|
||||
}
|
||||
|
||||
impl<Chil, Fal, Rndr> Mountable<Rndr> for ErrorBoundaryViewState<Chil, Fal>
|
||||
impl<Chil, Fal> Mountable for ErrorBoundaryViewState<Chil, Fal>
|
||||
where
|
||||
Chil: Mountable<Rndr>,
|
||||
Fal: Mountable<Rndr>,
|
||||
Rndr: Renderer,
|
||||
Chil: Mountable,
|
||||
Fal: Mountable,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
if let Some(fallback) = &mut self.fallback {
|
||||
@@ -148,7 +144,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn mount(&mut self, parent: &Rndr::Element, marker: Option<&Rndr::Node>) {
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &tachys::renderer::types::Element,
|
||||
marker: Option<&tachys::renderer::types::Node>,
|
||||
) {
|
||||
if let Some(fallback) = &mut self.fallback {
|
||||
fallback.mount(parent, marker);
|
||||
} else {
|
||||
@@ -156,7 +156,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<Rndr>) -> bool {
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
|
||||
if let Some(fallback) = &self.fallback {
|
||||
fallback.insert_before_this(child)
|
||||
} else {
|
||||
@@ -165,13 +165,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Chil, FalFn, Fal, Rndr> Render<Rndr>
|
||||
for ErrorBoundaryView<Chil, FalFn, Rndr>
|
||||
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
|
||||
where
|
||||
Chil: Render<Rndr> + 'static,
|
||||
Chil: Render + 'static,
|
||||
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
|
||||
Fal: Render<Rndr> + 'static,
|
||||
Rndr: Renderer,
|
||||
Fal: Render + 'static,
|
||||
{
|
||||
type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
|
||||
|
||||
@@ -228,26 +226,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<Chil, FalFn, Fal, Rndr> AddAnyAttr<Rndr>
|
||||
for ErrorBoundaryView<Chil, FalFn, Rndr>
|
||||
impl<Chil, FalFn, Fal> AddAnyAttr for ErrorBoundaryView<Chil, FalFn>
|
||||
where
|
||||
Chil: RenderHtml<Rndr> + 'static,
|
||||
Chil: RenderHtml + 'static,
|
||||
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
|
||||
Fal: RenderHtml<Rndr> + Send + 'static,
|
||||
Rndr: Renderer,
|
||||
Fal: RenderHtml + Send + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> = ErrorBoundaryView<
|
||||
Chil::Output<SomeNewAttr::CloneableOwned>,
|
||||
FalFn,
|
||||
Rndr,
|
||||
>;
|
||||
type Output<SomeNewAttr: Attribute> =
|
||||
ErrorBoundaryView<Chil::Output<SomeNewAttr::CloneableOwned>, FalFn>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
let ErrorBoundaryView {
|
||||
hook,
|
||||
@@ -256,7 +249,6 @@ where
|
||||
children,
|
||||
fallback,
|
||||
errors,
|
||||
rndr,
|
||||
} = self;
|
||||
ErrorBoundaryView {
|
||||
hook,
|
||||
@@ -265,20 +257,17 @@ where
|
||||
children: children.add_any_attr(attr.into_cloneable_owned()),
|
||||
fallback,
|
||||
errors,
|
||||
rndr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Chil, FalFn, Fal, Rndr> RenderHtml<Rndr>
|
||||
for ErrorBoundaryView<Chil, FalFn, Rndr>
|
||||
impl<Chil, FalFn, Fal> RenderHtml for ErrorBoundaryView<Chil, FalFn>
|
||||
where
|
||||
Chil: RenderHtml<Rndr> + Send + 'static,
|
||||
Chil: RenderHtml + Send + 'static,
|
||||
FalFn: FnMut(ArcRwSignal<Errors>) -> Fal + Send + 'static,
|
||||
Fal: RenderHtml<Rndr> + Send + 'static,
|
||||
Rndr: Renderer,
|
||||
Fal: RenderHtml + Send + 'static,
|
||||
{
|
||||
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn, Rndr>;
|
||||
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
|
||||
|
||||
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
|
||||
|
||||
@@ -303,7 +292,6 @@ where
|
||||
children: children.resolve().await,
|
||||
fallback,
|
||||
errors,
|
||||
rndr: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +365,7 @@ where
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
mut self,
|
||||
cursor: &Cursor<Rndr>,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
let mut children = Some(self.children);
|
||||
|
||||
@@ -157,7 +157,7 @@ where
|
||||
};
|
||||
move || keyed(each(), key.clone(), children.clone())
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
@@ -168,7 +168,7 @@ mod tests {
|
||||
fn creates_list() {
|
||||
Owner::new().with(|| {
|
||||
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
let list: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
let list: View<HtmlElement<_, _, _>> = view! {
|
||||
<ol>
|
||||
<For each=move || values.get() key=|i| *i let:i>
|
||||
<li>{i}</li>
|
||||
@@ -187,7 +187,7 @@ mod tests {
|
||||
fn creates_list_enumerate() {
|
||||
Owner::new().with(|| {
|
||||
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
let list: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
let list: View<HtmlElement<_, _, _>> = view! {
|
||||
<ol>
|
||||
<ForEnumerate each=move || values.get() key=|i| *i let(index, i)>
|
||||
<li>{move || index.get()}"-"{i}</li>
|
||||
@@ -200,7 +200,7 @@ mod tests {
|
||||
<!>-<!>4</li><li>4<!>-<!>5</li><!></ol>"
|
||||
);
|
||||
|
||||
let list: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
let list: View<HtmlElement<_, _, _>> = view! {
|
||||
<ol>
|
||||
<ForEnumerate each=move || values.get() key=|i| *i let(index, i)>
|
||||
<li>{move || index.get()}"-"{i}</li>
|
||||
@@ -216,3 +216,4 @@ mod tests {
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
(function (root, pkg_path, output_name, wasm_output_name) {
|
||||
import(`${root}/${pkg_path}/${output_name}.js`)
|
||||
.then(mod => {
|
||||
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
|
||||
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
|
||||
mod.hydrate();
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
if (islandFn) {
|
||||
islandFn(el);
|
||||
} else {
|
||||
console.warn(`Could not find WASM function for the island ${l}.`);
|
||||
console.warn(`Could not find WASM function for the island ${id}.`);
|
||||
}
|
||||
}
|
||||
function hydrateIslands(entry, mod) {
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::borrow::Cow;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
|
||||
@@ -50,14 +49,14 @@ impl<T> View<T> {
|
||||
|
||||
pub trait IntoView
|
||||
where
|
||||
Self: Sized + Render<Dom> + RenderHtml<Dom> + Send,
|
||||
Self: Sized + Render + RenderHtml + Send,
|
||||
{
|
||||
fn into_view(self) -> View<Self>;
|
||||
}
|
||||
|
||||
impl<T> IntoView for T
|
||||
where
|
||||
T: Sized + Render<Dom> + RenderHtml<Dom> + Send, //+ AddAnyAttr<Dom>,
|
||||
T: Sized + Render + RenderHtml + Send, //+ AddAnyAttr,
|
||||
{
|
||||
fn into_view(self) -> View<Self> {
|
||||
View {
|
||||
@@ -68,7 +67,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Render<Rndr>, Rndr: Renderer> Render<Rndr> for View<T> {
|
||||
impl<T: Render> Render for View<T> {
|
||||
type State = T::State;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
@@ -80,10 +79,10 @@ impl<T: Render<Rndr>, Rndr: Renderer> Render<Rndr> for View<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RenderHtml<Rndr>, Rndr: Renderer> RenderHtml<Rndr> for View<T> {
|
||||
impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
type AsyncOutput = T::AsyncOutput;
|
||||
|
||||
const MIN_LENGTH: usize = <T as RenderHtml<Rndr>>::MIN_LENGTH;
|
||||
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self.inner.resolve().await
|
||||
@@ -147,7 +146,7 @@ impl<T: RenderHtml<Rndr>, Rndr: Renderer> RenderHtml<Rndr> for View<T> {
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Rndr>,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
self.inner.hydrate::<FROM_SERVER>(cursor, position)
|
||||
@@ -166,18 +165,15 @@ impl<T: ToTemplate> ToTemplate for View<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AddAnyAttr<Rndr>, Rndr> AddAnyAttr<Rndr> for View<T>
|
||||
where
|
||||
Rndr: Renderer,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> = View<T::Output<SomeNewAttr>>;
|
||||
impl<T: AddAnyAttr> AddAnyAttr for View<T> {
|
||||
type Output<SomeNewAttr: Attribute> = View<T::Output<SomeNewAttr>>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
let View {
|
||||
inner,
|
||||
|
||||
@@ -168,14 +168,15 @@ pub mod prelude {
|
||||
pub use leptos_server::*;
|
||||
pub use oco_ref::*;
|
||||
pub use reactive_graph::{
|
||||
actions::*, computed::*, effect::*, owner::*, signal::*, untrack,
|
||||
wrappers::read::*,
|
||||
actions::*, computed::*, effect::*, graph::untrack, owner::*,
|
||||
signal::*, wrappers::read::*,
|
||||
};
|
||||
pub use server_fn::{self, ServerFnError};
|
||||
pub use tachys::{
|
||||
self,
|
||||
reactive_graph::{node_ref::*, Suspend},
|
||||
view::template::ViewTemplate,
|
||||
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
|
||||
view::{
|
||||
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
|
||||
},
|
||||
};
|
||||
}
|
||||
pub use export_types::*;
|
||||
@@ -201,10 +202,12 @@ pub mod error {
|
||||
pub use throw_error::*;
|
||||
}
|
||||
|
||||
/// Control-flow components like `<Show>` and `<For>`.
|
||||
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
|
||||
pub mod control_flow {
|
||||
pub use crate::{for_loop::*, show::*};
|
||||
pub use crate::{animated_show::*, await_::*, for_loop::*, show::*};
|
||||
}
|
||||
mod animated_show;
|
||||
mod await_;
|
||||
mod for_loop;
|
||||
mod show;
|
||||
|
||||
@@ -230,6 +233,7 @@ mod suspense_component;
|
||||
pub mod text_prop;
|
||||
mod transition;
|
||||
pub use leptos_macro::*;
|
||||
#[doc(inline)]
|
||||
pub use server_fn;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder;
|
||||
@@ -237,16 +241,22 @@ pub use typed_builder;
|
||||
pub use typed_builder_macro;
|
||||
mod into_view;
|
||||
pub use into_view::IntoView;
|
||||
#[doc(inline)]
|
||||
pub use leptos_dom;
|
||||
mod provider;
|
||||
#[doc(inline)]
|
||||
pub use tachys;
|
||||
/// Tools to mount an application to the DOM, or to hydrate it from server-rendered HTML.
|
||||
pub mod mount;
|
||||
#[doc(inline)]
|
||||
pub use leptos_config as config;
|
||||
#[doc(inline)]
|
||||
pub use oco_ref as oco;
|
||||
mod from_form_data;
|
||||
#[doc(inline)]
|
||||
pub use either_of as either;
|
||||
pub use reactive_graph;
|
||||
#[doc(inline)]
|
||||
pub use reactive_graph as reactive;
|
||||
|
||||
/// Provide and access data along the reactive graph, sharing data without directly passing arguments.
|
||||
pub mod context {
|
||||
@@ -254,17 +264,22 @@ pub mod context {
|
||||
pub use reactive_graph::owner::{provide_context, use_context};
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use leptos_server as server;
|
||||
/// HTML attribute types.
|
||||
#[doc(inline)]
|
||||
pub use tachys::html::attribute as attr;
|
||||
/// HTML element types.
|
||||
#[doc(inline)]
|
||||
pub use tachys::html::element as html;
|
||||
/// HTML event types.
|
||||
#[doc(no_inline)]
|
||||
pub use tachys::html::event as ev;
|
||||
/// MathML element types.
|
||||
#[doc(inline)]
|
||||
pub use tachys::mathml as math;
|
||||
/// SVG element types.
|
||||
#[doc(inline)]
|
||||
pub use tachys::svg;
|
||||
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
@@ -272,7 +287,7 @@ pub mod logging {
|
||||
pub use leptos_dom::{debug_warn, error, log, warn};
|
||||
}
|
||||
|
||||
pub mod spawn {
|
||||
pub mod task {
|
||||
pub use any_spawner::Executor;
|
||||
use std::future::Future;
|
||||
|
||||
@@ -290,9 +305,14 @@ pub mod spawn {
|
||||
Executor::spawn_local(fut)
|
||||
}
|
||||
|
||||
/// Waits until the next "tick" of the current async executor.
|
||||
pub async fn tick() {
|
||||
Executor::tick().await
|
||||
}
|
||||
|
||||
pub use reactive_graph::{
|
||||
spawn_local_scoped, spawn_local_scoped_with_cancellation,
|
||||
};
|
||||
}
|
||||
|
||||
// these reexports are used in islands
|
||||
@@ -309,234 +329,3 @@ pub use tracing;
|
||||
pub use wasm_bindgen;
|
||||
#[doc(hidden)]
|
||||
pub use web_sys;
|
||||
|
||||
/*mod additional_attributes;
|
||||
pub use additional_attributes::*;
|
||||
mod await_;
|
||||
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()
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -5,10 +5,8 @@ use any_spawner::Executor;
|
||||
use reactive_graph::owner::Owner;
|
||||
#[cfg(debug_assertions)]
|
||||
use std::cell::Cell;
|
||||
use std::marker::PhantomData;
|
||||
use tachys::{
|
||||
dom::body,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
view::{Mountable, Render},
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
@@ -38,10 +36,7 @@ thread_local! {
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
/// Runs the provided closure and mounts the result to the provided element.
|
||||
pub fn hydrate_from<F, N>(
|
||||
parent: HtmlElement,
|
||||
f: F,
|
||||
) -> UnmountHandle<N::State, Dom>
|
||||
pub fn hydrate_from<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State>
|
||||
where
|
||||
F: FnOnce() -> N + 'static,
|
||||
N: IntoView,
|
||||
@@ -85,11 +80,7 @@ where
|
||||
|
||||
// returns a handle that owns the owner
|
||||
// when this is dropped, it will clean up the reactive system and unmount the view
|
||||
UnmountHandle {
|
||||
owner,
|
||||
mountable,
|
||||
rndr: PhantomData,
|
||||
}
|
||||
UnmountHandle { owner, mountable }
|
||||
}
|
||||
|
||||
/// Runs the provided closure and mounts the result to the `<body>`.
|
||||
@@ -103,7 +94,7 @@ where
|
||||
}
|
||||
|
||||
/// Runs the provided closure and mounts the result to the provided element.
|
||||
pub fn mount_to<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State, Dom>
|
||||
pub fn mount_to<F, N>(parent: HtmlElement, f: F) -> UnmountHandle<N::State>
|
||||
where
|
||||
F: FnOnce() -> N + 'static,
|
||||
N: IntoView,
|
||||
@@ -140,22 +131,17 @@ where
|
||||
|
||||
// returns a handle that owns the owner
|
||||
// when this is dropped, it will clean up the reactive system and unmount the view
|
||||
UnmountHandle {
|
||||
owner,
|
||||
mountable,
|
||||
rndr: PhantomData,
|
||||
}
|
||||
UnmountHandle { owner, mountable }
|
||||
}
|
||||
|
||||
/// Runs the provided closure and mounts the result to the provided element.
|
||||
pub fn mount_to_renderer<F, N, R>(
|
||||
parent: &R::Element,
|
||||
pub fn mount_to_renderer<F, N>(
|
||||
parent: &tachys::renderer::types::Element,
|
||||
f: F,
|
||||
) -> UnmountHandle<N::State, R>
|
||||
) -> UnmountHandle<N::State>
|
||||
where
|
||||
F: FnOnce() -> N + 'static,
|
||||
N: Render<R>,
|
||||
R: Renderer,
|
||||
N: Render,
|
||||
{
|
||||
// use wasm-bindgen-futures to drive the reactive system
|
||||
// we ignore the return value because an Err here just means the wasm-bindgen executor is
|
||||
@@ -173,11 +159,7 @@ where
|
||||
|
||||
// returns a handle that owns the owner
|
||||
// when this is dropped, it will clean up the reactive system and unmount the view
|
||||
UnmountHandle {
|
||||
owner,
|
||||
mountable,
|
||||
rndr: PhantomData,
|
||||
}
|
||||
UnmountHandle { owner, mountable }
|
||||
}
|
||||
|
||||
/// Hydrates any islands that are currently present on the page.
|
||||
@@ -211,21 +193,18 @@ pub fn hydrate_islands() {
|
||||
reactive system. You should either call `.forget()` to keep the \
|
||||
view permanently mounted, or store the `UnmountHandle` somewhere \
|
||||
and drop it when you'd like to unmount the view."]
|
||||
pub struct UnmountHandle<M, R>
|
||||
pub struct UnmountHandle<M>
|
||||
where
|
||||
M: Mountable<R>,
|
||||
R: Renderer,
|
||||
M: Mountable,
|
||||
{
|
||||
#[allow(dead_code)]
|
||||
owner: Owner,
|
||||
mountable: M,
|
||||
rndr: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<M, R> UnmountHandle<M, R>
|
||||
impl<M> UnmountHandle<M>
|
||||
where
|
||||
M: Mountable<R>,
|
||||
R: Renderer,
|
||||
M: Mountable,
|
||||
{
|
||||
/// Leaks the handle, preventing the reactive system from being cleaned up and the view from
|
||||
/// being unmounted. This should always be called when [`mount_to`] is used for the root of an
|
||||
@@ -235,10 +214,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, R> Drop for UnmountHandle<M, R>
|
||||
impl<M> Drop for UnmountHandle<M>
|
||||
where
|
||||
M: Mountable<R>,
|
||||
R: Renderer,
|
||||
M: Mountable,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
self.mountable.unmount();
|
||||
|
||||
@@ -6,7 +6,7 @@ use base64::{
|
||||
};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use std::{fmt::Display, ops::Deref, sync::Arc};
|
||||
use tachys::{html::attribute::AttributeValue, renderer::Renderer};
|
||||
use tachys::html::attribute::AttributeValue;
|
||||
|
||||
/// A cryptographic nonce ("number used once") which can be
|
||||
/// used by Content Security Policy to determine whether or not a given
|
||||
@@ -65,12 +65,9 @@ impl Display for Nonce {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> AttributeValue<R> for Nonce
|
||||
where
|
||||
R: Renderer,
|
||||
{
|
||||
impl AttributeValue for Nonce {
|
||||
type AsyncOutput = Self;
|
||||
type State = <Arc<str> as AttributeValue<R>>::State;
|
||||
type State = <Arc<str> as AttributeValue>::State;
|
||||
type Cloneable = Self;
|
||||
type CloneableOwned = Self;
|
||||
|
||||
@@ -79,7 +76,7 @@ where
|
||||
}
|
||||
|
||||
fn to_html(self, key: &str, buf: &mut String) {
|
||||
<Arc<str> as AttributeValue<R>>::to_html(self.0, key, buf)
|
||||
<Arc<str> as AttributeValue>::to_html(self.0, key, buf)
|
||||
}
|
||||
|
||||
fn to_template(_key: &str, _buf: &mut String) {}
|
||||
@@ -87,17 +84,21 @@ where
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
key: &str,
|
||||
el: &<R as Renderer>::Element,
|
||||
el: &tachys::renderer::types::Element,
|
||||
) -> Self::State {
|
||||
<Arc<str> as AttributeValue<R>>::hydrate::<FROM_SERVER>(self.0, key, el)
|
||||
<Arc<str> as AttributeValue>::hydrate::<FROM_SERVER>(self.0, key, el)
|
||||
}
|
||||
|
||||
fn build(self, el: &<R as Renderer>::Element, key: &str) -> Self::State {
|
||||
<Arc<str> as AttributeValue<R>>::build(self.0, el, key)
|
||||
fn build(
|
||||
self,
|
||||
el: &tachys::renderer::types::Element,
|
||||
key: &str,
|
||||
) -> Self::State {
|
||||
<Arc<str> as AttributeValue>::build(self.0, el, key)
|
||||
}
|
||||
|
||||
fn rebuild(self, key: &str, state: &mut Self::State) {
|
||||
<Arc<str> as AttributeValue<R>>::rebuild(self.0, key, state)
|
||||
<Arc<str> as AttributeValue>::rebuild(self.0, key, state)
|
||||
}
|
||||
|
||||
fn into_cloneable(self) -> Self::Cloneable {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{children::TypedChildrenFn, mount, IntoView};
|
||||
use leptos_dom::helpers::document;
|
||||
use leptos_macro::component;
|
||||
use reactive_graph::{effect::Effect, owner::Owner, untrack};
|
||||
use reactive_graph::{effect::Effect, graph::untrack, owner::Owner};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Renders components somewhere else in the DOM.
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn Provider<T, Chil>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Chil: IntoView,
|
||||
Chil: IntoView + 'static,
|
||||
{
|
||||
let owner = Owner::current()
|
||||
.expect("no current reactive Owner found")
|
||||
|
||||
@@ -21,7 +21,6 @@ use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
reactive_graph::{OwnedView, OwnedViewState},
|
||||
renderer::Renderer,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr,
|
||||
@@ -135,15 +134,14 @@ pub(crate) struct SuspenseBoundary<const TRANSITION: bool, Fal, Chil> {
|
||||
pub children: Chil,
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Chil, Rndr> Render<Rndr>
|
||||
impl<const TRANSITION: bool, Fal, Chil> Render
|
||||
for SuspenseBoundary<TRANSITION, Fal, Chil>
|
||||
where
|
||||
Fal: Render<Rndr> + Send + 'static,
|
||||
Chil: Render<Rndr> + Send + 'static,
|
||||
Rndr: Renderer + 'static,
|
||||
Fal: Render + Send + 'static,
|
||||
Chil: Render + Send + 'static,
|
||||
{
|
||||
type State = RenderEffect<
|
||||
OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>, Rndr>,
|
||||
OwnedViewState<EitherKeepAliveState<Chil::State, Fal::State>>,
|
||||
>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
@@ -187,25 +185,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Chil, Rndr> AddAnyAttr<Rndr>
|
||||
impl<const TRANSITION: bool, Fal, Chil> AddAnyAttr
|
||||
for SuspenseBoundary<TRANSITION, Fal, Chil>
|
||||
where
|
||||
Fal: RenderHtml<Rndr> + Send + 'static,
|
||||
Chil: RenderHtml<Rndr> + Send + 'static,
|
||||
Rndr: Renderer + 'static,
|
||||
Fal: RenderHtml + Send + 'static,
|
||||
Chil: RenderHtml + Send + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> = SuspenseBoundary<
|
||||
type Output<SomeNewAttr: Attribute> = SuspenseBoundary<
|
||||
TRANSITION,
|
||||
Fal,
|
||||
Chil::Output<SomeNewAttr::CloneableOwned>,
|
||||
>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
let attr = attr.into_cloneable_owned();
|
||||
let SuspenseBoundary {
|
||||
@@ -223,12 +220,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<const TRANSITION: bool, Fal, Chil, Rndr> RenderHtml<Rndr>
|
||||
impl<const TRANSITION: bool, Fal, Chil> RenderHtml
|
||||
for SuspenseBoundary<TRANSITION, Fal, Chil>
|
||||
where
|
||||
Fal: RenderHtml<Rndr> + Send + 'static,
|
||||
Chil: RenderHtml<Rndr> + Send + 'static,
|
||||
Rndr: Renderer + 'static,
|
||||
Fal: RenderHtml + Send + 'static,
|
||||
Chil: RenderHtml + Send + 'static,
|
||||
{
|
||||
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
|
||||
// itself
|
||||
@@ -405,7 +401,7 @@ where
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Rndr>,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
let cursor = cursor.to_owned();
|
||||
@@ -455,10 +451,9 @@ impl<T> Unsuspend<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Rndr> Render<Rndr> for Unsuspend<T>
|
||||
impl<T> Render for Unsuspend<T>
|
||||
where
|
||||
T: Render<Rndr>,
|
||||
Rndr: Renderer,
|
||||
T: Render,
|
||||
{
|
||||
type State = T::State;
|
||||
|
||||
@@ -471,30 +466,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Rndr> AddAnyAttr<Rndr> for Unsuspend<T>
|
||||
impl<T> AddAnyAttr for Unsuspend<T>
|
||||
where
|
||||
T: AddAnyAttr<Rndr> + 'static,
|
||||
Rndr: Renderer,
|
||||
T: AddAnyAttr + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Rndr>> =
|
||||
type Output<SomeNewAttr: Attribute> =
|
||||
Unsuspend<T::Output<SomeNewAttr::CloneableOwned>>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Rndr>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
let attr = attr.into_cloneable_owned();
|
||||
Unsuspend::new(move || (self.0)().add_any_attr(attr))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Rndr> RenderHtml<Rndr> for Unsuspend<T>
|
||||
impl<T> RenderHtml for Unsuspend<T>
|
||||
where
|
||||
T: RenderHtml<Rndr> + 'static,
|
||||
Rndr: Renderer,
|
||||
T: RenderHtml + 'static,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
|
||||
@@ -535,7 +528,7 @@ where
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Rndr>,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
use leptos_dom::{IntoView, View};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
|
||||
/// to enable optional props in for example `<Show>` and `<Suspense>`.
|
||||
#[derive(Clone)]
|
||||
pub struct ViewFn(Rc<dyn Fn() -> View>);
|
||||
|
||||
impl Default for ViewFn {
|
||||
fn default() -> Self {
|
||||
Self(Rc::new(|| ().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, IV> From<F> for ViewFn
|
||||
where
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
fn from(value: F) -> Self {
|
||||
Self(Rc::new(move || value().into_view()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewFn {
|
||||
/// Execute the wrapped function
|
||||
pub fn run(&self) -> View {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::html::HtmlElement;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn simple_ssr_test() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
let (value, set_value) = signal(0);
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
@@ -20,6 +22,7 @@ fn simple_ssr_test() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_test_with_components() {
|
||||
use leptos::prelude::*;
|
||||
@@ -36,7 +39,7 @@ fn ssr_test_with_components() {
|
||||
}
|
||||
}
|
||||
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div class="counters">
|
||||
<Counter initial_value=1/>
|
||||
<Counter initial_value=2/>
|
||||
@@ -51,6 +54,7 @@ fn ssr_test_with_components() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_test_with_snake_case_components() {
|
||||
use leptos::prelude::*;
|
||||
@@ -66,7 +70,7 @@ fn ssr_test_with_snake_case_components() {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div class="counters">
|
||||
<SnakeCaseCounter initial_value=1/>
|
||||
<SnakeCaseCounter initial_value=2/>
|
||||
@@ -81,12 +85,13 @@ fn ssr_test_with_snake_case_components() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_classes() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
let (value, _set_value) = signal(5);
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! {
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div
|
||||
class="my big"
|
||||
class:a=move || { value.get() > 10 }
|
||||
@@ -98,13 +103,14 @@ 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::*;
|
||||
|
||||
let (_, set_value) = signal(0);
|
||||
let styles = "myclass";
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! { class=styles,
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! { class=styles,
|
||||
<div>
|
||||
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>
|
||||
"-1"
|
||||
@@ -119,12 +125,13 @@ fn ssr_with_styles() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_option() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
let (_, _) = signal(0);
|
||||
let rendered: View<HtmlElement<_, _, _, Dom>> = view! { <option></option> };
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! { <option></option> };
|
||||
|
||||
assert_eq!(rendered.to_html(), "<option></option>");
|
||||
}
|
||||
|
||||
@@ -396,8 +396,7 @@ impl IntervalHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls,
|
||||
/// returning a cancelable handle.
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls.
|
||||
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.7.0-beta5"
|
||||
version = "0.7.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -51,7 +51,27 @@ 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"]]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"csr",
|
||||
],
|
||||
[
|
||||
"hydrate",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"actix",
|
||||
"axum",
|
||||
],
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }
|
||||
|
||||
@@ -18,6 +18,7 @@ use syn::{
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
is_transparent: bool,
|
||||
island: Option<String>,
|
||||
docs: Docs,
|
||||
unknown_attrs: UnknownAttrs,
|
||||
@@ -62,6 +63,7 @@ impl Parse for Model {
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
is_transparent: false,
|
||||
island: None,
|
||||
docs,
|
||||
unknown_attrs,
|
||||
@@ -102,6 +104,7 @@ 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,
|
||||
@@ -116,20 +119,22 @@ impl ToTokens for Model {
|
||||
let no_props = props.is_empty();
|
||||
|
||||
// check for components that end ;
|
||||
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 you’d like \
|
||||
to return (), you can do it it explicitly by returning () as \
|
||||
the last item from the component."
|
||||
);
|
||||
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 you’d \
|
||||
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);
|
||||
@@ -255,7 +260,7 @@ impl ToTokens for Model {
|
||||
let body_name = unmodified_fn_name_from_fn_name(&body_name);
|
||||
let body_expr = if is_island {
|
||||
quote! {
|
||||
::leptos::reactive_graph::owner::Owner::with_hydration(move || {
|
||||
::leptos::reactive::owner::Owner::with_hydration(move || {
|
||||
#body_name(#prop_names)
|
||||
})
|
||||
}
|
||||
@@ -265,14 +270,30 @@ impl ToTokens for Model {
|
||||
}
|
||||
};
|
||||
|
||||
let component = quote! {
|
||||
::leptos::reactive_graph::untrack(
|
||||
move || {
|
||||
#tracing_guard_expr
|
||||
#tracing_props_expr
|
||||
#body_expr
|
||||
}
|
||||
)
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// add island wrapper if island
|
||||
@@ -280,7 +301,7 @@ impl ToTokens for Model {
|
||||
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
|
||||
quote! {
|
||||
{
|
||||
if ::leptos::reactive_graph::owner::Owner::current_shared_context()
|
||||
if ::leptos::reactive::owner::Owner::current_shared_context()
|
||||
.map(|sc| sc.get_is_hydrating())
|
||||
.unwrap_or(false) {
|
||||
::leptos::either::Either::Left(
|
||||
@@ -316,9 +337,9 @@ impl ToTokens for Model {
|
||||
quote! {
|
||||
use leptos::tachys::view::any_view::IntoAny;
|
||||
let children = Box::new(|| {
|
||||
let sc = ::leptos::reactive_graph::owner::Owner::current_shared_context().unwrap();
|
||||
let sc = ::leptos::reactive::owner::Owner::current_shared_context().unwrap();
|
||||
let prev = sc.get_is_hydrating();
|
||||
let value = ::leptos::reactive_graph::owner::Owner::with_no_hydration(||
|
||||
let value = ::leptos::reactive::owner::Owner::with_no_hydration(||
|
||||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
|
||||
);
|
||||
sc.set_is_hydrating(prev);
|
||||
@@ -347,41 +368,60 @@ impl ToTokens for Model {
|
||||
let island_props = if is_island_with_children
|
||||
|| is_island_with_other_props
|
||||
{
|
||||
let (destructure, prop_builders) = if is_island_with_other_props
|
||||
{
|
||||
let prop_names = props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
if prop.name.ident == "children" {
|
||||
None
|
||||
} else {
|
||||
let name = &prop.name.ident;
|
||||
Some(quote! { #name, })
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
let destructure = quote! {
|
||||
let #props_serialized_name {
|
||||
#prop_names
|
||||
} = props;
|
||||
let (destructure, prop_builders, optional_props) =
|
||||
if is_island_with_other_props {
|
||||
let prop_names = props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
if prop.name.ident == "children" {
|
||||
None
|
||||
} else {
|
||||
let name = &prop.name.ident;
|
||||
Some(quote! { #name, })
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
let destructure = quote! {
|
||||
let #props_serialized_name {
|
||||
#prop_names
|
||||
} = props;
|
||||
};
|
||||
let prop_builders = props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
if prop.name.ident == "children"
|
||||
|| prop.prop_opts.optional
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let name = &prop.name.ident;
|
||||
Some(quote! {
|
||||
.#name(#name)
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
let optional_props = props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
if prop.name.ident == "children"
|
||||
|| !prop.prop_opts.optional
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let name = &prop.name.ident;
|
||||
Some(quote! {
|
||||
if let Some(#name) = #name {
|
||||
props.#name = Some(#name)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
(destructure, prop_builders, optional_props)
|
||||
} else {
|
||||
(quote! {}, quote! {}, quote! {})
|
||||
};
|
||||
let prop_builders = props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
if prop.name.ident == "children" {
|
||||
None
|
||||
} else {
|
||||
let name = &prop.name.ident;
|
||||
Some(quote! {
|
||||
.#name(#name)
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
(destructure, prop_builders)
|
||||
} else {
|
||||
(quote! {}, quote! {})
|
||||
};
|
||||
let children = if is_island_with_children {
|
||||
quote! {
|
||||
.children({Box::new(|| {
|
||||
@@ -405,10 +445,14 @@ impl ToTokens for Model {
|
||||
|
||||
quote! {{
|
||||
#destructure
|
||||
#props_name::builder()
|
||||
let mut props = #props_name::builder()
|
||||
#prop_builders
|
||||
#children
|
||||
.build()
|
||||
.build();
|
||||
|
||||
#optional_props
|
||||
|
||||
props
|
||||
}}
|
||||
} else {
|
||||
quote! {}
|
||||
@@ -497,6 +541,13 @@ 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;
|
||||
|
||||
@@ -535,11 +535,24 @@ 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 {
|
||||
component_macro(s, None)
|
||||
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)
|
||||
}
|
||||
|
||||
/// Defines a component as an interactive island when you are using the
|
||||
@@ -615,17 +628,37 @@ pub fn component(
|
||||
/// ```
|
||||
#[proc_macro_error2::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
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
|
||||
};
|
||||
|
||||
let island_src = s.to_string();
|
||||
component_macro(s, Some(island_src))
|
||||
component_macro(s, is_transparent, Some(island_src))
|
||||
}
|
||||
|
||||
fn component_macro(s: TokenStream, island: Option<String>) -> TokenStream {
|
||||
fn component_macro(
|
||||
s: TokenStream,
|
||||
is_transparent: bool,
|
||||
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.with_island(island).into_token_stream();
|
||||
let expanded = model.is_transparent(is_transparent).with_island(island).into_token_stream();
|
||||
if !matches!(unexpanded.vis, Visibility::Public(_)) {
|
||||
unexpanded.vis = Visibility::Public(Pub {
|
||||
span: unexpanded.vis.span(),
|
||||
@@ -893,7 +926,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
|
||||
/// Derives a trait that parses a map of string keys and values into a typed
|
||||
/// data structure, e.g., for route params.
|
||||
#[proc_macro_derive(Params, attributes(params))]
|
||||
#[proc_macro_derive(Params)]
|
||||
pub fn params_derive(
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
|
||||
@@ -40,9 +40,9 @@ impl ToTokens for MemoMacroInput {
|
||||
let path = &self.path;
|
||||
|
||||
tokens.extend(quote! {
|
||||
::leptos::reactive_graph::computed::Memo::new(
|
||||
::leptos::reactive::computed::Memo::new(
|
||||
move |_| {
|
||||
use ::leptos::reactive_graph::traits::With;
|
||||
use ::leptos::reactive::traits::With;
|
||||
#root.with(|st: _| st.#path.clone())
|
||||
}
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
|
||||
|
||||
let gen = quote! {
|
||||
impl Params for #name {
|
||||
fn from_map(map: &::leptos_router::params::ParamsMap) -> Result<Self, ::leptos_router::params::ParamsError> {
|
||||
fn from_map(map: &::leptos_router::params::ParamsMap) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
|
||||
Ok(Self {
|
||||
#(#fields,)*
|
||||
})
|
||||
|
||||
@@ -40,7 +40,7 @@ impl ToTokens for SliceMacroInput {
|
||||
let path = &self.path;
|
||||
|
||||
tokens.extend(quote! {
|
||||
::leptos::reactive_graph::computed::create_slice(
|
||||
::leptos::reactive::computed::create_slice(
|
||||
#root,
|
||||
|st: &_| st.#path.clone(),
|
||||
|st: &mut _, n| st.#path = n
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::{fragment_to_tokens, TagType};
|
||||
use crate::view::attribute_absolute;
|
||||
use super::{
|
||||
fragment_to_tokens, utils::is_nostrip_optional_and_update_key, TagType,
|
||||
};
|
||||
use crate::view::{attribute_absolute, utils::filter_prefixed_attrs};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use rstml::node::{
|
||||
@@ -44,9 +46,10 @@ pub(crate) fn component_to_tokens(
|
||||
})
|
||||
.unwrap_or_else(|| node.attributes().len());
|
||||
|
||||
let attrs = node
|
||||
.attributes()
|
||||
.iter()
|
||||
// Initially using uncloned mutable reference, as the node.key might be mutated during prop extraction (for nostrip:)
|
||||
let mut attrs = node
|
||||
.attributes_mut()
|
||||
.iter_mut()
|
||||
.filter_map(|node| {
|
||||
if let NodeAttribute::Attribute(node) = node {
|
||||
Some(node)
|
||||
@@ -54,39 +57,46 @@ pub(crate) fn component_to_tokens(
|
||||
None
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let props = attrs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(idx, attr)| {
|
||||
idx < &spread_marker && {
|
||||
let attr_key = attr.key.to_string();
|
||||
!is_attr_let(&attr.key)
|
||||
&& !attr_key.starts_with("clone:")
|
||||
&& !attr_key.starts_with("class:")
|
||||
&& !attr_key.starts_with("style:")
|
||||
&& !attr_key.starts_with("attr:")
|
||||
&& !attr_key.starts_with("prop:")
|
||||
&& !attr_key.starts_with("on:")
|
||||
&& !attr_key.starts_with("use:")
|
||||
}
|
||||
})
|
||||
.map(|(_, attr)| {
|
||||
let name = &attr.key;
|
||||
let mut required_props = vec![];
|
||||
let mut optional_props = vec![];
|
||||
for (_, attr) in attrs.iter_mut().enumerate().filter(|(idx, attr)| {
|
||||
idx < &spread_marker && {
|
||||
let attr_key = attr.key.to_string();
|
||||
!is_attr_let(&attr.key)
|
||||
&& !attr_key.starts_with("clone:")
|
||||
&& !attr_key.starts_with("class:")
|
||||
&& !attr_key.starts_with("style:")
|
||||
&& !attr_key.starts_with("attr:")
|
||||
&& !attr_key.starts_with("prop:")
|
||||
&& !attr_key.starts_with("on:")
|
||||
&& !attr_key.starts_with("use:")
|
||||
}
|
||||
}) {
|
||||
let optional = is_nostrip_optional_and_update_key(&mut attr.key);
|
||||
let name = &attr.key;
|
||||
|
||||
let value = attr
|
||||
.value()
|
||||
.map(|v| {
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
let value = attr
|
||||
.value()
|
||||
.map(|v| {
|
||||
quote! { #v }
|
||||
})
|
||||
.unwrap_or_else(|| quote! { #name });
|
||||
|
||||
quote! {
|
||||
if optional {
|
||||
optional_props.push(quote! {
|
||||
props.#name = { #value }.map(Into::into);
|
||||
})
|
||||
} else {
|
||||
required_props.push(quote! {
|
||||
.#name(#[allow(unused_braces)] { #value })
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the mutable reference to the node, go to an owned clone:
|
||||
let attrs = attrs.into_iter().map(|a| a.clone()).collect::<Vec<_>>();
|
||||
|
||||
let items_to_bind = attrs
|
||||
.iter()
|
||||
@@ -105,20 +115,13 @@ pub(crate) fn component_to_tokens(
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let inputs = &binding.inputs;
|
||||
Some(quote! { #inputs })
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let items_to_clone = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("clone:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let items_to_clone = filter_prefixed_attrs(attrs.iter(), "clone:");
|
||||
|
||||
// include all attribute that are either
|
||||
// 1) blocks ({..attrs} or {attrs}),
|
||||
@@ -271,14 +274,20 @@ pub(crate) fn component_to_tokens(
|
||||
let mut component = quote! {
|
||||
{
|
||||
#[allow(unreachable_code)]
|
||||
#[allow(unused_mut)]
|
||||
#[allow(clippy::let_and_return)]
|
||||
::leptos::component::component_view(
|
||||
#[allow(clippy::needless_borrows_for_generic_args)]
|
||||
&#name,
|
||||
::leptos::component::component_props_builder(&#name #generics)
|
||||
#(#props)*
|
||||
#(#slots)*
|
||||
#children
|
||||
.build()
|
||||
{
|
||||
let mut props = ::leptos::component::component_props_builder(&#name #generics)
|
||||
#(#required_props)*
|
||||
#(#slots)*
|
||||
#children
|
||||
.build();
|
||||
#(#optional_props)*
|
||||
props
|
||||
}
|
||||
)
|
||||
#spreads
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
mod component_builder;
|
||||
mod slot_helper;
|
||||
mod utils;
|
||||
|
||||
use self::{
|
||||
component_builder::component_to_tokens,
|
||||
slot_helper::{get_slot, slot_to_tokens},
|
||||
};
|
||||
use convert_case::{Case::Snake, Casing};
|
||||
use convert_case::{
|
||||
Case::{Snake, UpperCamel},
|
||||
Casing,
|
||||
};
|
||||
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use proc_macro_error2::abort;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use quote::{format_ident, quote, quote_spanned, ToTokens};
|
||||
use rstml::node::{
|
||||
CustomNode, KVAttributeValue, KeyedAttribute, Node, NodeAttribute,
|
||||
NodeBlock, NodeElement, NodeName, NodeNameFragment,
|
||||
@@ -174,7 +179,7 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
|
||||
}
|
||||
|
||||
enum Item<'a, T> {
|
||||
Node(&'a Node<T>),
|
||||
Node(&'a Node<T>, bool),
|
||||
ClosingTag(String),
|
||||
}
|
||||
|
||||
@@ -285,10 +290,11 @@ 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)]);
|
||||
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
|
||||
|
||||
while let Some(current) = nodes.pop_front() {
|
||||
match current {
|
||||
@@ -298,19 +304,32 @@ fn inert_element_to_tokens(
|
||||
html.push_str(&tag);
|
||||
html.push('>');
|
||||
}
|
||||
Item::Node(current) => {
|
||||
Item::Node(current, escape) => {
|
||||
match current {
|
||||
Node::RawText(raw) => {
|
||||
let text = raw.to_string_best();
|
||||
let text = if escape {
|
||||
html_escape::encode_text(&text)
|
||||
} else {
|
||||
text.into()
|
||||
};
|
||||
html.push_str(&text);
|
||||
}
|
||||
Node::Text(text) => {
|
||||
let text = text.value_string();
|
||||
let text = if escape {
|
||||
html_escape::encode_text(&text)
|
||||
} else {
|
||||
text.into()
|
||||
};
|
||||
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('<');
|
||||
@@ -319,9 +338,12 @@ fn inert_element_to_tokens(
|
||||
for attr in node.attributes() {
|
||||
if let NodeAttribute::Attribute(attr) = attr {
|
||||
let attr_name = attr.key.to_string();
|
||||
// trim r# from raw identifiers like r#as
|
||||
let attr_name =
|
||||
attr_name.trim_start_matches("r#");
|
||||
if attr_name != "class" {
|
||||
html.push(' ');
|
||||
html.push_str(&attr_name);
|
||||
html.push_str(attr_name);
|
||||
}
|
||||
|
||||
if let Some(value) =
|
||||
@@ -332,11 +354,13 @@ fn inert_element_to_tokens(
|
||||
)) = &value.value
|
||||
{
|
||||
if let Lit::Str(txt) = &lit.lit {
|
||||
let value = txt.value();
|
||||
let value = html_escape::encode_double_quoted_attribute(&value);
|
||||
if attr_name == "class" {
|
||||
html.push_class(&txt.value());
|
||||
html.push_class(&value);
|
||||
} else {
|
||||
html.push_str("=\"");
|
||||
html.push_str(&txt.value());
|
||||
html.push_str(&value);
|
||||
html.push('"');
|
||||
}
|
||||
}
|
||||
@@ -352,7 +376,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));
|
||||
nodes.push_front(Item::Node(child, escape));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,7 +560,9 @@ fn node_to_tokens(
|
||||
view_marker,
|
||||
disable_inert_html,
|
||||
),
|
||||
Node::Block(block) => Some(quote! { #block }),
|
||||
Node::Block(block) => {
|
||||
Some(quote! { ::leptos::prelude::IntoRender::into_render(#block) })
|
||||
}
|
||||
Node::Text(text) => Some(text_to_tokens(&text.value)),
|
||||
Node::RawText(raw) => {
|
||||
let text = raw.to_string_best();
|
||||
@@ -545,7 +571,11 @@ fn node_to_tokens(
|
||||
}
|
||||
Node::Element(el_node) => {
|
||||
if !top_level && is_inert {
|
||||
inert_element_to_tokens(node, global_class)
|
||||
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)
|
||||
} else {
|
||||
element_to_tokens(
|
||||
el_node,
|
||||
@@ -713,6 +743,11 @@ 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;
|
||||
@@ -866,7 +901,7 @@ fn attribute_to_tokens(
|
||||
NodeName::Path(path) => path.path.get_ident(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
quote! {
|
||||
.#node_ref(#value)
|
||||
}
|
||||
@@ -874,6 +909,8 @@ fn attribute_to_tokens(
|
||||
directive_call_from_attribute_node(node, name)
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
event_to_tokens(name, node)
|
||||
} else if let Some(name) = name.strip_prefix("bind:") {
|
||||
two_way_binding_to_tokens(name, node)
|
||||
} else if let Some(name) = name.strip_prefix("class:") {
|
||||
let class = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
@@ -914,13 +951,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);
|
||||
let value = attribute_value(node, true);
|
||||
quote! {
|
||||
.attr(#name, #value)
|
||||
}
|
||||
} else {
|
||||
let key = attribute_name(&node.key);
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, true);
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if &node.key.to_string() == "class"
|
||||
@@ -957,11 +994,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" {
|
||||
@@ -969,6 +1006,7 @@ 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("_");
|
||||
@@ -997,6 +1035,7 @@ 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)
|
||||
@@ -1005,6 +1044,7 @@ 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(
|
||||
@@ -1057,6 +1097,20 @@ pub(crate) fn attribute_absolute(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn two_way_binding_to_tokens(
|
||||
name: &str,
|
||||
node: &KeyedAttribute,
|
||||
) -> TokenStream {
|
||||
let value = attribute_value(node, false);
|
||||
|
||||
let ident =
|
||||
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
|
||||
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn event_to_tokens(
|
||||
name: &str,
|
||||
node: &KeyedAttribute,
|
||||
@@ -1072,7 +1126,7 @@ pub(crate) fn event_type_and_handler(
|
||||
name: &str,
|
||||
node: &KeyedAttribute,
|
||||
) -> (TokenStream, TokenStream, TokenStream) {
|
||||
let handler = attribute_value(node);
|
||||
let handler = attribute_value(node, false);
|
||||
|
||||
let (event_type, is_custom, is_force_undelegated, is_targeted) =
|
||||
parse_event_name(name);
|
||||
@@ -1129,7 +1183,7 @@ fn class_to_tokens(
|
||||
class: TokenStream,
|
||||
class_name: Option<&str>,
|
||||
) -> TokenStream {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
if let Some(class_name) = class_name {
|
||||
quote! {
|
||||
.#class((#class_name, #value))
|
||||
@@ -1146,7 +1200,7 @@ fn style_to_tokens(
|
||||
style: TokenStream,
|
||||
style_name: Option<&str>,
|
||||
) -> TokenStream {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
if let Some(style_name) = style_name {
|
||||
quote! {
|
||||
.#style((#style_name, #value))
|
||||
@@ -1163,7 +1217,7 @@ fn prop_to_tokens(
|
||||
prop: TokenStream,
|
||||
key: &str,
|
||||
) -> TokenStream {
|
||||
let value = attribute_value(node);
|
||||
let value = attribute_value(node, false);
|
||||
quote! {
|
||||
.#prop(#key, #value)
|
||||
}
|
||||
@@ -1320,7 +1374,10 @@ fn attribute_name(name: &NodeName) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
|
||||
fn attribute_value(
|
||||
attr: &KeyedAttribute,
|
||||
is_attribute_proper: bool,
|
||||
) -> TokenStream {
|
||||
match attr.possible_value.to_value() {
|
||||
None => quote! { true },
|
||||
Some(value) => match &value.value {
|
||||
@@ -1335,14 +1392,26 @@ fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
{#expr}
|
||||
if matches!(expr, Expr::Lit(_)) || !is_attribute_proper {
|
||||
quote! {
|
||||
#expr
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::prelude::IntoAttributeValue::into_attribute_value(#expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
// any value in braces: expand as-is to give proper r-a support
|
||||
KVAttributeValue::InvalidBraced(block) => {
|
||||
quote! {
|
||||
#block
|
||||
if is_attribute_proper {
|
||||
quote! {
|
||||
::leptos::prelude::IntoAttributeValue::into_attribute_value(#block)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#block
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{convert_to_snake_case, ident_from_tag_name};
|
||||
use crate::view::{fragment_to_tokens, TagType};
|
||||
use crate::view::{fragment_to_tokens, utils::filter_prefixed_attrs, TagType};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use quote::{quote, quote_spanned};
|
||||
use rstml::node::{CustomNode, KeyedAttribute, NodeAttribute, NodeElement};
|
||||
use std::collections::HashMap;
|
||||
use syn::spanned::Spanned;
|
||||
@@ -70,25 +70,9 @@ pub(crate) fn slot_to_tokens(
|
||||
}
|
||||
});
|
||||
|
||||
let items_to_bind = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("let:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:");
|
||||
|
||||
let items_to_clone = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix("clone:")
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let items_to_clone = filter_prefixed_attrs(attrs.iter(), "clone:");
|
||||
|
||||
let dyn_attrs = attrs
|
||||
.iter()
|
||||
|
||||
53
leptos_macro/src/view/utils.rs
Normal file
53
leptos_macro/src/view/utils.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use proc_macro2::Ident;
|
||||
use quote::format_ident;
|
||||
use rstml::node::{KeyedAttribute, NodeName};
|
||||
use syn::{spanned::Spanned, ExprPath};
|
||||
|
||||
pub fn filter_prefixed_attrs<'a, A>(attrs: A, prefix: &str) -> Vec<Ident>
|
||||
where
|
||||
A: IntoIterator<Item = &'a KeyedAttribute> + Clone,
|
||||
{
|
||||
attrs
|
||||
.into_iter()
|
||||
.filter_map(|attr| {
|
||||
attr.key
|
||||
.to_string()
|
||||
.strip_prefix(prefix)
|
||||
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Handle nostrip: prefix:
|
||||
/// if there strip from the name, and return true to indicate that
|
||||
/// the prop should be an Option<T> and shouldn't be called on the builder if None,
|
||||
/// if Some(T) then T supplied to the builder.
|
||||
pub fn is_nostrip_optional_and_update_key(key: &mut NodeName) -> bool {
|
||||
let maybe_cleaned_name_and_span = if let NodeName::Punctuated(punct) = &key
|
||||
{
|
||||
if punct.len() == 2 {
|
||||
if let Some(cleaned_name) = key.to_string().strip_prefix("nostrip:")
|
||||
{
|
||||
punct
|
||||
.get(1)
|
||||
.map(|segment| (cleaned_name.to_string(), segment.span()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((cleaned_name, span)) = maybe_cleaned_name_and_span {
|
||||
*key = NodeName::Path(ExprPath {
|
||||
attrs: vec![],
|
||||
qself: None,
|
||||
path: format_ident!("{}", cleaned_name, span = span).into(),
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use leptos::prelude::*;
|
||||
#[component]
|
||||
fn Component(
|
||||
#[prop(optional)] optional: bool,
|
||||
#[prop(optional, into)] optional_into: Option<String>,
|
||||
#[prop(optional_no_strip)] optional_no_strip: Option<String>,
|
||||
#[prop(strip_option)] strip_option: Option<u8>,
|
||||
#[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize,
|
||||
@@ -11,6 +12,7 @@ fn Component(
|
||||
impl_trait: impl Fn() -> i32 + 'static,
|
||||
) -> impl IntoView {
|
||||
_ = optional;
|
||||
_ = optional_into;
|
||||
_ = optional_no_strip;
|
||||
_ = strip_option;
|
||||
_ = default;
|
||||
@@ -26,9 +28,29 @@ fn component() {
|
||||
.impl_trait(|| 42)
|
||||
.build();
|
||||
assert!(!cp.optional);
|
||||
assert_eq!(cp.optional_into, None);
|
||||
assert_eq!(cp.optional_no_strip, None);
|
||||
assert_eq!(cp.strip_option, Some(9));
|
||||
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());
|
||||
assert_eq!(cp.into, "");
|
||||
assert_eq!((cp.impl_trait)(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_nostrip() {
|
||||
// Should compile (using nostrip:optional_into in second <Component />)
|
||||
view! {
|
||||
<Component
|
||||
optional_into="foo"
|
||||
strip_option=9
|
||||
into=""
|
||||
impl_trait=|| 42
|
||||
/>
|
||||
<Component
|
||||
nostrip:optional_into=Some("foo")
|
||||
strip_option=9
|
||||
into=""
|
||||
impl_trait=|| 42
|
||||
/>
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(not(erase_components))]
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
@@ -19,6 +19,7 @@ tracing = { version = "0.1.40", optional = true }
|
||||
futures = "0.3.30"
|
||||
|
||||
any_spawner = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
|
||||
send_wrapper = "0.6"
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ mod local_resource;
|
||||
pub use local_resource::*;
|
||||
mod multi_action;
|
||||
pub use multi_action::*;
|
||||
mod once_resource;
|
||||
pub use once_resource::*;
|
||||
mod resource;
|
||||
pub use resource::*;
|
||||
mod shared;
|
||||
@@ -183,25 +185,23 @@ impl FromEncodedStr for [u8] {
|
||||
mod view_implementations {
|
||||
use crate::Resource;
|
||||
use reactive_graph::traits::Read;
|
||||
use std::{future::Future, pin::Pin};
|
||||
use std::future::Future;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
reactive_graph::{RenderEffectState, Suspend, SuspendState},
|
||||
renderer::Renderer,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
|
||||
impl<T, R, Ser> Render<R> for Resource<T, Ser>
|
||||
impl<T, Ser> Render for Resource<T, Ser>
|
||||
where
|
||||
T: Render<R> + Send + Sync + Clone,
|
||||
T: Render + Send + Sync + Clone,
|
||||
Ser: Send + 'static,
|
||||
R: Renderer,
|
||||
{
|
||||
type State = RenderEffectState<SuspendState<T, R>>;
|
||||
type State = RenderEffectState<SuspendState<T>>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
(move || Suspend::new(async move { self.await })).build()
|
||||
@@ -212,42 +212,35 @@ mod view_implementations {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, Ser> AddAnyAttr<R> for Resource<T, Ser>
|
||||
impl<T, Ser> AddAnyAttr for Resource<T, Ser>
|
||||
where
|
||||
T: RenderHtml<R> + Send + Sync + Clone,
|
||||
T: RenderHtml + Send + Sync + Clone,
|
||||
Ser: Send + 'static,
|
||||
R: Renderer,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<R>> = Box<
|
||||
type Output<SomeNewAttr: Attribute> = Box<
|
||||
dyn FnMut() -> Suspend<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = <T as AddAnyAttr<R>>::Output<
|
||||
<SomeNewAttr::CloneableOwned as Attribute<R>>::CloneableOwned,
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
>,
|
||||
> + Send,
|
||||
<T as AddAnyAttr>::Output<
|
||||
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
|
||||
>,
|
||||
>
|
||||
+ Send
|
||||
>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<R>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<R>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
(move || Suspend::new(async move { self.await })).add_any_attr(attr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, Ser> RenderHtml<R> for Resource<T, Ser>
|
||||
impl<T, Ser> RenderHtml for Resource<T, Ser>
|
||||
where
|
||||
T: RenderHtml<R> + Send + Sync + Clone,
|
||||
T: RenderHtml + Send + Sync + Clone,
|
||||
Ser: Send + 'static,
|
||||
R: Renderer,
|
||||
{
|
||||
type AsyncOutput = Option<T>;
|
||||
|
||||
@@ -296,7 +289,7 @@ mod view_implementations {
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<R>,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
(move || Suspend::new(async move { self.await }))
|
||||
|
||||
@@ -35,10 +35,10 @@ impl<T> Clone for ArcLocalResource<T> {
|
||||
|
||||
impl<T> ArcLocalResource<T> {
|
||||
#[track_caller]
|
||||
pub fn new<Fut>(fetcher: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Fut: Future<Output = T> + Send + 'static,
|
||||
T: 'static,
|
||||
Fut: Future<Output = T> + 'static,
|
||||
{
|
||||
let fetcher = move || {
|
||||
let fut = fetcher();
|
||||
@@ -60,7 +60,7 @@ impl<T> ArcLocalResource<T> {
|
||||
}
|
||||
};
|
||||
Self {
|
||||
data: ArcAsyncDerived::new(fetcher),
|
||||
data: ArcAsyncDerived::new_unsync(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: Send + Sync + 'static,
|
||||
T: '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> + Send + 'static,
|
||||
Fut: Future<Output = T> + '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();
|
||||
async move { SendWrapper::new(fut.await) }
|
||||
SendWrapper::new(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: Send + Sync + 'static,
|
||||
T: '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: Send + Sync + 'static,
|
||||
T: '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: Send + Sync + 'static,
|
||||
T: '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: Send + Sync + 'static,
|
||||
T: '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: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn mark_dirty(&self) {
|
||||
self.data.mark_dirty();
|
||||
@@ -363,7 +363,7 @@ where
|
||||
|
||||
impl<T> Subscriber for LocalResource<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn add_source(&self, source: AnySource) {
|
||||
self.data.add_source(source);
|
||||
|
||||
708
leptos_server/src/once_resource.rs
Normal file
708
leptos_server/src/once_resource.rs
Normal file
@@ -0,0 +1,708 @@
|
||||
use crate::{
|
||||
initial_value, FromEncodedStr, IntoEncodedString,
|
||||
IS_SUPPRESSING_RESOURCE_LOAD,
|
||||
};
|
||||
#[cfg(feature = "rkyv")]
|
||||
use codee::binary::RkyvCodec;
|
||||
#[cfg(feature = "serde-wasm-bindgen")]
|
||||
use codee::string::JsonSerdeWasmCodec;
|
||||
#[cfg(feature = "miniserde")]
|
||||
use codee::string::MiniserdeCodec;
|
||||
#[cfg(feature = "serde-lite")]
|
||||
use codee::SerdeLite;
|
||||
use codee::{
|
||||
string::{FromToStringCodec, JsonSerdeCodec},
|
||||
Decoder, Encoder,
|
||||
};
|
||||
use core::{fmt::Debug, marker::PhantomData};
|
||||
use futures::Future;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
computed::{
|
||||
suspense::SuspenseContext, AsyncDerivedReadyFuture, ScopedFuture,
|
||||
},
|
||||
diagnostics::{SpecialNonReactiveFuture, SpecialNonReactiveZone},
|
||||
graph::{AnySource, ToAnySource},
|
||||
owner::{use_context, ArenaItem, Owner},
|
||||
prelude::*,
|
||||
signal::{
|
||||
guards::{Plain, ReadGuard},
|
||||
ArcTrigger,
|
||||
},
|
||||
unwrap_signal,
|
||||
};
|
||||
use std::{
|
||||
future::IntoFuture,
|
||||
mem,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArcOnceResource<T, Ser = JsonSerdeCodec> {
|
||||
trigger: ArcTrigger,
|
||||
value: Arc<RwLock<Option<T>>>,
|
||||
wakers: Arc<RwLock<Vec<Waker>>>,
|
||||
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
|
||||
loading: Arc<AtomicBool>,
|
||||
ser: PhantomData<fn() -> Ser>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<T, Ser> Clone for ArcOnceResource<T, Ser> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
trigger: self.trigger.clone(),
|
||||
value: self.value.clone(),
|
||||
wakers: self.wakers.clone(),
|
||||
suspenses: self.suspenses.clone(),
|
||||
loading: self.loading.clone(),
|
||||
ser: self.ser,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ArcOnceResource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Ser: Encoder<T> + Decoder<T>,
|
||||
<Ser as Encoder<T>>::Error: Debug,
|
||||
<Ser as Decoder<T>>::Error: Debug,
|
||||
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_with_options(
|
||||
fut: impl Future<Output = T> + Send + 'static,
|
||||
#[allow(unused)] // this is used with `feature = "ssr"`
|
||||
blocking: bool,
|
||||
) -> Self {
|
||||
let shared_context = Owner::current_shared_context();
|
||||
let id = shared_context
|
||||
.as_ref()
|
||||
.map(|sc| sc.next_id())
|
||||
.unwrap_or_default();
|
||||
|
||||
let initial = initial_value::<T, Ser>(&id, shared_context.as_ref());
|
||||
let is_ready = initial.is_some();
|
||||
let value = Arc::new(RwLock::new(initial));
|
||||
let wakers = Arc::new(RwLock::new(Vec::<Waker>::new()));
|
||||
let suspenses = Arc::new(RwLock::new(Vec::<SuspenseContext>::new()));
|
||||
let loading = Arc::new(AtomicBool::new(!is_ready));
|
||||
let trigger = ArcTrigger::new();
|
||||
|
||||
let fut = ScopedFuture::new(fut);
|
||||
|
||||
if !is_ready && !IS_SUPPRESSING_RESOURCE_LOAD.load(Ordering::Relaxed) {
|
||||
let value = Arc::clone(&value);
|
||||
let wakers = Arc::clone(&wakers);
|
||||
let loading = Arc::clone(&loading);
|
||||
let trigger = trigger.clone();
|
||||
reactive_graph::spawn(async move {
|
||||
let loaded = fut.await;
|
||||
*value.write().or_poisoned() = Some(loaded);
|
||||
loading.store(false, Ordering::Relaxed);
|
||||
for waker in mem::take(&mut *wakers.write().or_poisoned()) {
|
||||
waker.wake();
|
||||
}
|
||||
trigger.notify();
|
||||
});
|
||||
}
|
||||
|
||||
let data = Self {
|
||||
trigger,
|
||||
value: value.clone(),
|
||||
loading,
|
||||
wakers,
|
||||
suspenses,
|
||||
ser: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(shared_context) = shared_context {
|
||||
let value = Arc::clone(&value);
|
||||
let ready_fut = data.ready();
|
||||
|
||||
if blocking {
|
||||
shared_context.defer_stream(Box::pin(data.ready()));
|
||||
}
|
||||
|
||||
if shared_context.get_is_hydrating() {
|
||||
shared_context.write_async(
|
||||
id,
|
||||
Box::pin(async move {
|
||||
ready_fut.await;
|
||||
let value = value.read().or_poisoned();
|
||||
let value = value.as_ref().unwrap();
|
||||
Ser::encode(value).unwrap().into_encoded_string()
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ArcOnceResource<T, Ser> {
|
||||
/// Returns a `Future` that is ready when this resource has next finished loading.
|
||||
pub fn ready(&self) -> AsyncDerivedReadyFuture {
|
||||
AsyncDerivedReadyFuture::new(
|
||||
self.to_any_source(),
|
||||
&self.loading,
|
||||
&self.wakers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> DefinedAt for ArcOnceResource<T, Ser> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> IsDisposed for ArcOnceResource<T, Ser> {
|
||||
#[inline(always)]
|
||||
fn is_disposed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ToAnySource for ArcOnceResource<T, Ser> {
|
||||
fn to_any_source(&self) -> AnySource {
|
||||
self.trigger.to_any_source()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Track for ArcOnceResource<T, Ser> {
|
||||
fn track(&self) {
|
||||
self.trigger.track();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for ArcOnceResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, Plain<Option<T>>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(suspense_context) = use_context::<SuspenseContext>() {
|
||||
if self.value.read().or_poisoned().is_none() {
|
||||
let handle = suspense_context.task_id();
|
||||
let ready = SpecialNonReactiveFuture::new(self.ready());
|
||||
reactive_graph::spawn(async move {
|
||||
ready.await;
|
||||
drop(handle);
|
||||
});
|
||||
self.suspenses.write().or_poisoned().push(suspense_context);
|
||||
}
|
||||
}
|
||||
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> IntoFuture for ArcOnceResource<T, Ser>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = T;
|
||||
type IntoFuture = OnceResourceFuture<T>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
OnceResourceFuture {
|
||||
source: self.to_any_source(),
|
||||
value: Arc::clone(&self.value),
|
||||
loading: Arc::clone(&self.loading),
|
||||
wakers: Arc::clone(&self.wakers),
|
||||
suspenses: Arc::clone(&self.suspenses),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Future`] that is ready when an [`ArcAsyncDerived`] is finished loading or reloading,
|
||||
/// and contains its value. `.await`ing this clones the value `T`.
|
||||
pub struct OnceResourceFuture<T> {
|
||||
source: AnySource,
|
||||
value: Arc<RwLock<Option<T>>>,
|
||||
loading: Arc<AtomicBool>,
|
||||
wakers: Arc<RwLock<Vec<Waker>>>,
|
||||
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
|
||||
}
|
||||
|
||||
impl<T> Future for OnceResourceFuture<T>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
#[track_caller]
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = SpecialNonReactiveZone::enter();
|
||||
let waker = cx.waker();
|
||||
self.source.track();
|
||||
|
||||
if let Some(suspense_context) = use_context::<SuspenseContext>() {
|
||||
self.suspenses.write().or_poisoned().push(suspense_context);
|
||||
}
|
||||
|
||||
if self.loading.load(Ordering::Relaxed) {
|
||||
self.wakers.write().or_poisoned().push(waker.clone());
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(
|
||||
self.value.read().or_poisoned().as_ref().unwrap().clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcOnceResource<T, JsonSerdeCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
JsonSerdeCodec: Encoder<T> + Decoder<T>,
|
||||
<JsonSerdeCodec as Encoder<T>>::Error: Debug,
|
||||
<JsonSerdeCodec as Decoder<T>>::Error: Debug,
|
||||
<<JsonSerdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
|
||||
ArcOnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_blocking(fut: impl Future<Output = T> + Send + 'static) -> Self {
|
||||
ArcOnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcOnceResource<T, FromToStringCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
FromToStringCodec: Encoder<T> + Decoder<T>,
|
||||
<FromToStringCodec as Encoder<T>>::Error: Debug, <FromToStringCodec as Decoder<T>>::Error: Debug,
|
||||
<<FromToStringCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
pub fn new_str(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
ArcOnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
pub fn new_str_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
ArcOnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde-wasm-bindgen")]
|
||||
impl<T> ArcOnceResource<T, JsonSerdeWasmCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
JsonSerdeWasmCodec: Encoder<T> + Decoder<T>,
|
||||
<JsonSerdeWasmCodec as Encoder<T>>::Error: Debug, <JsonSerdeWasmCodec as Decoder<T>>::Error: Debug,
|
||||
<<JsonSerdeWasmCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_serde_wb(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
ArcOnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_serde_wb_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
ArcOnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "miniserde")]
|
||||
impl<T> ArcOnceResource<T, MiniserdeCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
MiniserdeCodec: Encoder<T> + Decoder<T>,
|
||||
<MiniserdeCodec as Encoder<T>>::Error: Debug,
|
||||
<MiniserdeCodec as Decoder<T>>::Error: Debug,
|
||||
<<MiniserdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_miniserde(
|
||||
fut: impl Future<Output = T> + Send + 'static,
|
||||
) -> Self {
|
||||
ArcOnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_miniserde_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static,
|
||||
) -> Self {
|
||||
ArcOnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde-lite")]
|
||||
impl<T> ArcOnceResource<T, SerdeLite<JsonSerdeCodec>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
SerdeLite<JsonSerdeCodec>: Encoder<T> + Decoder<T>,
|
||||
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Error: Debug, <SerdeLite<JsonSerdeCodec> as Decoder<T>>::Error: Debug,
|
||||
<<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_serde_lite(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
ArcOnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_serde_lite_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
ArcOnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rkyv")]
|
||||
impl<T> ArcOnceResource<T, RkyvCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
RkyvCodec: Encoder<T> + Decoder<T>,
|
||||
<RkyvCodec as Encoder<T>>::Error: Debug,
|
||||
<RkyvCodec as Decoder<T>>::Error: Debug,
|
||||
<<RkyvCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_rkyv(fut: impl Future<Output = T> + Send + 'static) -> Self {
|
||||
ArcOnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_rkyv_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static,
|
||||
) -> Self {
|
||||
ArcOnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OnceResource<T, Ser = JsonSerdeCodec> {
|
||||
inner: ArenaItem<ArcOnceResource<T, Ser>>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<T, Ser> Clone for OnceResource<T, Ser> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Copy for OnceResource<T, Ser> {}
|
||||
|
||||
impl<T, Ser> OnceResource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Ser: Encoder<T> + Decoder<T>,
|
||||
<Ser as Encoder<T>>::Error: Debug,
|
||||
<Ser as Decoder<T>>::Error: Debug,
|
||||
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_with_options(
|
||||
fut: impl Future<Output = T> + Send + 'static,
|
||||
blocking: bool,
|
||||
) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
let defined_at = Location::caller();
|
||||
Self {
|
||||
inner: ArenaItem::new(ArcOnceResource::new_with_options(
|
||||
fut, blocking,
|
||||
)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> OnceResource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Ser: 'static,
|
||||
{
|
||||
/// Returns a `Future` that is ready when this resource has next finished loading.
|
||||
pub fn ready(&self) -> AsyncDerivedReadyFuture {
|
||||
self.inner
|
||||
.try_with_value(|inner| inner.ready())
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> DefinedAt for OnceResource<T, Ser> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> IsDisposed for OnceResource<T, Ser> {
|
||||
#[inline(always)]
|
||||
fn is_disposed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ToAnySource for OnceResource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Ser: 'static,
|
||||
{
|
||||
fn to_any_source(&self) -> AnySource {
|
||||
self.inner
|
||||
.try_with_value(|inner| inner.to_any_source())
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Track for OnceResource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Ser: 'static,
|
||||
{
|
||||
fn track(&self) {
|
||||
if let Some(inner) = self.inner.try_get_value() {
|
||||
inner.track();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for OnceResource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
Ser: 'static,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, Plain<Option<T>>>;
|
||||
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
self.inner
|
||||
.try_with_value(|inner| inner.try_read_untracked())
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> IntoFuture for OnceResource<T, Ser>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
Ser: 'static,
|
||||
{
|
||||
type Output = T;
|
||||
type IntoFuture = OnceResourceFuture<T>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
self.inner
|
||||
.try_get_value()
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
.into_future()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> OnceResource<T, JsonSerdeCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
JsonSerdeCodec: Encoder<T> + Decoder<T>,
|
||||
<JsonSerdeCodec as Encoder<T>>::Error: Debug,
|
||||
<JsonSerdeCodec as Decoder<T>>::Error: Debug,
|
||||
<<JsonSerdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
|
||||
OnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_blocking(fut: impl Future<Output = T> + Send + 'static) -> Self {
|
||||
OnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> OnceResource<T, FromToStringCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
FromToStringCodec: Encoder<T> + Decoder<T>,
|
||||
<FromToStringCodec as Encoder<T>>::Error: Debug, <FromToStringCodec as Decoder<T>>::Error: Debug,
|
||||
<<FromToStringCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
pub fn new_str(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
OnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
pub fn new_str_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
OnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde-wasm-bindgen")]
|
||||
impl<T> OnceResource<T, JsonSerdeWasmCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
JsonSerdeWasmCodec: Encoder<T> + Decoder<T>,
|
||||
<JsonSerdeWasmCodec as Encoder<T>>::Error: Debug, <JsonSerdeWasmCodec as Decoder<T>>::Error: Debug,
|
||||
<<JsonSerdeWasmCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_serde_wb(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
OnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_serde_wb_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
OnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "miniserde")]
|
||||
impl<T> OnceResource<T, MiniserdeCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
MiniserdeCodec: Encoder<T> + Decoder<T>,
|
||||
<MiniserdeCodec as Encoder<T>>::Error: Debug,
|
||||
<MiniserdeCodec as Decoder<T>>::Error: Debug,
|
||||
<<MiniserdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_miniserde(
|
||||
fut: impl Future<Output = T> + Send + 'static,
|
||||
) -> Self {
|
||||
OnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_miniserde_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static,
|
||||
) -> Self {
|
||||
OnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde-lite")]
|
||||
impl<T> OnceResource<T, SerdeLite<JsonSerdeCodec>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
SerdeLite<JsonSerdeCodec>: Encoder<T> + Decoder<T>,
|
||||
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Error: Debug, <SerdeLite<JsonSerdeCodec> as Decoder<T>>::Error: Debug,
|
||||
<<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_serde_lite(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
OnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_serde_lite_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static
|
||||
) -> Self
|
||||
{
|
||||
OnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rkyv")]
|
||||
impl<T> OnceResource<T, RkyvCodec>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
RkyvCodec: Encoder<T> + Decoder<T>,
|
||||
<RkyvCodec as Encoder<T>>::Error: Debug,
|
||||
<RkyvCodec as Decoder<T>>::Error: Debug,
|
||||
<<RkyvCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn new_rkyv(fut: impl Future<Output = T> + Send + 'static) -> Self {
|
||||
OnceResource::new_with_options(fut, false)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_rkyv_blocking(
|
||||
fut: impl Future<Output = T> + Send + 'static,
|
||||
) -> Self {
|
||||
OnceResource::new_with_options(fut, true)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use codee::{
|
||||
};
|
||||
use core::{fmt::Debug, marker::PhantomData};
|
||||
use futures::Future;
|
||||
use hydration_context::SerializedDataId;
|
||||
use hydration_context::{SerializedDataId, SharedContext};
|
||||
use reactive_graph::{
|
||||
computed::{
|
||||
ArcAsyncDerived, ArcMemo, AsyncDerived, AsyncDerivedFuture,
|
||||
@@ -24,7 +24,39 @@ use reactive_graph::{
|
||||
prelude::*,
|
||||
signal::{ArcRwSignal, RwSignal},
|
||||
};
|
||||
use std::{future::IntoFuture, ops::Deref, panic::Location};
|
||||
use std::{
|
||||
future::{pending, IntoFuture},
|
||||
ops::Deref,
|
||||
panic::Location,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) static IS_SUPPRESSING_RESOURCE_LOAD: AtomicBool =
|
||||
AtomicBool::new(false);
|
||||
|
||||
pub struct SuppressResourceLoad;
|
||||
|
||||
impl SuppressResourceLoad {
|
||||
pub fn new() -> Self {
|
||||
IS_SUPPRESSING_RESOURCE_LOAD.store(true, Ordering::Relaxed);
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SuppressResourceLoad {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SuppressResourceLoad {
|
||||
fn drop(&mut self) {
|
||||
IS_SUPPRESSING_RESOURCE_LOAD.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArcResource<T, Ser = JsonSerdeCodec> {
|
||||
ser: PhantomData<Ser>,
|
||||
@@ -44,6 +76,38 @@ impl<T, Ser> Debug for ArcResource<T, Ser> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> From<ArcResource<T, Ser>> for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(arc_resource: ArcResource<T, Ser>) -> Self {
|
||||
Resource {
|
||||
ser: PhantomData,
|
||||
data: arc_resource.data.into(),
|
||||
refetch: arc_resource.refetch.into(),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> From<Resource<T, Ser>> for ArcResource<T, Ser>
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(resource: Resource<T, Ser>) -> Self {
|
||||
ArcResource {
|
||||
ser: PhantomData,
|
||||
data: resource.data.into(),
|
||||
refetch: resource.refetch.into(),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> DefinedAt for ArcResource<T, Ser> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -77,6 +141,50 @@ impl<T, Ser> Deref for ArcResource<T, Ser> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Track for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn track(&self) {
|
||||
self.data.track();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = <ArcAsyncDerived<T> as ReadUntracked>::Value;
|
||||
|
||||
#[track_caller]
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
#[cfg(all(feature = "hydration", debug_assertions))]
|
||||
{
|
||||
use reactive_graph::{
|
||||
computed::suspense::SuspenseContext, effect::in_effect_scope,
|
||||
owner::use_context,
|
||||
};
|
||||
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
reactive_graph::log_warning(format_args!(
|
||||
"At {location}, you are reading a resource in `hydrate` \
|
||||
mode outside a <Suspense/> or <Transition/> or effect. \
|
||||
This can cause hydration mismatch errors and loses out \
|
||||
on a significant performance optimization. To fix this \
|
||||
issue, you can either: \n1. Wrap the place where you \
|
||||
read the resource in a <Suspense/> or <Transition/> \
|
||||
component, or \n2. Switch to using \
|
||||
ArcLocalResource::new(), which will wait to load the \
|
||||
resource until the app is hydrated on the client side. \
|
||||
(This will have worse performance in most cases.)",
|
||||
));
|
||||
}
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ArcResource<T, Ser>
|
||||
where
|
||||
Ser: Encoder<T> + Decoder<T>,
|
||||
@@ -104,7 +212,7 @@ where
|
||||
.map(|sc| sc.next_id())
|
||||
.unwrap_or_default();
|
||||
|
||||
let initial = Self::initial_value(&id);
|
||||
let initial = initial_value::<T, Ser>(&id, shared_context.as_ref());
|
||||
let is_ready = initial.is_some();
|
||||
|
||||
let refetch = ArcRwSignal::new(0);
|
||||
@@ -116,7 +224,14 @@ where
|
||||
let source = source.clone();
|
||||
move || {
|
||||
let (_, source) = source.get();
|
||||
fetcher(source)
|
||||
let fut = fetcher(source);
|
||||
async move {
|
||||
if IS_SUPPRESSING_RESOURCE_LOAD.load(Ordering::Relaxed) {
|
||||
pending().await
|
||||
} else {
|
||||
fut.await
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,43 +290,53 @@ where
|
||||
pub fn refetch(&self) {
|
||||
*self.refetch.write() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[allow(unused)]
|
||||
fn initial_value(id: &SerializedDataId) -> Option<T> {
|
||||
#[cfg(feature = "hydration")]
|
||||
{
|
||||
use std::borrow::Borrow;
|
||||
#[inline(always)]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn initial_value<T, Ser>(
|
||||
id: &SerializedDataId,
|
||||
shared_context: Option<&Arc<dyn SharedContext + Send + Sync>>,
|
||||
) -> Option<T>
|
||||
where
|
||||
Ser: Encoder<T> + Decoder<T>,
|
||||
<Ser as Encoder<T>>::Error: Debug,
|
||||
<Ser as Decoder<T>>::Error: Debug,
|
||||
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
|
||||
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
|
||||
{
|
||||
#[cfg(feature = "hydration")]
|
||||
{
|
||||
use std::borrow::Borrow;
|
||||
|
||||
let shared_context = Owner::current_shared_context();
|
||||
if let Some(shared_context) = shared_context {
|
||||
let value = shared_context.read_data(id);
|
||||
if let Some(value) = value {
|
||||
let encoded =
|
||||
match <Ser as Decoder<T>>::Encoded::from_encoded_str(
|
||||
&value,
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("couldn't deserialize: {e:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let encoded = encoded.borrow();
|
||||
match Ser::decode(encoded) {
|
||||
Ok(value) => return Some(value),
|
||||
#[allow(unused)]
|
||||
let shared_context = Owner::current_shared_context();
|
||||
if let Some(shared_context) = shared_context {
|
||||
let value = shared_context.read_data(id);
|
||||
if let Some(value) = value {
|
||||
let encoded =
|
||||
match <Ser as Decoder<T>>::Encoded::from_encoded_str(&value)
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("couldn't deserialize: {e:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let encoded = encoded.borrow();
|
||||
match Ser::decode(encoded) {
|
||||
Ok(value) => return Some(value),
|
||||
#[allow(unused)]
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("couldn't deserialize: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl<T, E, Ser> ArcResource<Result<T, E>, Ser>
|
||||
@@ -529,6 +654,50 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Track for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
fn track(&self) {
|
||||
self.data.track();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
type Value = <AsyncDerived<T> as ReadUntracked>::Value;
|
||||
|
||||
#[track_caller]
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
#[cfg(all(feature = "hydration", debug_assertions))]
|
||||
{
|
||||
use reactive_graph::{
|
||||
computed::suspense::SuspenseContext, effect::in_effect_scope,
|
||||
owner::use_context,
|
||||
};
|
||||
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
reactive_graph::log_warning(format_args!(
|
||||
"At {location}, you are reading a resource in `hydrate` \
|
||||
mode outside a <Suspense/> or <Transition/> or effect. \
|
||||
This can cause hydration mismatch errors and loses out \
|
||||
on a significant performance optimization. To fix this \
|
||||
issue, you can either: \n1. Wrap the place where you \
|
||||
read the resource in a <Suspense/> or <Transition/> \
|
||||
component, or \n2. Switch to using LocalResource::new(), \
|
||||
which will wait to load the resource until the app is \
|
||||
hydrated on the client side. (This will have worse \
|
||||
performance in most cases.)",
|
||||
));
|
||||
}
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Resource<T, FromToStringCodec>
|
||||
where
|
||||
FromToStringCodec: Encoder<T> + Decoder<T>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.7.0-beta5"
|
||||
version = "0.7.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -2,12 +2,11 @@ use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::NextAttribute,
|
||||
component, html,
|
||||
reactive_graph::owner::use_context,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
dom::document,
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
@@ -56,14 +55,14 @@ struct BodyView<At> {
|
||||
|
||||
struct BodyViewState<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
attributes: At::State,
|
||||
}
|
||||
|
||||
impl<At> Render<Dom> for BodyView<At>
|
||||
impl<At> Render for BodyView<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
type State = BodyViewState<At>;
|
||||
|
||||
@@ -79,19 +78,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> AddAnyAttr<Dom> for BodyView<At>
|
||||
impl<At> AddAnyAttr for BodyView<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Dom>> =
|
||||
BodyView<<At as NextAttribute<Dom>>::Output<SomeNewAttr>>;
|
||||
type Output<SomeNewAttr: Attribute> =
|
||||
BodyView<<At as NextAttribute>::Output<SomeNewAttr>>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Dom>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Dom>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
BodyView {
|
||||
attributes: self.attributes.add_any_attr(attr),
|
||||
@@ -99,9 +98,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> RenderHtml<Dom> for BodyView<At>
|
||||
impl<At> RenderHtml for BodyView<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = BodyView<At::AsyncOutput>;
|
||||
|
||||
@@ -135,7 +134,7 @@ where
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
_cursor: &Cursor<Dom>,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
let el = document().body().expect("there to be a <body> element");
|
||||
@@ -145,20 +144,20 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> Mountable<Dom> for BodyViewState<At>
|
||||
impl<At> Mountable for BodyViewState<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
fn unmount(&mut self) {}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
_parent: &<Dom as Renderer>::Element,
|
||||
_marker: Option<&<Dom as Renderer>::Node>,
|
||||
_parent: &leptos::tachys::renderer::types::Element,
|
||||
_marker: Option<&leptos::tachys::renderer::types::Node>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable<Dom>) -> bool {
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::NextAttribute,
|
||||
component, html,
|
||||
reactive_graph::owner::use_context,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
dom::document,
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
@@ -53,14 +52,14 @@ struct HtmlView<At> {
|
||||
|
||||
struct HtmlViewState<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
attributes: At::State,
|
||||
}
|
||||
|
||||
impl<At> Render<Dom> for HtmlView<At>
|
||||
impl<At> Render for HtmlView<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
type State = HtmlViewState<At>;
|
||||
|
||||
@@ -79,19 +78,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> AddAnyAttr<Dom> for HtmlView<At>
|
||||
impl<At> AddAnyAttr for HtmlView<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Dom>> =
|
||||
HtmlView<<At as NextAttribute<Dom>>::Output<SomeNewAttr>>;
|
||||
type Output<SomeNewAttr: Attribute> =
|
||||
HtmlView<<At as NextAttribute>::Output<SomeNewAttr>>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Dom>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Dom>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
HtmlView {
|
||||
attributes: self.attributes.add_any_attr(attr),
|
||||
@@ -99,9 +98,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> RenderHtml<Dom> for HtmlView<At>
|
||||
impl<At> RenderHtml for HtmlView<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = HtmlView<At::AsyncOutput>;
|
||||
|
||||
@@ -135,7 +134,7 @@ where
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
_cursor: &Cursor<Dom>,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
let el = document()
|
||||
@@ -148,22 +147,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> Mountable<Dom> for HtmlViewState<At>
|
||||
impl<At> Mountable for HtmlViewState<At>
|
||||
where
|
||||
At: Attribute<Dom>,
|
||||
At: Attribute,
|
||||
{
|
||||
fn unmount(&mut self) {}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
_parent: &<Dom as Renderer>::Element,
|
||||
_marker: Option<&<Dom as Renderer>::Node>,
|
||||
_parent: &leptos::tachys::renderer::types::Element,
|
||||
_marker: Option<&leptos::tachys::renderer::types::Node>,
|
||||
) {
|
||||
// <Html> only sets attributes
|
||||
// the <html> tag doesn't need to be mounted anywhere, of course
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable<Dom>) -> bool {
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,15 +52,14 @@ use leptos::{
|
||||
attr::NextAttribute,
|
||||
component,
|
||||
logging::debug_warn,
|
||||
reactive_graph::owner::{provide_context, use_context},
|
||||
reactive::owner::{provide_context, use_context},
|
||||
tachys::{
|
||||
dom::document,
|
||||
html::{
|
||||
attribute::Attribute,
|
||||
element::{CreateElement, ElementType, HtmlElement},
|
||||
element::{ElementType, HtmlElement},
|
||||
},
|
||||
hydration::Cursor,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
@@ -106,7 +105,7 @@ pub struct MetaContext {
|
||||
/// Metadata associated with the `<title>` element.
|
||||
pub(crate) title: TitleContext,
|
||||
/// The hydration cursor for the location in the `<head>` for arbitrary tags will be rendered.
|
||||
pub(crate) cursor: Arc<Lazy<SendWrapper<Cursor<Dom>>>>,
|
||||
pub(crate) cursor: Arc<Lazy<SendWrapper<Cursor>>>,
|
||||
}
|
||||
|
||||
impl MetaContext {
|
||||
@@ -123,7 +122,7 @@ const COMMENT_NODE: u16 = 8;
|
||||
|
||||
impl Default for MetaContext {
|
||||
fn default() -> Self {
|
||||
let build_cursor: fn() -> SendWrapper<Cursor<Dom>> = || {
|
||||
let build_cursor: fn() -> SendWrapper<Cursor> = || {
|
||||
let head = document().head().expect("missing <head> element");
|
||||
let mut cursor = None;
|
||||
let mut child = head.first_child();
|
||||
@@ -323,10 +322,10 @@ pub fn use_head() -> MetaContext {
|
||||
}
|
||||
|
||||
pub(crate) fn register<E, At, Ch>(
|
||||
el: HtmlElement<E, At, Ch, Dom>,
|
||||
el: HtmlElement<E, At, Ch>,
|
||||
) -> RegisteredMetaTag<E, At, Ch>
|
||||
where
|
||||
HtmlElement<E, At, Ch, Dom>: RenderHtml<Dom>,
|
||||
HtmlElement<E, At, Ch>: RenderHtml,
|
||||
{
|
||||
#[allow(unused_mut)] // used for `ssr`
|
||||
let mut el = Some(el);
|
||||
@@ -358,19 +357,19 @@ where
|
||||
struct RegisteredMetaTag<E, At, Ch> {
|
||||
// this is `None` if we've already taken it out to render to HTML on the server
|
||||
// we don't render it in place in RenderHtml, so it's fine
|
||||
el: Option<HtmlElement<E, At, Ch, Dom>>,
|
||||
el: Option<HtmlElement<E, At, Ch>>,
|
||||
}
|
||||
|
||||
struct RegisteredMetaTagState<E, At, Ch>
|
||||
where
|
||||
HtmlElement<E, At, Ch, Dom>: Render<Dom>,
|
||||
HtmlElement<E, At, Ch>: Render,
|
||||
{
|
||||
state: <HtmlElement<E, At, Ch, Dom> as Render<Dom>>::State,
|
||||
state: <HtmlElement<E, At, Ch> as Render>::State,
|
||||
}
|
||||
|
||||
impl<E, At, Ch> Drop for RegisteredMetaTagState<E, At, Ch>
|
||||
where
|
||||
HtmlElement<E, At, Ch, Dom>: Render<Dom>,
|
||||
HtmlElement<E, At, Ch>: Render,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
self.state.unmount();
|
||||
@@ -387,11 +386,11 @@ fn document_head() -> HtmlHeadElement {
|
||||
})
|
||||
}
|
||||
|
||||
impl<E, At, Ch> Render<Dom> for RegisteredMetaTag<E, At, Ch>
|
||||
impl<E, At, Ch> Render for RegisteredMetaTag<E, At, Ch>
|
||||
where
|
||||
E: ElementType + CreateElement<Dom>,
|
||||
At: Attribute<Dom>,
|
||||
Ch: Render<Dom>,
|
||||
E: ElementType,
|
||||
At: Attribute,
|
||||
Ch: Render,
|
||||
{
|
||||
type State = RegisteredMetaTagState<E, At, Ch>;
|
||||
|
||||
@@ -405,24 +404,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, At, Ch> AddAnyAttr<Dom> for RegisteredMetaTag<E, At, Ch>
|
||||
impl<E, At, Ch> AddAnyAttr for RegisteredMetaTag<E, At, Ch>
|
||||
where
|
||||
E: ElementType + CreateElement<Dom> + Send,
|
||||
At: Attribute<Dom> + Send,
|
||||
Ch: RenderHtml<Dom> + Send,
|
||||
E: ElementType + Send,
|
||||
At: Attribute + Send,
|
||||
Ch: RenderHtml + Send,
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute<Dom>> = RegisteredMetaTag<
|
||||
E,
|
||||
<At as NextAttribute<Dom>>::Output<SomeNewAttr>,
|
||||
Ch,
|
||||
>;
|
||||
type Output<SomeNewAttr: Attribute> =
|
||||
RegisteredMetaTag<E, <At as NextAttribute>::Output<SomeNewAttr>, Ch>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Dom>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Dom>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
RegisteredMetaTag {
|
||||
el: self.el.map(|inner| inner.add_any_attr(attr)),
|
||||
@@ -430,11 +426,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, At, Ch> RenderHtml<Dom> for RegisteredMetaTag<E, At, Ch>
|
||||
impl<E, At, Ch> RenderHtml for RegisteredMetaTag<E, At, Ch>
|
||||
where
|
||||
E: ElementType + CreateElement<Dom>,
|
||||
At: Attribute<Dom>,
|
||||
Ch: RenderHtml<Dom> + Send,
|
||||
E: ElementType,
|
||||
At: Attribute,
|
||||
Ch: RenderHtml + Send,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
|
||||
@@ -461,7 +457,7 @@ where
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
_cursor: &Cursor<Dom>,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
let cursor = use_context::<MetaContext>()
|
||||
@@ -471,18 +467,18 @@ where
|
||||
)
|
||||
.cursor;
|
||||
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
|
||||
&*cursor,
|
||||
&cursor,
|
||||
&PositionState::new(Position::NextChild),
|
||||
);
|
||||
RegisteredMetaTagState { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, At, Ch> Mountable<Dom> for RegisteredMetaTagState<E, At, Ch>
|
||||
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
|
||||
where
|
||||
E: ElementType + CreateElement<Dom>,
|
||||
At: Attribute<Dom>,
|
||||
Ch: Render<Dom>,
|
||||
E: ElementType,
|
||||
At: Attribute,
|
||||
Ch: Render,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
self.state.unmount();
|
||||
@@ -490,8 +486,8 @@ where
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
_parent: &<Dom as Renderer>::Element,
|
||||
_marker: Option<&<Dom as Renderer>::Node>,
|
||||
_parent: &leptos::tachys::renderer::types::Element,
|
||||
_marker: Option<&leptos::tachys::renderer::types::Node>,
|
||||
) {
|
||||
// we always mount this to the <head>, which is the whole point
|
||||
// but this shouldn't warn about the parent being a regular element or being unused
|
||||
@@ -500,7 +496,7 @@ where
|
||||
self.state.mount(&document_head(), None);
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable<Dom>) -> bool {
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
// Registered meta tags will be mounted in the <head>, but *seem* to be mounted somewhere
|
||||
// else in the DOM. We should never tell the renderer that we have successfully mounted
|
||||
// something before this, because if e.g., a <Meta/> is the first item in an Either, then
|
||||
@@ -525,7 +521,7 @@ struct MetaTagsView;
|
||||
// rendering HTML for all the tags that will be injected into the `<head>`
|
||||
//
|
||||
// client-side rendering is handled by the individual components
|
||||
impl Render<Dom> for MetaTagsView {
|
||||
impl Render for MetaTagsView {
|
||||
type State = ();
|
||||
|
||||
fn build(self) -> Self::State {}
|
||||
@@ -533,21 +529,21 @@ impl Render<Dom> for MetaTagsView {
|
||||
fn rebuild(self, _state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
impl AddAnyAttr<Dom> for MetaTagsView {
|
||||
type Output<SomeNewAttr: Attribute<Dom>> = MetaTagsView;
|
||||
impl AddAnyAttr for MetaTagsView {
|
||||
type Output<SomeNewAttr: Attribute> = MetaTagsView;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Dom>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
_attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Dom>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderHtml<Dom> for MetaTagsView {
|
||||
impl RenderHtml for MetaTagsView {
|
||||
type AsyncOutput = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
@@ -570,7 +566,7 @@ impl RenderHtml<Dom> for MetaTagsView {
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
_cursor: &Cursor<Dom>,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@ use leptos::{
|
||||
attr::Attribute,
|
||||
component,
|
||||
oco::Oco,
|
||||
reactive_graph::{
|
||||
reactive::{
|
||||
effect::RenderEffect,
|
||||
owner::{use_context, Owner},
|
||||
},
|
||||
tachys::{
|
||||
dom::document,
|
||||
hydration::Cursor,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
@@ -187,7 +186,7 @@ struct TitleViewState {
|
||||
effect: RenderEffect<Oco<'static, str>>,
|
||||
}
|
||||
|
||||
impl Render<Dom> for TitleView {
|
||||
impl Render for TitleView {
|
||||
type State = TitleViewState;
|
||||
|
||||
fn build(mut self) -> Self::State {
|
||||
@@ -219,21 +218,21 @@ impl Render<Dom> for TitleView {
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAnyAttr<Dom> for TitleView {
|
||||
type Output<SomeNewAttr: Attribute<Dom>> = TitleView;
|
||||
impl AddAnyAttr for TitleView {
|
||||
type Output<SomeNewAttr: Attribute> = TitleView;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute<Dom>>(
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
_attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml<Dom>,
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderHtml<Dom> for TitleView {
|
||||
impl RenderHtml for TitleView {
|
||||
type AsyncOutput = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
@@ -257,7 +256,7 @@ impl RenderHtml<Dom> for TitleView {
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
mut self,
|
||||
_cursor: &Cursor<Dom>,
|
||||
_cursor: &Cursor,
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
let el = self.el();
|
||||
@@ -285,19 +284,19 @@ impl RenderHtml<Dom> for TitleView {
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<Dom> for TitleViewState {
|
||||
impl Mountable for TitleViewState {
|
||||
fn unmount(&mut self) {}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
_parent: &<Dom as Renderer>::Element,
|
||||
_marker: Option<&<Dom as Renderer>::Node>,
|
||||
_parent: &leptos::tachys::renderer::types::Element,
|
||||
_marker: Option<&leptos::tachys::renderer::types::Node>,
|
||||
) {
|
||||
// <title> doesn't need to be mounted
|
||||
// TitleView::el() guarantees that there is a <title> in the <head>
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable<Dom>) -> bool {
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "next_tuple"
|
||||
version = "0.1.0-beta5"
|
||||
version = "0.1.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -30,23 +30,21 @@ sqlx = { version = "0.8.0", features = [
|
||||
], optional = true }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2.0"
|
||||
axum_session_auth = { version = "0.14.0", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.14.0", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session_auth = { version = "0.14.0", features = [], optional = true }
|
||||
axum_session = { version = "0.14.0", features = [], optional = true }
|
||||
axum_session_sqlx = { version = "0.3.0", features = [ "sqlite", "tls-rustls"], optional = true }
|
||||
bcrypt = { version = "0.15.0", optional = true }
|
||||
async-trait = { version = "0.1.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:axum_session_sqlx",
|
||||
"dep:axum_session_auth",
|
||||
"dep:axum_session",
|
||||
"dep:async-trait",
|
||||
|
||||
@@ -28,9 +28,8 @@ impl Default for User {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
pub use super::{User, UserPasshash};
|
||||
pub use axum_session_auth::{
|
||||
Authentication, HasPermission, SessionSqlitePool,
|
||||
};
|
||||
pub use axum_session_auth::{Authentication, HasPermission};
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
pub use sqlx::SqlitePool;
|
||||
pub use std::collections::HashSet;
|
||||
pub type AuthSession = axum_session_auth::AuthSession<
|
||||
|
||||
@@ -8,10 +8,10 @@ use leptos_axum::ResponseOptions;
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
#[prop(optional)] errors: Option<ArcRwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => RwSignal::new(e),
|
||||
Some(e) => ArcRwSignal::new(e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, Errors, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(TodoAppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ pub mod auth;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod state;
|
||||
pub mod todo;
|
||||
|
||||
@@ -14,5 +12,5 @@ pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(TodoApp);
|
||||
leptos::mount::hydrate_body(TodoApp);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,16 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
|
||||
use leptos::{get_configuration, logging::log, provide_context};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer};
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use leptos::{
|
||||
config::get_configuration, logging::log, prelude::provide_context,
|
||||
};
|
||||
use leptos_axum::{
|
||||
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
|
||||
};
|
||||
use session_auth_axum::{
|
||||
auth::{ssr::AuthSession, User},
|
||||
fallback::file_and_error_handler,
|
||||
state::AppState,
|
||||
todo::*,
|
||||
};
|
||||
@@ -40,19 +42,19 @@ async fn server_fn_handler(
|
||||
|
||||
async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
state: State<AppState>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let State(app_state) = state.clone();
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
app_state.leptos_options.clone(),
|
||||
app_state.routes.clone(),
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
TodoApp,
|
||||
move || shell(app_state.leptos_options.clone()),
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
handler(state, req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -111,7 +113,7 @@ async fn main() {
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.fallback(file_and_error_handler)
|
||||
.fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
|
||||
.layer(
|
||||
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
|
||||
Some(pool.clone()),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::extract::FromRef;
|
||||
use leptos::LeptosOptions;
|
||||
use leptos_router::RouteListing;
|
||||
use leptos::prelude::LeptosOptions;
|
||||
use leptos_axum::AxumRouteListing;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
|
||||
@@ -9,5 +9,5 @@ use sqlx::SqlitePool;
|
||||
pub struct AppState {
|
||||
pub leptos_options: LeptosOptions,
|
||||
pub pool: SqlitePool,
|
||||
pub routes: Vec<RouteListing>,
|
||||
pub routes: Vec<AxumRouteListing>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{auth::*, error_template::ErrorTemplate};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_router::{components::*, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -109,13 +109,33 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options/>
|
||||
<link rel="stylesheet" id="leptos" href="/pkg/session_auth_axum.css"/>
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<TodoApp/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
let login = create_server_action::<Login>();
|
||||
let logout = create_server_action::<Logout>();
|
||||
let signup = create_server_action::<Signup>();
|
||||
let login = ServerAction::<Login>::new();
|
||||
let logout = ServerAction::<Logout>::new();
|
||||
let signup = ServerAction::<Signup>::new();
|
||||
|
||||
let user = create_resource(
|
||||
let user = Resource::new(
|
||||
move || {
|
||||
(
|
||||
login.version().get(),
|
||||
@@ -128,8 +148,6 @@ pub fn TodoApp() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/session_auth_axum.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<A href="/">
|
||||
@@ -149,7 +167,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
", "
|
||||
<span>{format!("Login error: {}", e)}</span>
|
||||
}
|
||||
.into_view()
|
||||
.into_any()
|
||||
}
|
||||
Ok(None) => {
|
||||
view! {
|
||||
@@ -159,7 +177,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
", "
|
||||
<span>"Logged out."</span>
|
||||
}
|
||||
.into_view()
|
||||
.into_any()
|
||||
}
|
||||
Ok(Some(user)) => {
|
||||
view! {
|
||||
@@ -169,7 +187,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
{format!("Logged in as: {} ({})", user.username, user.id)}
|
||||
</span>
|
||||
}
|
||||
.into_view()
|
||||
.into_any()
|
||||
}
|
||||
})
|
||||
}}
|
||||
@@ -178,13 +196,15 @@ pub fn TodoApp() -> impl IntoView {
|
||||
</header>
|
||||
<hr/>
|
||||
<main>
|
||||
<Routes>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
// Route
|
||||
<Route path="" view=Todos/>
|
||||
<Route path="signup" view=move || view! { <Signup action=signup/> }/>
|
||||
<Route path="login" view=move || view! { <Login action=login/> }/>
|
||||
<Route
|
||||
path="settings"
|
||||
<Route path=path!("") view=Todos/>
|
||||
<Route path=path!("signup") view=move || view! { <Signup action=signup/> }/>
|
||||
<Route path=path!("login") view=move || view! { <Login action=login/> }/>
|
||||
<ProtectedRoute
|
||||
path=path!("settings")
|
||||
condition=move || user.get().map(|r| r.ok().flatten().is_some())
|
||||
redirect_path=|| "/"
|
||||
view=move || {
|
||||
view! {
|
||||
<h1>"Settings"</h1>
|
||||
@@ -193,7 +213,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
|
||||
</Routes>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
@@ -201,12 +221,12 @@ pub fn TodoApp() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn Todos() -> impl IntoView {
|
||||
let add_todo = create_server_multi_action::<AddTodo>();
|
||||
let delete_todo = create_server_action::<DeleteTodo>();
|
||||
let add_todo = ServerMultiAction::<AddTodo>::new();
|
||||
let delete_todo = ServerAction::<DeleteTodo>::new();
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
let todos = Resource::new(
|
||||
move || (add_todo.version().get(), delete_todo.version().get()),
|
||||
move |_| get_todos(),
|
||||
);
|
||||
@@ -231,11 +251,11 @@ pub fn Todos() -> impl IntoView {
|
||||
view! {
|
||||
<pre class="error">"Server Error: " {e.to_string()}</pre>
|
||||
}
|
||||
.into_view()
|
||||
.into_any()
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { <p>"No tasks were found."</p> }.into_view()
|
||||
view! { <p>"No tasks were found."</p> }.into_any()
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
@@ -252,10 +272,11 @@ pub fn Todos() -> impl IntoView {
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.unwrap_or(().into_any())
|
||||
}
|
||||
};
|
||||
let pending_todos = move || {
|
||||
@@ -266,7 +287,7 @@ pub fn Todos() -> impl IntoView {
|
||||
.map(|submission| {
|
||||
view! {
|
||||
<li class="pending">
|
||||
{move || submission.input.get().map(|data| data.title)}
|
||||
{move || submission.input().get().map(|data| data.title)}
|
||||
</li>
|
||||
}
|
||||
})
|
||||
@@ -282,9 +303,7 @@ pub fn Todos() -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Login(
|
||||
action: Action<Login, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Login(action: ServerAction<Login>) -> impl IntoView {
|
||||
view! {
|
||||
<ActionForm action=action>
|
||||
<h1>"Log In"</h1>
|
||||
@@ -317,9 +336,7 @@ pub fn Login(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Signup(
|
||||
action: Action<Signup, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Signup(action: ServerAction<Signup>) -> impl IntoView {
|
||||
view! {
|
||||
<ActionForm action=action>
|
||||
<h1>"Sign Up"</h1>
|
||||
@@ -362,9 +379,7 @@ pub fn Signup(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Logout(
|
||||
action: Action<Logout, Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
pub fn Logout(action: ServerAction<Logout>) -> impl IntoView {
|
||||
view! {
|
||||
<div id="loginbox">
|
||||
<ActionForm action=action>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.1.0-beta5"
|
||||
version = "0.1.0-gamma3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::{
|
||||
computed::{ArcMemo, Memo},
|
||||
diagnostics::is_suppressing_resource_load,
|
||||
owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage},
|
||||
owner::{
|
||||
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
|
||||
SyncStorage,
|
||||
},
|
||||
signal::{ArcRwSignal, RwSignal},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, Update},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update},
|
||||
unwrap_signal,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
@@ -24,7 +27,7 @@ use std::{future::Future, panic::Location, pin::Pin, sync::Arc};
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -93,6 +96,7 @@ 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,
|
||||
@@ -108,6 +112,7 @@ 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,
|
||||
@@ -135,7 +140,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = ArcAction::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -191,6 +196,7 @@ 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(),
|
||||
@@ -230,14 +236,14 @@ where
|
||||
|
||||
// Update the state before loading
|
||||
self.in_flight.update(|n| *n += 1);
|
||||
let current_version =
|
||||
self.version.try_get_untracked().unwrap_or_default();
|
||||
let current_version = self.dispatched.get_value();
|
||||
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 {
|
||||
@@ -249,7 +255,7 @@ where
|
||||
// otherwise, update the value
|
||||
result = fut => {
|
||||
in_flight.update(|n| *n = n.saturating_sub(1));
|
||||
let is_latest = version.get_untracked() <= current_version;
|
||||
let is_latest = dispatched.get_value() <= current_version;
|
||||
if is_latest {
|
||||
version.update(|n| *n += 1);
|
||||
value.update(|n| *n = Some(result));
|
||||
@@ -282,8 +288,7 @@ where
|
||||
|
||||
// Update the state before loading
|
||||
self.in_flight.update(|n| *n += 1);
|
||||
let current_version =
|
||||
self.version.try_get_untracked().unwrap_or_default();
|
||||
let current_version = self.dispatched.get_value();
|
||||
self.input.try_update(|inp| *inp = Some(input));
|
||||
|
||||
// Spawn the task
|
||||
@@ -291,6 +296,7 @@ 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! {
|
||||
@@ -301,7 +307,7 @@ where
|
||||
// otherwise, update the value
|
||||
result = fut => {
|
||||
in_flight.update(|n| *n = n.saturating_sub(1));
|
||||
let is_latest = version.get_untracked() <= current_version;
|
||||
let is_latest = dispatched.get_value() <= current_version;
|
||||
if is_latest {
|
||||
version.update(|n| *n += 1);
|
||||
value.update(|n| *n = Some(result));
|
||||
@@ -351,6 +357,7 @@ 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)))
|
||||
}),
|
||||
@@ -367,7 +374,7 @@ impl<I, O> ArcAction<I, O> {
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = ArcAction::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -395,7 +402,7 @@ impl<I, O> ArcAction<I, O> {
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = ArcAction::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -425,7 +432,7 @@ impl<I, O> ArcAction<I, O> {
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = ArcAction::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -456,7 +463,7 @@ impl<I, O> ArcAction<I, O> {
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = ArcAction::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -510,7 +517,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -561,7 +568,7 @@ where
|
||||
/// function, because it is stored in [Action::input] as well.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::actions::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// // if there's a single argument, just use that
|
||||
/// let action1 = Action::new(|input: &String| {
|
||||
/// let input = input.clone();
|
||||
@@ -575,7 +582,7 @@ where
|
||||
/// let action3 = Action::new(|input: &(usize, String)| async { todo!() });
|
||||
/// ```
|
||||
pub struct Action<I, O, S = SyncStorage> {
|
||||
inner: StoredValue<ArcAction<I, O>, S>,
|
||||
inner: ArenaItem<ArcAction<I, O>, S>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -607,7 +614,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = Action::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -639,7 +646,7 @@ where
|
||||
Fu: Future<Output = O> + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: StoredValue::new(ArcAction::new(action_fn)),
|
||||
inner: ArenaItem::new(ArcAction::new(action_fn)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
@@ -664,9 +671,7 @@ where
|
||||
Fu: Future<Output = O> + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: StoredValue::new(ArcAction::new_with_value(
|
||||
value, action_fn,
|
||||
)),
|
||||
inner: ArenaItem::new(ArcAction::new_with_value(value, action_fn)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
@@ -688,7 +693,7 @@ where
|
||||
Fu: Future<Output = O> + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: StoredValue::new_local(ArcAction::new_unsync(action_fn)),
|
||||
inner: ArenaItem::new_local(ArcAction::new_unsync(action_fn)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
@@ -704,7 +709,7 @@ where
|
||||
Fu: Future<Output = O> + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: StoredValue::new_local(ArcAction::new_unsync_with_value(
|
||||
inner: ArenaItem::new_local(ArcAction::new_unsync_with_value(
|
||||
value, action_fn,
|
||||
)),
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -723,7 +728,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = Action::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -754,7 +759,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = Action::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -793,7 +798,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = ArcAction::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -849,7 +854,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = Action::new(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
@@ -908,7 +913,9 @@ where
|
||||
/// Calls the `async` function with a reference to the input type as its argument.
|
||||
#[track_caller]
|
||||
pub fn dispatch(&self, input: I) -> ActionAbortHandle {
|
||||
self.inner.with_value(|inner| inner.dispatch(input))
|
||||
self.inner
|
||||
.try_with_value(|inner| inner.dispatch(input))
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,7 +928,9 @@ where
|
||||
/// Calls the `async` function with a reference to the input type as its argument.
|
||||
#[track_caller]
|
||||
pub fn dispatch_local(&self, input: I) -> ActionAbortHandle {
|
||||
self.inner.with_value(|inner| inner.dispatch_local(input))
|
||||
self.inner
|
||||
.try_with_value(|inner| inner.dispatch_local(input))
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -942,7 +951,7 @@ where
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: StoredValue::new_with_storage(ArcAction::new_unsync(
|
||||
inner: ArenaItem::new_with_storage(ArcAction::new_unsync(
|
||||
action_fn,
|
||||
)),
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -961,7 +970,7 @@ where
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: StoredValue::new_with_storage(
|
||||
inner: ArenaItem::new_with_storage(
|
||||
ArcAction::new_unsync_with_value(value, action_fn),
|
||||
),
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -1007,7 +1016,7 @@ impl<I, O, S> Copy for Action<I, O, S> {}
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// let act = create_action(|n: &u8| {
|
||||
/// let n = n.to_owned();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
diagnostics::is_suppressing_resource_load,
|
||||
owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage},
|
||||
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
|
||||
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||
traits::{DefinedAt, Dispose, GetUntracked, Set, Update},
|
||||
unwrap_signal,
|
||||
@@ -24,7 +24,7 @@ use std::{fmt::Debug, future::Future, panic::Location, pin::Pin, sync::Arc};
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -45,7 +45,7 @@ use std::{fmt::Debug, future::Future, panic::Location, pin::Pin, sync::Arc};
|
||||
/// # });
|
||||
/// ```
|
||||
pub struct MultiAction<I, O, S = SyncStorage> {
|
||||
inner: StoredValue<ArcMultiAction<I, O>, S>,
|
||||
inner: ArenaItem<ArcMultiAction<I, O>, S>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -105,7 +105,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// // if there's a single argument, just use that
|
||||
/// let action1 = MultiAction::new(|input: &String| {
|
||||
@@ -129,9 +129,7 @@ where
|
||||
Fut: Future<Output = O> + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: StoredValue::new_with_storage(ArcMultiAction::new(
|
||||
action_fn,
|
||||
)),
|
||||
inner: ArenaItem::new_with_storage(ArcMultiAction::new(action_fn)),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
@@ -153,7 +151,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -189,7 +187,7 @@ where
|
||||
/// ```
|
||||
pub fn dispatch(&self, input: I) {
|
||||
if !is_suppressing_resource_load() {
|
||||
self.inner.with_value(|inner| inner.dispatch(input));
|
||||
self.inner.try_with_value(|inner| inner.dispatch(input));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +202,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -232,7 +230,8 @@ where
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn dispatch_sync(&self, value: O) {
|
||||
self.inner.with_value(|inner| inner.dispatch_sync(value));
|
||||
self.inner
|
||||
.try_with_value(|inner| inner.dispatch_sync(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +245,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -287,7 +286,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -337,7 +336,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -404,7 +403,7 @@ impl<I, O> ArcMultiAction<I, O> {
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// // if there's a single argument, just use that
|
||||
/// let action1 = ArcMultiAction::new(|input: &String| {
|
||||
@@ -453,7 +452,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -530,7 +529,7 @@ where
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -580,7 +579,7 @@ impl<I, O> ArcMultiAction<I, O> {
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
@@ -610,7 +609,7 @@ impl<I, O> ArcMultiAction<I, O> {
|
||||
/// # use reactive_graph::actions::*;
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
/// async fn send_new_todo_to_api(task: String) -> usize {
|
||||
/// // do something...
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::{
|
||||
};
|
||||
pub use arc_memo::*;
|
||||
pub use async_derived::*;
|
||||
pub(crate) use inner::MemoInner;
|
||||
pub use memo::*;
|
||||
pub use selector::*;
|
||||
|
||||
@@ -34,7 +35,7 @@ pub use selector::*;
|
||||
/// In the example below, setting an auth token will only trigger
|
||||
/// the token signal, but none of the other derived signals.
|
||||
/// ```
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # use reactive_graph::prelude::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::effect::Effect;
|
||||
/// # use reactive_graph::signal::RwSignal;
|
||||
/// # use reactive_graph::computed::*;
|
||||
|
||||
@@ -40,7 +40,7 @@ use std::{
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # use reactive_graph::prelude::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::computed::*;
|
||||
/// # use reactive_graph::signal::signal;
|
||||
/// # fn really_expensive_computation(value: i32) -> i32 { value };
|
||||
@@ -132,10 +132,7 @@ where
|
||||
pub fn new_with_compare(
|
||||
fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static,
|
||||
changed: fn(Option<&T>, Option<&T>) -> bool,
|
||||
) -> Self
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
) -> Self {
|
||||
Self::new_owning(move |prev: Option<T>| {
|
||||
let new_value = fun(prev.as_ref());
|
||||
let changed = changed(prev.as_ref(), Some(&new_value));
|
||||
@@ -157,10 +154,7 @@ where
|
||||
)]
|
||||
pub fn new_owning(
|
||||
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
) -> Self {
|
||||
let inner = Arc::new_cyclic(|weak| {
|
||||
let subscriber = AnySubscriber(
|
||||
weak.as_ptr() as usize,
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
|
||||
Writeable,
|
||||
Write,
|
||||
},
|
||||
transition::AsyncTransition,
|
||||
};
|
||||
@@ -53,10 +53,10 @@ use std::{
|
||||
/// ## Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::computed::*;
|
||||
/// # use reactive_graph::signal::*;
|
||||
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
///
|
||||
/// let signal1 = RwSignal::new(0);
|
||||
@@ -534,11 +534,11 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
|
||||
/// Returns a `Future` that is ready when this resource has next finished loading.
|
||||
pub fn ready(&self) -> AsyncDerivedReadyFuture {
|
||||
AsyncDerivedReadyFuture {
|
||||
source: self.to_any_source(),
|
||||
loading: Arc::clone(&self.loading),
|
||||
wakers: Arc::clone(&self.wakers),
|
||||
}
|
||||
AsyncDerivedReadyFuture::new(
|
||||
self.to_any_source(),
|
||||
&self.loading,
|
||||
&self.wakers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@ impl<T: 'static> Notify for ArcAsyncDerived<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Writeable for ArcAsyncDerived<T> {
|
||||
impl<T: 'static> Write for ArcAsyncDerived<T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
|
||||
@@ -4,11 +4,11 @@ use crate::{
|
||||
AnySource, AnySubscriber, ReactiveNode, Source, Subscriber,
|
||||
ToAnySource, ToAnySubscriber,
|
||||
},
|
||||
owner::{FromLocal, LocalStorage, Storage, StoredValue, SyncStorage},
|
||||
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
|
||||
signal::guards::{AsyncPlain, ReadGuard, WriteGuard},
|
||||
traits::{
|
||||
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
|
||||
UntrackableGuard, Writeable,
|
||||
UntrackableGuard, Write,
|
||||
},
|
||||
unwrap_signal,
|
||||
};
|
||||
@@ -29,10 +29,10 @@ use std::{future::Future, ops::DerefMut, panic::Location};
|
||||
/// ## Examples
|
||||
/// ```rust
|
||||
/// # use reactive_graph::computed::*;
|
||||
/// # use reactive_graph::signal::*;
|
||||
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # tokio_test::block_on(async move {
|
||||
/// # any_spawner::Executor::init_tokio();
|
||||
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
|
||||
///
|
||||
/// let signal1 = RwSignal::new(0);
|
||||
@@ -85,7 +85,7 @@ use std::{future::Future, ops::DerefMut, panic::Location};
|
||||
pub struct AsyncDerived<T, S = SyncStorage> {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
pub(crate) inner: StoredValue<ArcAsyncDerived<T>, S>,
|
||||
pub(crate) inner: ArenaItem<ArcAsyncDerived<T>, S>,
|
||||
}
|
||||
|
||||
impl<T, S> Dispose for AsyncDerived<T, S> {
|
||||
@@ -104,11 +104,24 @@ where
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
inner: StoredValue::new_with_storage(value),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<AsyncDerived<T>> for ArcAsyncDerived<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: AsyncDerived<T>) -> Self {
|
||||
value
|
||||
.inner
|
||||
.try_get_value()
|
||||
.unwrap_or_else(unwrap_signal!(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromLocal<ArcAsyncDerived<T>> for AsyncDerived<T, LocalStorage>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -119,7 +132,7 @@ where
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
inner: StoredValue::new_with_storage(value),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +154,7 @@ where
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: StoredValue::new_with_storage(ArcAsyncDerived::new(fun)),
|
||||
inner: ArenaItem::new_with_storage(ArcAsyncDerived::new(fun)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +172,7 @@ where
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: StoredValue::new_with_storage(
|
||||
inner: ArenaItem::new_with_storage(
|
||||
ArcAsyncDerived::new_with_initial(initial_value, fun),
|
||||
),
|
||||
}
|
||||
@@ -176,9 +189,7 @@ impl<T> AsyncDerived<SendWrapper<T>> {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: StoredValue::new_with_storage(ArcAsyncDerived::new_mock(
|
||||
fun,
|
||||
)),
|
||||
inner: ArenaItem::new_with_storage(ArcAsyncDerived::new_mock(fun)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +211,7 @@ where
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: StoredValue::new_with_storage(ArcAsyncDerived::new_unsync(
|
||||
inner: ArenaItem::new_with_storage(ArcAsyncDerived::new_unsync(
|
||||
fun,
|
||||
)),
|
||||
}
|
||||
@@ -221,7 +232,7 @@ where
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
inner: StoredValue::new_with_storage(
|
||||
inner: ArenaItem::new_with_storage(
|
||||
ArcAsyncDerived::new_unsync_with_initial(initial_value, fun),
|
||||
),
|
||||
}
|
||||
@@ -302,7 +313,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Writeable for AsyncDerived<T, S>
|
||||
impl<T, S> Write for AsyncDerived<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<ArcAsyncDerived<T>>,
|
||||
|
||||
@@ -34,6 +34,21 @@ pub struct AsyncDerivedReadyFuture {
|
||||
pub(crate) wakers: Arc<RwLock<Vec<Waker>>>,
|
||||
}
|
||||
|
||||
impl AsyncDerivedReadyFuture {
|
||||
/// Creates a new [`Future`] that will be ready when the given resource is ready.
|
||||
pub fn new(
|
||||
source: AnySource,
|
||||
loading: &Arc<AtomicBool>,
|
||||
wakers: &Arc<RwLock<Vec<Waker>>>,
|
||||
) -> Self {
|
||||
AsyncDerivedReadyFuture {
|
||||
source,
|
||||
loading: Arc::clone(loading),
|
||||
wakers: Arc::clone(wakers),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for AsyncDerivedReadyFuture {
|
||||
type Output = ();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user