mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 09:02:37 -05:00
Compare commits
47 Commits
cleanup-te
...
v0.7.0-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b8cd90a6e | ||
|
|
d0ef7b904d | ||
|
|
7904e0c395 | ||
|
|
7b4c470155 | ||
|
|
98eccc9eb8 | ||
|
|
70d06e2716 | ||
|
|
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 |
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-gamma"
|
||||
version = "0.7.0-rc0"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-gamma" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-rc0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-gamma" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-gamma" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-gamma" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-rc0" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-rc0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-rc0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-rc0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-rc0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-rc0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-rc0" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-rc0" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-rc0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-rc0" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-rc0" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-rc0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-gamma" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-gamma" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-rc0" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-rc0" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-rc0" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-rc0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-rc0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-rc0" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-rc0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0-gamma"
|
||||
version = "0.2.0-rc0"
|
||||
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);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -28,9 +28,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -56,7 +56,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -46,9 +46,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -42,9 +42,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -4,7 +4,7 @@ mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
@@ -46,9 +46,7 @@ pub fn App() -> impl IntoView {
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
<Route path=OptionalParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -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,7 +70,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
|
||||
// You can define other routes in their own component.
|
||||
// Routes implement the MatchNestedRoutes
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("") view=ContactList>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -39,7 +39,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use chrono::{Local, NaiveDate};
|
||||
use leptos::prelude::*;
|
||||
use reactive_stores::{Field, Patch, Store};
|
||||
use reactive_stores_macro::{Patch, Store};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ID starts higher than 0 because we have a few starting todos by default
|
||||
|
||||
@@ -41,7 +41,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
})
|
||||
.bind(addr)?
|
||||
.workers(1)
|
||||
|
||||
@@ -40,7 +40,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -59,7 +59,7 @@ async fn main() -> std::io::Result<()> {
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
.service(Files::new("/", site_root.as_ref()))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.2.0-gamma"
|
||||
version = "0.2.0-rc0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -35,13 +35,14 @@ use leptos_router::{
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, ResolvedStaticPath},
|
||||
Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use send_wrapper::SendWrapper;
|
||||
use server_fn::{
|
||||
redirect::REDIRECT_HEADER, request::actix::ActixRequest, ServerFnError,
|
||||
actix::unregister_server_fns, redirect::REDIRECT_HEADER,
|
||||
request::actix::ActixRequest, ServerFnError,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
@@ -900,7 +901,7 @@ trait ActixPath {
|
||||
fn to_actix_path(&self) -> String;
|
||||
}
|
||||
|
||||
impl ActixPath for &[PathSegment] {
|
||||
impl ActixPath for Vec<PathSegment> {
|
||||
fn to_actix_path(&self) -> String {
|
||||
let mut path = String::new();
|
||||
for segment in self.iter() {
|
||||
@@ -922,6 +923,14 @@ impl ActixPath for &[PathSegment] {
|
||||
path.push_str(":.*}");
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!(
|
||||
"to_axum_path should only be called on expanded \
|
||||
paths, which do not have OptionalParam any longer"
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
@@ -937,23 +946,34 @@ pub struct ActixRouteListing {
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
}
|
||||
|
||||
impl From<RouteListing> for ActixRouteListing {
|
||||
fn from(value: RouteListing) -> Self {
|
||||
let path = value.path().to_actix_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
trait IntoRouteListing: Sized {
|
||||
fn into_route_listing(self) -> Vec<ActixRouteListing>;
|
||||
}
|
||||
|
||||
impl IntoRouteListing for RouteListing {
|
||||
fn into_route_listing(self) -> Vec<ActixRouteListing> {
|
||||
self.path()
|
||||
.to_vec()
|
||||
.expand_optionals()
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let path = path.to_actix_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = self.mode();
|
||||
let methods = self.methods().collect();
|
||||
let regenerate = self.regenerate().into();
|
||||
ActixRouteListing {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1004,6 +1024,11 @@ where
|
||||
{
|
||||
let _ = any_spawner::Executor::init_tokio();
|
||||
|
||||
// remove any server fns that match excluded paths
|
||||
if let Some(excluded) = &excluded_routes {
|
||||
unregister_server_fns(excluded);
|
||||
}
|
||||
|
||||
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
|
||||
let (mock_meta, _) = ServerMetaContext::new();
|
||||
let routes = owner
|
||||
@@ -1027,7 +1052,7 @@ where
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(ActixRouteListing::from)
|
||||
.flat_map(IntoRouteListing::into_route_listing)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(
|
||||
@@ -1381,39 +1406,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,7 +24,6 @@ leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.39", default-features = false }
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
tower-http = "0.5.2"
|
||||
|
||||
@@ -66,12 +66,14 @@ use leptos_router::{
|
||||
components::provide_server_redirect,
|
||||
location::RequestUrl,
|
||||
static_routes::{RegenerationFn, StaticParamsMap},
|
||||
PathSegment, RouteList, RouteListing, SsrMode,
|
||||
ExpandOptionals, PathSegment, RouteList, RouteListing, SsrMode,
|
||||
};
|
||||
#[cfg(feature = "default")]
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
||||
use server_fn::{
|
||||
axum::unregister_server_fns, redirect::REDIRECT_HEADER, ServerFnError,
|
||||
};
|
||||
#[cfg(feature = "default")]
|
||||
use std::path::Path;
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
@@ -606,9 +608,9 @@ where
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// http::Request,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::{config::LeptosOptions, context::provide_context, prelude::*};
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
@@ -806,9 +808,9 @@ where
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// http::Request,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::context::provide_context;
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
@@ -1025,9 +1027,9 @@ where
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// http::Request,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::context::provide_context;
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
@@ -1093,9 +1095,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(
|
||||
@@ -1265,23 +1267,34 @@ pub struct AxumRouteListing {
|
||||
regenerate: Vec<RegenerationFn>,
|
||||
}
|
||||
|
||||
impl From<RouteListing> for AxumRouteListing {
|
||||
fn from(value: RouteListing) -> Self {
|
||||
let path = value.path().to_axum_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = value.mode();
|
||||
let methods = value.methods().collect();
|
||||
let regenerate = value.regenerate().into();
|
||||
Self {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
trait IntoRouteListing: Sized {
|
||||
fn into_route_listing(self) -> Vec<AxumRouteListing>;
|
||||
}
|
||||
|
||||
impl IntoRouteListing for RouteListing {
|
||||
fn into_route_listing(self) -> Vec<AxumRouteListing> {
|
||||
self.path()
|
||||
.to_vec()
|
||||
.expand_optionals()
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let path = path.to_axum_path();
|
||||
let path = if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let mode = self.mode();
|
||||
let methods = self.methods().collect();
|
||||
let regenerate = self.regenerate().into();
|
||||
AxumRouteListing {
|
||||
path,
|
||||
mode: mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1338,12 +1351,16 @@ where
|
||||
init_executor();
|
||||
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
|
||||
|
||||
// remove any server fns that match excluded paths
|
||||
if let Some(excluded) = &excluded_routes {
|
||||
unregister_server_fns(excluded);
|
||||
}
|
||||
|
||||
let routes = owner
|
||||
.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();
|
||||
@@ -1361,7 +1378,7 @@ where
|
||||
let mut routes = routes
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(AxumRouteListing::from)
|
||||
.flat_map(IntoRouteListing::into_route_listing)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(
|
||||
@@ -1402,8 +1419,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 +1512,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."
|
||||
);
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1692,7 +1711,7 @@ trait AxumPath {
|
||||
fn to_axum_path(&self) -> String;
|
||||
}
|
||||
|
||||
impl AxumPath for &[PathSegment] {
|
||||
impl AxumPath for Vec<PathSegment> {
|
||||
fn to_axum_path(&self) -> String {
|
||||
let mut path = String::new();
|
||||
for segment in self.iter() {
|
||||
@@ -1712,6 +1731,14 @@ impl AxumPath for &[PathSegment] {
|
||||
path.push_str(s);
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!(
|
||||
"to_axum_path should only be called on expanded \
|
||||
paths, which do not have OptionalParam any longer"
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
path
|
||||
@@ -1933,7 +1960,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 +2019,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 +2053,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,
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -87,7 +87,7 @@ where
|
||||
T: Send + Sync + Serialize + DeserializeOwned + 'static,
|
||||
Fut: std::future::Future<Output = T> + Send + 'static,
|
||||
Chil: FnOnce(&T) -> V + Send + 'static,
|
||||
V: IntoView,
|
||||
V: IntoView + 'static,
|
||||
{
|
||||
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
|
||||
let ready = res.ready();
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn HydrationScripts(
|
||||
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.join(&options.hash_file);
|
||||
.join(options.hash_file.as_ref());
|
||||
if hash_path.exists() {
|
||||
let hashes = std::fs::read_to_string(&hash_path)
|
||||
.expect("failed to read hash file");
|
||||
|
||||
@@ -174,7 +174,9 @@ pub mod prelude {
|
||||
pub use server_fn::{self, ServerFnError};
|
||||
pub use tachys::{
|
||||
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
|
||||
view::template::ViewTemplate,
|
||||
view::{
|
||||
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
|
||||
},
|
||||
};
|
||||
}
|
||||
pub use export_types::*;
|
||||
@@ -202,8 +204,9 @@ pub mod error {
|
||||
|
||||
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
|
||||
pub mod control_flow {
|
||||
pub use crate::{await_::*, for_loop::*, show::*};
|
||||
pub use crate::{animated_show::*, await_::*, for_loop::*, show::*};
|
||||
}
|
||||
mod animated_show;
|
||||
mod await_;
|
||||
mod for_loop;
|
||||
mod show;
|
||||
@@ -326,233 +329,3 @@ pub use tracing;
|
||||
pub use wasm_bindgen;
|
||||
#[doc(hidden)]
|
||||
pub use web_sys;
|
||||
|
||||
/*mod additional_attributes;
|
||||
pub use additional_attributes::*;
|
||||
pub use await_::*;
|
||||
pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
#[cfg(not(all(
|
||||
target_arch = "wasm32",
|
||||
any(feature = "csr", feature = "hydrate")
|
||||
)))]
|
||||
/// Utilities for server-side rendering HTML.
|
||||
pub mod ssr {
|
||||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, document, ev,
|
||||
helpers::{
|
||||
event_target, event_target_checked, event_target_value,
|
||||
request_animation_frame, request_animation_frame_with_handle,
|
||||
request_idle_callback, request_idle_callback_with_handle, set_interval,
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
},
|
||||
html,
|
||||
html::Binding,
|
||||
math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class,
|
||||
CollectView, Errors, EventHandlerFn, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
pub mod error {
|
||||
pub use server_fn::error::{Error, Result};
|
||||
}
|
||||
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
|
||||
pub use leptos_macro::template;
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "template_macro")))]
|
||||
pub use leptos_macro::view as template;
|
||||
pub use leptos_macro::{component, island, slice, slot, view, Params};
|
||||
cfg_if::cfg_if!(
|
||||
if #[cfg(feature="spin")] {
|
||||
pub use leptos_spin_macro::server;
|
||||
} else {
|
||||
pub use leptos_macro::server;
|
||||
}
|
||||
);
|
||||
pub use leptos_reactive::*;
|
||||
pub use leptos_server::{
|
||||
self, create_action, create_multi_action, create_server_action,
|
||||
create_server_multi_action, Action, MultiAction, ServerFnError,
|
||||
ServerFnErrorErr,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
mod animated_show;
|
||||
mod for_loop;
|
||||
mod provider;
|
||||
mod show;
|
||||
pub use animated_show::*;
|
||||
pub use for_loop::*;
|
||||
pub use provider::*;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
pub use serde;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
pub use serde_json;
|
||||
pub use show::*;
|
||||
//pub use suspense_component::*;
|
||||
mod suspense_component;
|
||||
//mod transition;
|
||||
#[cfg(feature = "tracing")]
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
pub use transition::*;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder::Optional;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder_macro;
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(
|
||||
feature = "csr",
|
||||
feature = "hydrate",
|
||||
feature = "template_macro"
|
||||
))]
|
||||
pub use wasm_bindgen; // used in islands
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(
|
||||
feature = "csr",
|
||||
feature = "hydrate",
|
||||
feature = "template_macro"
|
||||
))]
|
||||
pub use web_sys; // used in islands
|
||||
|
||||
mod children;
|
||||
mod portal;
|
||||
mod view_fn;
|
||||
pub use children::*;
|
||||
pub use portal::*;
|
||||
pub use view_fn::*;
|
||||
|
||||
extern crate self as leptos;
|
||||
|
||||
/// A type for taking anything that implements [`IntoAttribute`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyHeading(
|
||||
/// text: String,
|
||||
/// #[prop(optional, into)] class: Option<AttributeValue>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <h1 class=class>{text}</h1>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub type AttributeValue = Box<dyn IntoAttribute>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Component<P> {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Props {
|
||||
type Builder;
|
||||
fn builder() -> Self::Builder;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait DynAttrs {
|
||||
fn dyn_attrs(self, _args: Vec<(&'static str, Attribute)>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DynAttrs for () {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait DynBindings {
|
||||
fn dyn_bindings<B: Into<Binding>>(
|
||||
self,
|
||||
_args: impl IntoIterator<Item = B>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl DynBindings for () {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait PropsOrNoPropsBuilder {
|
||||
type Builder;
|
||||
fn builder_or_not() -> Self::Builder;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct EmptyPropsBuilder {}
|
||||
|
||||
impl EmptyPropsBuilder {
|
||||
pub fn build(self) {}
|
||||
}
|
||||
|
||||
impl<P: Props> PropsOrNoPropsBuilder for P {
|
||||
type Builder = <P as Props>::Builder;
|
||||
fn builder_or_not() -> Self::Builder {
|
||||
Self::builder()
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
|
||||
type Builder = EmptyPropsBuilder;
|
||||
fn builder_or_not() -> Self::Builder {
|
||||
EmptyPropsBuilder {}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, R> Component<EmptyPropsBuilder> for F where F: FnOnce() -> R {}
|
||||
|
||||
impl<P, F, R> Component<P> for F
|
||||
where
|
||||
F: FnOnce(P) -> R,
|
||||
P: Props,
|
||||
{
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
|
||||
_f: &impl Component<P>,
|
||||
) -> <P as PropsOrNoPropsBuilder>::Builder {
|
||||
<P as PropsOrNoPropsBuilder>::builder_or_not()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_view<P>(f: impl ComponentConstructor<P>, props: P) -> View {
|
||||
f.construct(props)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait ComponentConstructor<P> {
|
||||
fn construct(self, props: P) -> View;
|
||||
}
|
||||
|
||||
impl<Func, V> ComponentConstructor<()> for Func
|
||||
where
|
||||
Func: FnOnce() -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
fn construct(self, (): ()) -> View {
|
||||
(self)().into_view()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Func, V, P> ComponentConstructor<P> for Func
|
||||
where
|
||||
Func: FnOnce(P) -> V,
|
||||
V: IntoView,
|
||||
P: PropsOrNoPropsBuilder,
|
||||
{
|
||||
fn construct(self, props: P) -> View {
|
||||
(self)(props).into_view()
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,5 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::html::HtmlElement;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn simple_ssr_test() {
|
||||
use leptos::prelude::*;
|
||||
@@ -20,6 +22,7 @@ fn simple_ssr_test() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_test_with_components() {
|
||||
use leptos::prelude::*;
|
||||
@@ -51,6 +54,7 @@ fn ssr_test_with_components() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_test_with_snake_case_components() {
|
||||
use leptos::prelude::*;
|
||||
@@ -81,6 +85,7 @@ fn ssr_test_with_snake_case_components() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_classes() {
|
||||
use leptos::prelude::*;
|
||||
@@ -98,6 +103,7 @@ 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::*;
|
||||
@@ -119,6 +125,7 @@ fn ssr_with_styles() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_option() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
@@ -15,7 +15,7 @@ config = { version = "0.14.0", default-features = false, features = [
|
||||
"convert-case",
|
||||
] }
|
||||
regex = "1.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
thiserror = "1.0"
|
||||
typed-builder = "0.19.1"
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ pub mod errors;
|
||||
use crate::errors::LeptosConfigError;
|
||||
use config::{Case, Config, File, FileFormat};
|
||||
use regex::Regex;
|
||||
use std::{env::VarError, fs, net::SocketAddr, path::Path, str::FromStr};
|
||||
use std::{
|
||||
env::VarError, fs, net::SocketAddr, path::Path, str::FromStr, sync::Arc,
|
||||
};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
@@ -25,17 +27,17 @@ pub struct ConfFile {
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into), default=default_output_name())]
|
||||
pub output_name: String,
|
||||
pub output_name: Arc<str>,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
#[builder(setter(into), default=default_site_root())]
|
||||
#[serde(default = "default_site_root")]
|
||||
pub site_root: String,
|
||||
pub site_root: Arc<str>,
|
||||
/// The path of the WASM and JS files generated by wasm-bindgen from the root of your app
|
||||
/// By default, wasm-bindgen puts them in `pkg`.
|
||||
#[builder(setter(into), default=default_site_pkg_dir())]
|
||||
#[serde(default = "default_site_pkg_dir")]
|
||||
pub site_pkg_dir: String,
|
||||
pub site_pkg_dir: Arc<str>,
|
||||
/// Used to configure the running environment of Leptos. Can be used to load dev constants and keys v prod, or change
|
||||
/// things based on the deployment environment
|
||||
/// I recommend passing in the result of `env::var("LEPTOS_ENV")`
|
||||
@@ -66,11 +68,11 @@ pub struct LeptosOptions {
|
||||
/// The path of a custom 404 Not Found page to display when statically serving content, defaults to `site_root/404.html`
|
||||
#[builder(default = default_not_found_path())]
|
||||
#[serde(default = "default_not_found_path")]
|
||||
pub not_found_path: String,
|
||||
pub not_found_path: Arc<str>,
|
||||
/// The file name of the hash text file generated by cargo-leptos. Defaults to `hash.txt`.
|
||||
#[builder(default = default_hash_file_name())]
|
||||
#[serde(default = "default_hash_file_name")]
|
||||
pub hash_file: String,
|
||||
pub hash_file: Arc<str>,
|
||||
/// If true, hashes will be generated for all files in the site_root and added to their file names.
|
||||
/// Defaults to `true`.
|
||||
#[builder(default = default_hash_files())]
|
||||
@@ -96,9 +98,9 @@ impl LeptosOptions {
|
||||
);
|
||||
}
|
||||
Ok(LeptosOptions {
|
||||
output_name,
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
|
||||
output_name: output_name.into(),
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?.into(),
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?.into(),
|
||||
env: env_from_str(env_w_default("LEPTOS_ENV", "DEV")?.as_str())?,
|
||||
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
|
||||
.parse()?,
|
||||
@@ -113,8 +115,10 @@ impl LeptosOptions {
|
||||
reload_ws_protocol: ws_from_str(
|
||||
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
|
||||
)?,
|
||||
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?,
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?,
|
||||
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?
|
||||
.into(),
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
|
||||
.into(),
|
||||
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
|
||||
})
|
||||
}
|
||||
@@ -126,16 +130,16 @@ impl Default for LeptosOptions {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_name() -> String {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_")
|
||||
fn default_output_name() -> Arc<str> {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_").into()
|
||||
}
|
||||
|
||||
fn default_site_root() -> String {
|
||||
".".to_string()
|
||||
fn default_site_root() -> Arc<str> {
|
||||
".".into()
|
||||
}
|
||||
|
||||
fn default_site_pkg_dir() -> String {
|
||||
"pkg".to_string()
|
||||
fn default_site_pkg_dir() -> Arc<str> {
|
||||
"pkg".into()
|
||||
}
|
||||
|
||||
fn default_env() -> Env {
|
||||
@@ -150,12 +154,12 @@ fn default_reload_port() -> u32 {
|
||||
3001
|
||||
}
|
||||
|
||||
fn default_not_found_path() -> String {
|
||||
"/404".to_string()
|
||||
fn default_not_found_path() -> Arc<str> {
|
||||
"/404".into()
|
||||
}
|
||||
|
||||
fn default_hash_file_name() -> String {
|
||||
"hash.txt".to_string()
|
||||
fn default_hash_file_name() -> Arc<str> {
|
||||
"hash.txt".into()
|
||||
}
|
||||
|
||||
fn default_hash_files() -> bool {
|
||||
|
||||
@@ -76,9 +76,9 @@ fn try_from_env_test() {
|
||||
|| LeptosOptions::try_from_env().unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app_test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
|
||||
@@ -50,9 +50,9 @@ async fn get_configuration_from_file_ok() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -106,9 +106,9 @@ async fn get_config_from_file_ok() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -151,9 +151,9 @@ fn get_config_from_str_content() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -178,9 +178,9 @@ async fn get_config_from_env() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -202,8 +202,8 @@ async fn get_config_from_env() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(config.site_root, "target/site");
|
||||
assert_eq!(config.site_pkg_dir, "pkg");
|
||||
assert_eq!(config.site_root.as_ref(), "target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
@@ -215,10 +215,10 @@ async fn get_config_from_env() {
|
||||
#[test]
|
||||
fn leptos_options_builder_default() {
|
||||
let conf = LeptosOptions::builder().output_name("app-test").build();
|
||||
assert_eq!(conf.output_name, "app-test");
|
||||
assert_eq!(conf.output_name.as_ref(), "app-test");
|
||||
assert!(matches!(conf.env, Env::DEV));
|
||||
assert_eq!(conf.site_pkg_dir, "pkg");
|
||||
assert_eq!(conf.site_root, ".");
|
||||
assert_eq!(conf.site_pkg_dir.as_ref(), "pkg");
|
||||
assert_eq!(conf.site_root.as_ref(), ".");
|
||||
assert_eq!(
|
||||
conf.site_addr,
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
@@ -242,9 +242,9 @@ fn environment_variable_override() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
assert_eq!(config.site_root, "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
@@ -265,9 +265,9 @@ fn environment_variable_override() {
|
||||
|| get_config_from_str(CARGO_TOML_CONTENT_OK).unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(config.output_name, "app-test2");
|
||||
assert_eq!(config.site_root, "my_target/site2");
|
||||
assert_eq!(config.site_pkg_dir, "my_pkg2");
|
||||
assert_eq!(config.output_name.as_ref(), "app-test2");
|
||||
assert_eq!(config.site_root.as_ref(), "my_target/site2");
|
||||
assert_eq!(config.site_pkg_dir.as_ref(), "my_pkg2");
|
||||
assert_eq!(
|
||||
config.site_addr,
|
||||
SocketAddr::from_str("0.0.0.0:82").unwrap()
|
||||
|
||||
@@ -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-gamma"
|
||||
version = "0.7.0-rc0"
|
||||
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);
|
||||
@@ -265,14 +270,30 @@ impl ToTokens for Model {
|
||||
}
|
||||
};
|
||||
|
||||
let component = quote! {
|
||||
::leptos::prelude::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
|
||||
@@ -520,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 {
|
||||
|
||||
@@ -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,)*
|
||||
})
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::{fragment_to_tokens, TagType};
|
||||
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};
|
||||
@@ -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()
|
||||
@@ -264,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
|
||||
}
|
||||
|
||||
@@ -179,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),
|
||||
}
|
||||
|
||||
@@ -290,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 {
|
||||
@@ -303,21 +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 = html_escape::encode_text(&text);
|
||||
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 = html_escape::encode_text(&text);
|
||||
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('<');
|
||||
@@ -364,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use proc_macro2::Ident;
|
||||
use quote::format_ident;
|
||||
use rstml::node::KeyedAttribute;
|
||||
use syn::spanned::Spanned;
|
||||
use rstml::node::{KeyedAttribute, NodeName};
|
||||
use syn::{spanned::Spanned, ExprPath};
|
||||
|
||||
pub fn filter_prefixed_attrs<'a, A>(attrs: A, prefix: &str) -> Vec<Ident>
|
||||
where
|
||||
@@ -17,3 +17,37 @@ where
|
||||
})
|
||||
.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();
|
||||
|
||||
@@ -185,7 +185,7 @@ impl FromEncodedStr for [u8] {
|
||||
mod view_implementations {
|
||||
use crate::Resource;
|
||||
use reactive_graph::traits::Read;
|
||||
use std::{future::Future, pin::Pin};
|
||||
use std::future::Future;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
@@ -219,16 +219,11 @@ mod view_implementations {
|
||||
{
|
||||
type Output<SomeNewAttr: Attribute> = Box<
|
||||
dyn FnMut() -> Suspend<
|
||||
Pin<
|
||||
Box<
|
||||
dyn Future<
|
||||
Output = <T as AddAnyAttr>::Output<
|
||||
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
>,
|
||||
> + Send,
|
||||
<T as AddAnyAttr>::Output<
|
||||
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
|
||||
>,
|
||||
>
|
||||
+ Send
|
||||
>;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
|
||||
@@ -434,6 +434,14 @@ pub struct OnceResource<T, Ser = JsonSerdeCodec> {
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<T, Ser> Clone for OnceResource<T, Ser> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Copy for OnceResource<T, Ser> {}
|
||||
|
||||
impl<T, Ser> OnceResource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
|
||||
@@ -76,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)]
|
||||
@@ -129,22 +161,23 @@ where
|
||||
#[cfg(all(feature = "hydration", debug_assertions))]
|
||||
{
|
||||
use reactive_graph::{
|
||||
computed::suspense::SuspenseContext, owner::use_context,
|
||||
computed::suspense::SuspenseContext, effect::in_effect_scope,
|
||||
owner::use_context,
|
||||
};
|
||||
let suspense = use_context::<SuspenseContext>();
|
||||
if suspense.is_none() {
|
||||
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/>. 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.)",
|
||||
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.)",
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -641,22 +674,23 @@ where
|
||||
#[cfg(all(feature = "hydration", debug_assertions))]
|
||||
{
|
||||
use reactive_graph::{
|
||||
computed::suspense::SuspenseContext, owner::use_context,
|
||||
computed::suspense::SuspenseContext, effect::in_effect_scope,
|
||||
owner::use_context,
|
||||
};
|
||||
let suspense = use_context::<SuspenseContext>();
|
||||
if suspense.is_none() {
|
||||
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/>. 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.)",
|
||||
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.)",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.7.0-gamma"
|
||||
version = "0.7.0-rc0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn HashedStylesheet(
|
||||
path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.join(&options.hash_file);
|
||||
.join(options.hash_file.as_ref());
|
||||
if hash_path.exists() {
|
||||
let hashes = std::fs::read_to_string(&hash_path)
|
||||
.expect("failed to read hash file");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "next_tuple"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-rc0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-rc0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::{
|
||||
computed::{ArcMemo, Memo},
|
||||
diagnostics::is_suppressing_resource_load,
|
||||
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
|
||||
owner::{
|
||||
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
|
||||
SyncStorage,
|
||||
},
|
||||
signal::{ArcRwSignal, RwSignal},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, Update},
|
||||
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Set, Update},
|
||||
unwrap_signal,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
@@ -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,
|
||||
@@ -191,11 +196,21 @@ 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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the value of the action, setting its current value to `None`.
|
||||
///
|
||||
/// This has no other effect: i.e., it will not cancel in-flight actions, set the
|
||||
/// input, etc.
|
||||
#[track_caller]
|
||||
pub fn clear(&self) {
|
||||
self.value.set(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle that allows aborting an in-flight action. It is returned from [`Action::dispatch`] or
|
||||
@@ -230,14 +245,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 +264,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 +297,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 +305,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 +316,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 +366,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)))
|
||||
}),
|
||||
@@ -671,6 +687,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O, S> Action<I, O, S>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
S: Storage<ArcAction<I, O>>,
|
||||
{
|
||||
/// Clears the value of the action, setting its current value to `None`.
|
||||
///
|
||||
/// This has no other effect: i.e., it will not cancel in-flight actions, set the
|
||||
/// input, etc.
|
||||
#[track_caller]
|
||||
pub fn clear(&self) {
|
||||
self.inner.try_with_value(|inner| inner.value.set(None));
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> Action<I, O, LocalStorage>
|
||||
where
|
||||
I: 'static,
|
||||
|
||||
@@ -109,6 +109,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -13,7 +13,7 @@ use futures::StreamExt;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
mem,
|
||||
sync::{Arc, RwLock},
|
||||
sync::{atomic::AtomicBool, Arc, RwLock},
|
||||
};
|
||||
|
||||
/// Effects run a certain chunk of code whenever the signals they depend on change.
|
||||
@@ -109,6 +109,29 @@ fn effect_base() -> (Receiver, Owner, Arc<RwLock<EffectInner>>) {
|
||||
(rx, owner, inner)
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static EFFECT_SCOPE_ACTIVE: AtomicBool = const { AtomicBool::new(false) };
|
||||
}
|
||||
|
||||
/// Returns whether the current thread is currently running an effect.
|
||||
pub fn in_effect_scope() -> bool {
|
||||
EFFECT_SCOPE_ACTIVE
|
||||
.with(|scope| scope.load(std::sync::atomic::Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Set a static to true whilst running the given function.
|
||||
/// [`is_in_effect_scope`] will return true whilst the function is running.
|
||||
fn run_in_effect_scope<T>(fun: impl FnOnce() -> T) -> T {
|
||||
// For the theoretical nested case, set back to initial value rather than false:
|
||||
let initial = EFFECT_SCOPE_ACTIVE
|
||||
.with(|scope| scope.swap(true, std::sync::atomic::Ordering::Relaxed));
|
||||
let result = fun();
|
||||
EFFECT_SCOPE_ACTIVE.with(|scope| {
|
||||
scope.store(initial, std::sync::atomic::Ordering::Relaxed)
|
||||
});
|
||||
result
|
||||
}
|
||||
|
||||
impl<S> Effect<S>
|
||||
where
|
||||
S: Storage<StoredEffect>,
|
||||
@@ -157,7 +180,9 @@ impl Effect<LocalStorage> {
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun.run(old_value))
|
||||
subscriber.with_observer(|| {
|
||||
run_in_effect_scope(|| fun.run(old_value))
|
||||
})
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
@@ -375,7 +400,9 @@ impl Effect<SyncStorage> {
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun.run(old_value))
|
||||
subscriber.with_observer(|| {
|
||||
run_in_effect_scope(|| fun.run(old_value))
|
||||
})
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
@@ -419,7 +446,9 @@ impl Effect<SyncStorage> {
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun.run(old_value))
|
||||
subscriber.with_observer(|| {
|
||||
run_in_effect_scope(|| fun.run(old_value))
|
||||
})
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ impl<T: Serialize + 'static, St: Storage<T>> Serialize for ArcMemo<T, St> {
|
||||
|
||||
impl<T, St> Serialize for MaybeSignal<T, St>
|
||||
where
|
||||
T: Send + Sync + Serialize,
|
||||
T: Clone + Send + Sync + Serialize,
|
||||
St: Storage<SignalTypes<T, St>> + Storage<T>,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
||||
@@ -623,3 +623,46 @@ where
|
||||
Display::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper that implements [`Deref`] and [`Borrow`] for itself.
|
||||
pub struct Derefable<T>(pub T);
|
||||
|
||||
impl<T> Clone for Derefable<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Derefable(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for Derefable<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Borrow<T> for Derefable<T> {
|
||||
fn borrow(&self) -> &T {
|
||||
self.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<T> for Derefable<T>
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.deref() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Display for Derefable<T>
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,11 +168,20 @@ pub trait ReadUntracked: Sized + DefinedAt {
|
||||
self.try_read_untracked()
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
|
||||
/// This is a backdoor to allow overriding the [`Read::try_read`] implementation despite it being auto implemented.
|
||||
///
|
||||
/// If your type contains a [`Signal`](crate::wrappers::read::Signal),
|
||||
/// call it's [`ReadUntracked::custom_try_read`] here, else return `None`.
|
||||
#[track_caller]
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Give read-only access to a signal's value by reference through a guard type,
|
||||
/// and subscribes the active reactive observer (an effect or computed) to changes in its value.
|
||||
pub trait Read {
|
||||
pub trait Read: DefinedAt {
|
||||
/// The guard type that will be returned, which can be dereferenced to the value.
|
||||
type Value: Deref;
|
||||
|
||||
@@ -185,7 +194,9 @@ pub trait Read {
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that has been disposed.
|
||||
#[track_caller]
|
||||
fn read(&self) -> Self::Value;
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Read for T
|
||||
@@ -195,13 +206,18 @@ where
|
||||
type Value = T::Value;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
self.track();
|
||||
self.try_read_untracked()
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.track();
|
||||
self.read_untracked()
|
||||
// The [`Read`] trait is auto implemented for types that implement [`ReadUntracked`] + [`Track`]. The [`Read`] trait then auto implements the [`With`] and [`Get`] traits too.
|
||||
//
|
||||
// This is a problem for e.g. the [`Signal`](crate::wrappers::read::Signal) type,
|
||||
// this type must use a custom [`Read::try_read`] implementation to avoid an unnecessary clone.
|
||||
//
|
||||
// This is a backdoor to allow overriding the [`Read::try_read`] implementation despite it being auto implemented.
|
||||
if let Some(custom) = self.custom_try_read() {
|
||||
custom
|
||||
} else {
|
||||
self.track();
|
||||
self.try_read_untracked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,14 +323,13 @@ pub trait With: DefinedAt {
|
||||
|
||||
impl<T> With for T
|
||||
where
|
||||
T: WithUntracked + Track,
|
||||
T: Read,
|
||||
{
|
||||
type Value = <T as WithUntracked>::Value;
|
||||
type Value = <<T as Read>::Value as Deref>::Target;
|
||||
|
||||
#[track_caller]
|
||||
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
|
||||
self.track();
|
||||
self.try_with_untracked(fun)
|
||||
self.try_read().map(|val| fun(&val))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ pub mod read {
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
|
||||
With, WithValue,
|
||||
},
|
||||
unwrap_signal,
|
||||
};
|
||||
@@ -201,29 +200,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for ArcSignal<T, SyncStorage>
|
||||
where
|
||||
T: Default + Send + Sync + 'static,
|
||||
@@ -285,22 +261,23 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for ArcSignal<T, S>
|
||||
impl<T, S> Track for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
T: Clone,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
fn track(&self) {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => i.try_with(fun),
|
||||
SignalTypes::Memo(i) => i.try_with(fun),
|
||||
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
|
||||
SignalTypes::Stored(i) => i.try_with_value(fun),
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,32 +305,27 @@ pub mod read {
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for ArcSignal<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
/// Overriding the default auto implemented [`Read::try_read`] to combine read and track,
|
||||
/// to avoid 2 clones and just have 1 in the [`SignalTypes::DerivedSignal`].
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
Some(
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
SignalTypes::Memo(i) => i.try_read().map(SignalReadGuard::Memo),
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
.map(ReadGuard::new),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,27 +404,31 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for Signal<T, S>
|
||||
impl<T, S> Track for Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
S: Storage<T> + Storage<SignalTypes<T, S>>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
self.inner
|
||||
fn track(&self) {
|
||||
let inner = self
|
||||
.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| match &inner {
|
||||
SignalTypes::ReadSignal(i) => i.try_with(fun),
|
||||
SignalTypes::Memo(i) => i.try_with(fun),
|
||||
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
|
||||
SignalTypes::Stored(i) => i.try_with_value(fun),
|
||||
})
|
||||
.unwrap_or_else(unwrap_signal!(self));
|
||||
match inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,41 +462,33 @@ pub mod read {
|
||||
.map(ReadGuard::new)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
self.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| {
|
||||
match &inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
/// Overriding the default auto implemented [`Read::try_read`] to combine read and track,
|
||||
/// to avoid 2 clones and just have 1 in the [`SignalTypes::DerivedSignal`].
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
Some(
|
||||
self.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.and_then(|inner| {
|
||||
match &inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.try_read().map(SignalReadGuard::Read)
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.try_read().map(SignalReadGuard::Memo)
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
Some(SignalReadGuard::Owned(i()))
|
||||
}
|
||||
SignalTypes::Stored(i) => {
|
||||
i.try_read_value().map(SignalReadGuard::Read)
|
||||
}
|
||||
}
|
||||
.map(ReadGuard::new)
|
||||
})
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
.map(ReadGuard::new)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,36 +588,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Signal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
let inner = self
|
||||
.inner
|
||||
// clone the inner Arc type and release the lock
|
||||
// prevents deadlocking if the derived value includes taking a lock on the arena
|
||||
.try_with_value(Clone::clone)
|
||||
.unwrap_or_else(unwrap_signal!(self));
|
||||
match inner {
|
||||
SignalTypes::ReadSignal(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::Memo(i) => {
|
||||
i.track();
|
||||
}
|
||||
SignalTypes::DerivedSignal(i) => {
|
||||
i();
|
||||
}
|
||||
// Doesn't change.
|
||||
SignalTypes::Stored(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Signal<T>
|
||||
where
|
||||
T: Send + Sync + Default + 'static,
|
||||
@@ -830,6 +768,20 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Signal<String> {
|
||||
#[track_caller]
|
||||
fn from(value: &str) -> Self {
|
||||
Signal::stored(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Signal<String, LocalStorage> {
|
||||
#[track_caller]
|
||||
fn from(value: &str) -> Self {
|
||||
Signal::stored_local(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for a value that is *either* `T` or [`Signal<T>`].
|
||||
///
|
||||
/// This allows you to create APIs that take either a reactive or a non-reactive value
|
||||
@@ -903,20 +855,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for MaybeSignal<T, S>
|
||||
impl<T, S> Track for MaybeSignal<T, S>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
S: Storage<T> + Storage<SignalTypes<T, S>>,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
fn track(&self) {
|
||||
match self {
|
||||
Self::Static(t) => Some(fun(t)),
|
||||
Self::Dynamic(s) => s.try_with(fun),
|
||||
Self::Static(_) => {}
|
||||
Self::Dynamic(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -936,27 +882,13 @@ pub mod read {
|
||||
Self::Dynamic(s) => s.try_read_untracked(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for MaybeSignal<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
match self {
|
||||
Self::Static(t) => {
|
||||
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
|
||||
}
|
||||
Self::Dynamic(s) => s.try_read(),
|
||||
Self::Static(_) => None,
|
||||
Self::Dynamic(s) => s.custom_try_read(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeSignal<T>
|
||||
@@ -980,21 +912,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> MaybeSignal<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<T, S>> + Storage<T>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match self {
|
||||
Self::Static(_) => {}
|
||||
Self::Dynamic(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeSignal<T, SyncStorage>
|
||||
where
|
||||
SyncStorage: Storage<T>,
|
||||
@@ -1192,20 +1109,14 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> With for MaybeProp<T, S>
|
||||
impl<T, S> Track for MaybeProp<T, S>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_with<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&Self::Value) -> U,
|
||||
) -> Option<U> {
|
||||
fn track(&self) {
|
||||
match &self.0 {
|
||||
None => Some(fun(&None)),
|
||||
Some(inner) => inner.try_with(fun),
|
||||
None => {}
|
||||
Some(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1223,25 +1134,13 @@ pub mod read {
|
||||
Some(inner) => inner.try_read_untracked(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Read for MaybeProp<T, S>
|
||||
where
|
||||
T: Clone,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
{
|
||||
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
|
||||
|
||||
fn try_read(&self) -> Option<Self::Value> {
|
||||
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
|
||||
match &self.0 {
|
||||
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
|
||||
Some(inner) => inner.try_read(),
|
||||
None => None,
|
||||
Some(inner) => inner.custom_try_read(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self) -> Self::Value {
|
||||
self.try_read().unwrap_or_else(unwrap_signal!(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeProp<T>
|
||||
@@ -1257,21 +1156,6 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> MaybeProp<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
|
||||
{
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
#[track_caller]
|
||||
pub fn track(&self) {
|
||||
match &self.0 {
|
||||
None => {}
|
||||
Some(signal) => signal.track(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MaybeProp<T>
|
||||
where
|
||||
SyncStorage: Storage<Option<T>>,
|
||||
|
||||
88
reactive_graph/tests/cleanup.rs
Normal file
88
reactive_graph/tests/cleanup.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use reactive_graph::{
|
||||
computed::Memo,
|
||||
owner::{on_cleanup, Owner},
|
||||
signal::{RwSignal, Trigger},
|
||||
traits::{Dispose, GetUntracked, Track},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn cleanup_on_dispose() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
struct ExecuteOnDrop(Option<Box<dyn FnOnce() + Send + Sync>>);
|
||||
|
||||
impl ExecuteOnDrop {
|
||||
fn new(f: impl FnOnce() + Send + Sync + 'static) -> Self {
|
||||
Self(Some(Box::new(f)))
|
||||
}
|
||||
}
|
||||
impl Drop for ExecuteOnDrop {
|
||||
fn drop(&mut self) {
|
||||
self.0.take().unwrap()();
|
||||
}
|
||||
}
|
||||
|
||||
let trigger = Trigger::new();
|
||||
|
||||
println!("STARTING");
|
||||
|
||||
let memo = Memo::new(move |_| {
|
||||
trigger.track();
|
||||
|
||||
// An example of why you might want to do this is that
|
||||
// when something goes out of reactive scope you want it to be cleaned up.
|
||||
// The cleaning up might have side effects, and those side effects might cause
|
||||
// re-renders where new `on_cleanup` are registered.
|
||||
let on_drop = ExecuteOnDrop::new(|| {
|
||||
on_cleanup(|| println!("Nested cleanup in progress."))
|
||||
});
|
||||
|
||||
on_cleanup(move || {
|
||||
println!("Cleanup in progress.");
|
||||
drop(on_drop)
|
||||
});
|
||||
});
|
||||
println!("Memo 1: {:?}", memo);
|
||||
memo.get_untracked(); // First cleanup registered.
|
||||
|
||||
memo.dispose(); // Cleanup not run here.
|
||||
|
||||
println!("Cleanup should have been executed.");
|
||||
|
||||
let memo = Memo::new(move |_| {
|
||||
// New cleanup registered. It'll panic here.
|
||||
on_cleanup(move || println!("Test passed."));
|
||||
});
|
||||
println!("Memo 2: {:?}", memo);
|
||||
println!("^ Note how the memos have the same key (different versions).");
|
||||
memo.get_untracked(); // First cleanup registered.
|
||||
|
||||
println!("Test passed.");
|
||||
|
||||
memo.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leak_on_dispose() {
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let trigger = Trigger::new();
|
||||
|
||||
let value = Arc::new(());
|
||||
let weak = Arc::downgrade(&value);
|
||||
|
||||
let memo = Memo::new(move |_| {
|
||||
trigger.track();
|
||||
|
||||
RwSignal::new(value.clone());
|
||||
});
|
||||
|
||||
memo.get_untracked();
|
||||
|
||||
memo.dispose();
|
||||
|
||||
assert!(weak.upgrade().is_none()); // Should have been dropped.
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-rc0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Stores for holding deeply-nested reactive state while maintaining fine-grained reactive tracking."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -11,10 +16,10 @@ or_poisoned = { workspace = true }
|
||||
paste = "1.0"
|
||||
reactive_graph = { workspace = true }
|
||||
rustc-hash = "2.0"
|
||||
reactive_stores_macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.39", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
reactive_stores_macro = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["effects"] }
|
||||
|
||||
15
reactive_stores/README.md
Normal file
15
reactive_stores/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Stores
|
||||
|
||||
Stores are a data structure for nested reactivity.
|
||||
|
||||
The [`reactive_graph`](https://crates.io/crates/reactive_graph) crate provides primitives for fine-grained reactivity
|
||||
via signals, memos, and effects.
|
||||
|
||||
This crate extends that reactivity to support reactive access to nested dested, without the need to create nested signals.
|
||||
|
||||
Using the `#[derive(Store)]` macro on a struct creates a series of getters that allow accessing each field. Individual fields
|
||||
can then be read as if they were signals. Changes to parents will notify their children, but changing one sibling field will
|
||||
not notify any of the others, nor will it require diffing those sibling fields (unlike earlier solutions using memoized “slices”).
|
||||
|
||||
This is published for use with the Leptos framework but can be used in any scenario where `reactive_graph` is being used
|
||||
for reactivity.
|
||||
@@ -10,6 +10,7 @@ use reactive_graph::{
|
||||
Write,
|
||||
},
|
||||
};
|
||||
pub use reactive_stores_macro::*;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -171,12 +172,26 @@ impl KeyMap {
|
||||
where
|
||||
K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static,
|
||||
{
|
||||
// this incredibly defensive mechanism takes the guard twice
|
||||
// on initialization. unfortunately, this is because `initialize`, on
|
||||
// a nested keyed field can, when being initialized), can in fact try
|
||||
// to take the lock again, as we try to insert the keys of the parent
|
||||
// while inserting the keys on this child.
|
||||
//
|
||||
// see here https://github.com/leptos-rs/leptos/issues/3086
|
||||
let mut guard = self.0.write().or_poisoned();
|
||||
let entry = guard
|
||||
.entry(path)
|
||||
.or_insert_with(|| Box::new(FieldKeys::new(initialize())));
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
if guard.contains_key(&path) {
|
||||
let entry = guard.get_mut(&path)?;
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
} else {
|
||||
drop(guard);
|
||||
let keys = Box::new(FieldKeys::new(initialize()));
|
||||
let mut guard = self.0.write().or_poisoned();
|
||||
let entry = guard.entry(path).or_insert(keys);
|
||||
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
|
||||
Some(fun(entry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +445,6 @@ mod tests {
|
||||
effect::Effect,
|
||||
traits::{Read, ReadUntracked, Set, Update, Write},
|
||||
};
|
||||
use reactive_stores_macro::{Patch, Store};
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
|
||||
@@ -51,7 +51,6 @@ mod tests {
|
||||
effect::Effect,
|
||||
traits::{Get, Read, ReadUntracked, Set, Write},
|
||||
};
|
||||
use reactive_stores_macro::Store;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
[package]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-rc0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Stores for holding deeply-nested reactive state while maintaining fine-grained reactive tracking."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
|
||||
1
reactive_stores_macro/README.md
Normal file
1
reactive_stores_macro/README.md
Normal file
@@ -0,0 +1 @@
|
||||
This crate provides macro that are helpful or required when using the `reactive_stores` crate.
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.7.0-gamma"
|
||||
version = "0.7.0-rc0"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -22,13 +22,11 @@ url = "2.5"
|
||||
js-sys = { version = "0.3.69" }
|
||||
wasm-bindgen = { version = "0.2.93" }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
paste = "1.0"
|
||||
once_cell = "1.19"
|
||||
send_wrapper = "0.6.0"
|
||||
thiserror = "1.0"
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
gloo-net = "0.6.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.70"
|
||||
|
||||
@@ -24,6 +24,7 @@ use reactive_graph::{
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{Debug, Display},
|
||||
mem,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -47,7 +48,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Router<Chil>(
|
||||
/// The base URL for the router. Defaults to `""`.
|
||||
#[prop(optional, into)]
|
||||
@@ -68,16 +69,16 @@ where
|
||||
Chil: IntoView,
|
||||
{
|
||||
#[cfg(feature = "ssr")]
|
||||
let (current_url, redirect_hook) = {
|
||||
let (location_provider, current_url, redirect_hook) = {
|
||||
let req = use_context::<RequestUrl>().expect("no RequestUrl provided");
|
||||
let parsed = req.parse().expect("could not parse RequestUrl");
|
||||
let current_url = ArcRwSignal::new(parsed);
|
||||
|
||||
(current_url, Box::new(move |_: &str| {}))
|
||||
(None, current_url, Box::new(move |_: &str| {}))
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let (current_url, redirect_hook) = {
|
||||
let (location_provider, current_url, redirect_hook) = {
|
||||
let location =
|
||||
BrowserUrl::new().expect("could not access browser navigation"); // TODO options here
|
||||
location.init(base.clone());
|
||||
@@ -86,7 +87,7 @@ where
|
||||
|
||||
let redirect_hook = Box::new(|loc: &str| BrowserUrl::redirect(loc));
|
||||
|
||||
(current_url, redirect_hook)
|
||||
(Some(location), current_url, redirect_hook)
|
||||
};
|
||||
// provide router context
|
||||
let state = ArcRwSignal::new(State::new(None));
|
||||
@@ -101,6 +102,8 @@ where
|
||||
location,
|
||||
state,
|
||||
set_is_routing,
|
||||
query_mutations: Default::default(),
|
||||
location_provider,
|
||||
});
|
||||
|
||||
let children = children.into_inner();
|
||||
@@ -114,6 +117,9 @@ pub(crate) struct RouterContext {
|
||||
pub location: Location,
|
||||
pub state: ArcRwSignal<State>,
|
||||
pub set_is_routing: Option<SignalSetter<bool>>,
|
||||
pub query_mutations:
|
||||
ArcStoredValue<Vec<(Oco<'static, str>, Option<String>)>>,
|
||||
pub location_provider: Option<BrowserUrl>,
|
||||
}
|
||||
|
||||
impl RouterContext {
|
||||
@@ -130,7 +136,7 @@ impl RouterContext {
|
||||
resolve_path("", path, None)
|
||||
};
|
||||
|
||||
let url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
|
||||
let mut url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
|
||||
Some(Ok(url)) => url,
|
||||
Some(Err(e)) => {
|
||||
leptos::logging::error!("Error parsing URL: {e:?}");
|
||||
@@ -141,6 +147,22 @@ impl RouterContext {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let query_mutations =
|
||||
mem::take(&mut *self.query_mutations.write_value());
|
||||
if !query_mutations.is_empty() {
|
||||
for (key, value) in query_mutations {
|
||||
if let Some(value) = value {
|
||||
url.search_params_mut().replace(key, value);
|
||||
} else {
|
||||
url.search_params_mut().remove(&key);
|
||||
}
|
||||
}
|
||||
*url.search_mut() = url
|
||||
.search_params()
|
||||
.to_query_string()
|
||||
.trim_start_matches('?')
|
||||
.into()
|
||||
}
|
||||
|
||||
if url.origin() != current.origin() {
|
||||
window().location().set_href(path).unwrap();
|
||||
@@ -153,17 +175,20 @@ impl RouterContext {
|
||||
}
|
||||
|
||||
// update URL signal, if necessary
|
||||
let value = url.to_full_path();
|
||||
if current != url {
|
||||
drop(current);
|
||||
self.current_url.set(url);
|
||||
}
|
||||
|
||||
BrowserUrl::complete_navigation(&LocationChange {
|
||||
value: path.to_string(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: options.state,
|
||||
});
|
||||
if let Some(location_provider) = &self.location_provider {
|
||||
location_provider.complete_navigation(&LocationChange {
|
||||
value,
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: options.state,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_path<'a>(
|
||||
@@ -204,9 +229,12 @@ where
|
||||
}
|
||||
}*/
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Routes<Defs, FallbackFn, Fallback>(
|
||||
fallback: FallbackFn,
|
||||
/// Whether to use the View Transition API during navigation.
|
||||
#[prop(optional)]
|
||||
transition: bool,
|
||||
children: RouteChildren<Defs>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
@@ -227,7 +255,10 @@ where
|
||||
base.upgrade_inplace();
|
||||
base
|
||||
});
|
||||
let routes = Routes::new(children.into_inner());
|
||||
let routes = Routes::new_with_base(
|
||||
children.into_inner(),
|
||||
base.clone().unwrap_or_default(),
|
||||
);
|
||||
let outer_owner =
|
||||
Owner::current().expect("creating Routes, but no Owner was found");
|
||||
move || {
|
||||
@@ -243,13 +274,17 @@ where
|
||||
base: base.clone(),
|
||||
fallback: fallback.clone(),
|
||||
set_is_routing,
|
||||
transition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn FlatRoutes<Defs, FallbackFn, Fallback>(
|
||||
fallback: FallbackFn,
|
||||
/// Whether to use the View Transition API during navigation.
|
||||
#[prop(optional)]
|
||||
transition: bool,
|
||||
children: RouteChildren<Defs>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
@@ -273,7 +308,10 @@ where
|
||||
base.upgrade_inplace();
|
||||
base
|
||||
});
|
||||
let routes = Routes::new(children.into_inner());
|
||||
let routes = Routes::new_with_base(
|
||||
children.into_inner(),
|
||||
base.clone().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let outer_owner =
|
||||
Owner::current().expect("creating Router, but no Owner was found");
|
||||
@@ -290,11 +328,12 @@ where
|
||||
fallback: fallback.clone(),
|
||||
outer_owner: outer_owner.clone(),
|
||||
set_is_routing,
|
||||
transition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Route<Segments, View>(
|
||||
path: Segments,
|
||||
view: View,
|
||||
@@ -306,7 +345,7 @@ where
|
||||
NestedRoute::new(path, view).ssr_mode(ssr)
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn ParentRoute<Segments, View, Children>(
|
||||
path: Segments,
|
||||
view: View,
|
||||
@@ -320,7 +359,7 @@ where
|
||||
NestedRoute::new(path, view).ssr_mode(ssr).child(children)
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
|
||||
path: Segments,
|
||||
view: ViewFn,
|
||||
@@ -362,7 +401,7 @@ where
|
||||
NestedRoute::new(path, view).ssr_mode(ssr)
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedParentRoute<Segments, ViewFn, View, C, PathFn, P, Children>(
|
||||
path: Segments,
|
||||
view: ViewFn,
|
||||
@@ -424,7 +463,7 @@ where
|
||||
///
|
||||
/// [`leptos_actix`]: <https://docs.rs/leptos_actix/>
|
||||
/// [`leptos_axum`]: <https://docs.rs/leptos_axum/>
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Redirect<P>(
|
||||
/// The relative path to which the user should be redirected.
|
||||
path: P,
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use crate::{
|
||||
hooks::Matched,
|
||||
location::{LocationProvider, Url},
|
||||
matching::Routes,
|
||||
matching::{MatchParams, Routes},
|
||||
params::ParamsMap,
|
||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
|
||||
RouteList, RouteListing, RouteMatchId,
|
||||
view_transition::start_view_transition,
|
||||
ChooseView, MatchInterface, MatchNestedRoutes, PathSegment, RouteList,
|
||||
RouteListing, RouteMatchId,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use either_of::{Either, EitherOf3};
|
||||
use either_of::Either;
|
||||
use futures::FutureExt;
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, ScopedFuture},
|
||||
owner::{provide_context, Owner},
|
||||
signal::ArcRwSignal,
|
||||
traits::{ReadUntracked, Set},
|
||||
traits::{GetUntracked, ReadUntracked, Set},
|
||||
transition::AsyncTransition,
|
||||
wrappers::write::SignalSetter,
|
||||
};
|
||||
@@ -23,8 +24,9 @@ use tachys::{
|
||||
reactive_graph::OwnedView,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
|
||||
RenderHtml,
|
||||
add_attr::AddAnyAttr,
|
||||
any_view::{AnyView, AnyViewState, IntoAny},
|
||||
Mountable, Position, PositionState, Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -35,28 +37,21 @@ pub(crate) struct FlatRoutesView<Loc, Defs, FalFn> {
|
||||
pub fallback: FalFn,
|
||||
pub outer_owner: Owner,
|
||||
pub set_is_routing: Option<SignalSetter<bool>>,
|
||||
pub transition: bool,
|
||||
}
|
||||
|
||||
pub struct FlatRoutesViewState<Defs, Fal>
|
||||
where
|
||||
Defs: MatchNestedRoutes + 'static,
|
||||
Fal: Render + 'static,
|
||||
{
|
||||
pub struct FlatRoutesViewState {
|
||||
#[allow(clippy::type_complexity)]
|
||||
view: <EitherOf3<(), Fal, OwnedView<<Defs::Match as MatchInterface>::View>> as Render>::State,
|
||||
view: AnyViewState,
|
||||
id: Option<RouteMatchId>,
|
||||
owner: Owner,
|
||||
params: ArcRwSignal<ParamsMap>,
|
||||
path: String,
|
||||
url: ArcRwSignal<Url>,
|
||||
matched: ArcRwSignal<String>
|
||||
matched: ArcRwSignal<String>,
|
||||
}
|
||||
|
||||
impl<Defs, Fal> Mountable for FlatRoutesViewState<Defs, Fal>
|
||||
where
|
||||
Defs: MatchNestedRoutes + 'static,
|
||||
Fal: Render + 'static,
|
||||
{
|
||||
impl Mountable for FlatRoutesViewState {
|
||||
fn unmount(&mut self) {
|
||||
self.view.unmount();
|
||||
}
|
||||
@@ -79,9 +74,9 @@ where
|
||||
Loc: LocationProvider,
|
||||
Defs: MatchNestedRoutes + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send,
|
||||
Fal: Render + 'static,
|
||||
Fal: IntoAny,
|
||||
{
|
||||
type State = Rc<RefCell<FlatRoutesViewState<Defs, Fal>>>;
|
||||
type State = Rc<RefCell<FlatRoutesViewState>>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let FlatRoutesView {
|
||||
@@ -121,7 +116,7 @@ where
|
||||
|
||||
match new_match {
|
||||
None => Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::B(fallback()).build(),
|
||||
view: fallback().into_any().build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
@@ -154,7 +149,7 @@ where
|
||||
|
||||
match view.as_mut().now_or_never() {
|
||||
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::C(view).build(),
|
||||
view: view.into_any().build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
@@ -165,7 +160,7 @@ where
|
||||
None => {
|
||||
let state =
|
||||
Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::A(()).build(),
|
||||
view: ().into_any().build(),
|
||||
id,
|
||||
owner,
|
||||
params,
|
||||
@@ -178,7 +173,7 @@ where
|
||||
let state = Rc::clone(&state);
|
||||
async move {
|
||||
let view = view.await;
|
||||
EitherOf3::C(view)
|
||||
view.into_any()
|
||||
.rebuild(&mut state.borrow_mut().view);
|
||||
}
|
||||
});
|
||||
@@ -198,6 +193,7 @@ where
|
||||
fallback,
|
||||
outer_owner,
|
||||
set_is_routing,
|
||||
transition,
|
||||
} = self;
|
||||
let url_snapshot = current_url.read_untracked();
|
||||
|
||||
@@ -267,8 +263,7 @@ where
|
||||
provide_context(url);
|
||||
provide_context(params_memo);
|
||||
provide_context(Matched(ArcMemo::from(new_matched)));
|
||||
EitherOf3::B(fallback())
|
||||
.rebuild(&mut state.borrow_mut().view)
|
||||
fallback().into_any().rebuild(&mut state.borrow_mut().view)
|
||||
});
|
||||
}
|
||||
Some(new_match) => {
|
||||
@@ -283,6 +278,10 @@ where
|
||||
|
||||
let spawned_path = url_snapshot.path().to_string();
|
||||
|
||||
let is_back = location
|
||||
.as_ref()
|
||||
.map(|nav| nav.is_back().get_untracked())
|
||||
.unwrap_or(false);
|
||||
Executor::spawn_local(owner.with(|| {
|
||||
ScopedFuture::new({
|
||||
let state = Rc::clone(state);
|
||||
@@ -310,8 +309,15 @@ where
|
||||
if current_url.read_untracked().path()
|
||||
== spawned_path
|
||||
{
|
||||
EitherOf3::C(view)
|
||||
.rebuild(&mut state.borrow_mut().view);
|
||||
let rebuild = move || {
|
||||
view.into_any()
|
||||
.rebuild(&mut state.borrow_mut().view);
|
||||
};
|
||||
if transition {
|
||||
start_view_transition(0, is_back, rebuild);
|
||||
} else {
|
||||
rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(location) = location {
|
||||
@@ -357,9 +363,7 @@ where
|
||||
FalFn: FnOnce() -> Fal + Send,
|
||||
Fal: RenderHtml + 'static,
|
||||
{
|
||||
fn choose_ssr(
|
||||
self,
|
||||
) -> OwnedView<Either<Fal, <Defs::Match as MatchInterface>::View>> {
|
||||
fn choose_ssr(self) -> OwnedView<AnyView> {
|
||||
let current_url = self.current_url.read_untracked();
|
||||
let new_match = self.routes.match_route(current_url.path());
|
||||
let owner = self.outer_owner.child();
|
||||
@@ -382,7 +386,7 @@ where
|
||||
drop(current_url);
|
||||
|
||||
let view = match new_match {
|
||||
None => Either::Left((self.fallback)()),
|
||||
None => (self.fallback)().into_any(),
|
||||
Some(new_match) => {
|
||||
let (view, _) = new_match.into_view_and_child();
|
||||
let view = owner
|
||||
@@ -396,7 +400,7 @@ where
|
||||
})
|
||||
.now_or_never()
|
||||
.expect("async route used in SSR");
|
||||
Either::Right(view)
|
||||
view.into_any()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -413,10 +417,7 @@ where
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
|
||||
const MIN_LENGTH: usize = <Either<
|
||||
Fal,
|
||||
<Defs::Match as MatchInterface>::View,
|
||||
> as RenderHtml>::MIN_LENGTH;
|
||||
const MIN_LENGTH: usize = <Either<Fal, AnyView> as RenderHtml>::MIN_LENGTH;
|
||||
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
@@ -546,7 +547,8 @@ where
|
||||
|
||||
match new_match {
|
||||
None => Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::B(fallback())
|
||||
view: fallback()
|
||||
.into_any()
|
||||
.hydrate::<FROM_SERVER>(cursor, position),
|
||||
id,
|
||||
owner,
|
||||
@@ -580,7 +582,8 @@ where
|
||||
|
||||
match view.as_mut().now_or_never() {
|
||||
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
|
||||
view: EitherOf3::C(view)
|
||||
view: view
|
||||
.into_any()
|
||||
.hydrate::<FROM_SERVER>(cursor, position),
|
||||
id,
|
||||
owner,
|
||||
|
||||
@@ -4,15 +4,18 @@ use crate::{
|
||||
navigate::NavigateOptions,
|
||||
params::{Params, ParamsError, ParamsMap},
|
||||
};
|
||||
use leptos::oco::Oco;
|
||||
use leptos::{leptos_dom::helpers::request_animation_frame, oco::Oco};
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, Memo},
|
||||
owner::use_context,
|
||||
owner::{expect_context, use_context},
|
||||
signal::{ArcRwSignal, ReadSignal},
|
||||
traits::{Get, GetUntracked, With},
|
||||
traits::{Get, GetUntracked, ReadUntracked, With, WriteValue},
|
||||
wrappers::write::SignalSetter,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
#[track_caller]
|
||||
#[deprecated = "This has been renamed to `query_signal` to match Rust naming \
|
||||
@@ -93,10 +96,15 @@ pub fn query_signal_with_options<T>(
|
||||
where
|
||||
T: FromStr + ToString + PartialEq + Send + Sync,
|
||||
{
|
||||
static IS_NAVIGATING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
let mut key: Oco<'static, str> = key.into();
|
||||
let query_map = use_query_map();
|
||||
let navigate = use_navigate();
|
||||
let location = use_location();
|
||||
let RouterContext {
|
||||
query_mutations, ..
|
||||
} = expect_context();
|
||||
|
||||
let get = Memo::new({
|
||||
let key = key.clone_inplace();
|
||||
@@ -108,20 +116,25 @@ where
|
||||
});
|
||||
|
||||
let set = SignalSetter::map(move |value: Option<T>| {
|
||||
let mut new_query_map = query_map.get();
|
||||
match value {
|
||||
Some(value) => {
|
||||
new_query_map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
None => {
|
||||
new_query_map.remove(&key);
|
||||
}
|
||||
}
|
||||
let qs = new_query_map.to_query_string();
|
||||
let path = location.pathname.get_untracked();
|
||||
let hash = location.hash.get_untracked();
|
||||
let qs = location.query.read_untracked().to_query_string();
|
||||
let new_url = format!("{path}{qs}{hash}");
|
||||
navigate(&new_url, nav_options.clone());
|
||||
query_mutations
|
||||
.write_value()
|
||||
.push((key.clone(), value.as_ref().map(ToString::to_string)));
|
||||
|
||||
if !IS_NAVIGATING.load(Ordering::Relaxed) {
|
||||
IS_NAVIGATING.store(true, Ordering::Relaxed);
|
||||
request_animation_frame({
|
||||
let navigate = navigate.clone();
|
||||
let nav_options = nav_options.clone();
|
||||
move || {
|
||||
navigate(&new_url, nav_options.clone());
|
||||
IS_NAVIGATING.store(false, Ordering::Relaxed)
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
(get, set)
|
||||
@@ -259,37 +272,12 @@ pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone {
|
||||
move |path: &str, options: NavigateOptions| cx.navigate(path, options)
|
||||
}
|
||||
|
||||
/*
|
||||
/// Returns a signal that tells you whether you are currently navigating backwards.
|
||||
pub(crate) fn use_is_back_navigation() -> ReadSignal<bool> {
|
||||
let router = use_router();
|
||||
router.inner.is_back.read_only()
|
||||
/// Returns a reactive string that contains the route that was matched for
|
||||
/// this [`Route`](crate::components::Route).
|
||||
#[track_caller]
|
||||
pub fn use_matched() -> Memo<String> {
|
||||
use_context::<Matched>()
|
||||
.expect("use_matched called outside a matched Route")
|
||||
.0
|
||||
.into()
|
||||
}
|
||||
*/
|
||||
|
||||
/* TODO check how this is used in 0.6 and use it
|
||||
/// Resolves a redirect location to an (absolute) URL.
|
||||
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
|
||||
let origin = match window().location().origin() {
|
||||
Ok(origin) => origin,
|
||||
Err(e) => {
|
||||
leptos::logging::error!("Failed to get origin: {:#?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Use server function's URL as base instead.
|
||||
let base = origin;
|
||||
|
||||
match web_sys::Url::new_with_base(loc, &base) {
|
||||
Ok(url) => Some(url),
|
||||
Err(e) => {
|
||||
leptos::logging::error!(
|
||||
"Invalid redirect location: {}",
|
||||
e.as_string().unwrap_or_default(),
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -24,3 +24,71 @@ pub use matching::*;
|
||||
pub use method::*;
|
||||
pub use navigate::*;
|
||||
pub use ssr_mode::*;
|
||||
|
||||
pub(crate) mod view_transition {
|
||||
use js_sys::{Function, Promise, Reflect};
|
||||
use leptos::leptos_dom::helpers::document;
|
||||
use wasm_bindgen::{closure::Closure, intern, JsCast, JsValue};
|
||||
|
||||
pub fn start_view_transition(
|
||||
level: u8,
|
||||
is_back_navigation: bool,
|
||||
fun: impl FnOnce() + 'static,
|
||||
) {
|
||||
let document = document();
|
||||
let document_element = document.document_element().unwrap();
|
||||
let class_list = document_element.class_list();
|
||||
let svt = Reflect::get(
|
||||
&document,
|
||||
&JsValue::from_str(intern("startViewTransition")),
|
||||
)
|
||||
.and_then(|svt| svt.dyn_into::<Function>());
|
||||
_ = class_list.add_1(&format!("router-outlet-{level}"));
|
||||
if is_back_navigation {
|
||||
_ = class_list.add_1("router-back");
|
||||
}
|
||||
match svt {
|
||||
Ok(svt) => {
|
||||
let cb = Closure::once_into_js(Box::new(move || {
|
||||
fun();
|
||||
}));
|
||||
match svt.call1(
|
||||
document.unchecked_ref(),
|
||||
cb.as_ref().unchecked_ref(),
|
||||
) {
|
||||
Ok(view_transition) => {
|
||||
let class_list = document_element.class_list();
|
||||
let finished = Reflect::get(
|
||||
&view_transition,
|
||||
&JsValue::from_str("finished"),
|
||||
)
|
||||
.expect("no `finished` property on ViewTransition")
|
||||
.unchecked_into::<Promise>();
|
||||
let cb = Closure::new(Box::new(move |_| {
|
||||
if is_back_navigation {
|
||||
class_list.remove_1("router-back").unwrap();
|
||||
}
|
||||
class_list
|
||||
.remove_1(&format!("router-outlet-{level}"))
|
||||
.unwrap();
|
||||
})
|
||||
as Box<dyn FnMut(JsValue)>);
|
||||
_ = finished.then(&cb);
|
||||
cb.into_js_value();
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::log_1(&e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
leptos::logging::warn!(
|
||||
"NOTE: View transitions are not supported in this \
|
||||
browser; unless you provide a polyfill, view transitions \
|
||||
will not be applied."
|
||||
);
|
||||
fun();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ use web_sys::{Event, UrlSearchParams};
|
||||
#[derive(Clone)]
|
||||
pub struct BrowserUrl {
|
||||
url: ArcRwSignal<Url>,
|
||||
pending_navigation: Arc<Mutex<Option<oneshot::Sender<()>>>>,
|
||||
pub(crate) pending_navigation: Arc<Mutex<Option<oneshot::Sender<()>>>>,
|
||||
pub(crate) path_stack: ArcStoredValue<Vec<Url>>,
|
||||
pub(crate) is_back: ArcRwSignal<bool>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for BrowserUrl {
|
||||
@@ -59,10 +61,14 @@ impl LocationProvider for BrowserUrl {
|
||||
|
||||
fn new() -> Result<Self, JsValue> {
|
||||
let url = ArcRwSignal::new(Self::current()?);
|
||||
let pending_navigation = Default::default();
|
||||
let path_stack = ArcStoredValue::new(
|
||||
Self::current().map(|n| vec![n]).unwrap_or_default(),
|
||||
);
|
||||
Ok(Self {
|
||||
url,
|
||||
pending_navigation,
|
||||
pending_navigation: Default::default(),
|
||||
path_stack,
|
||||
is_back: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -114,6 +120,7 @@ impl LocationProvider for BrowserUrl {
|
||||
let navigate = {
|
||||
let url = self.url.clone();
|
||||
let pending = Arc::clone(&self.pending_navigation);
|
||||
let this = self.clone();
|
||||
move |new_url: Url, loc| {
|
||||
let same_path = {
|
||||
let curr = url.read_untracked();
|
||||
@@ -121,21 +128,29 @@ impl LocationProvider for BrowserUrl {
|
||||
&& curr.path() == new_url.path()
|
||||
};
|
||||
|
||||
url.set(new_url);
|
||||
url.set(new_url.clone());
|
||||
if same_path {
|
||||
Self::complete_navigation(&loc);
|
||||
this.complete_navigation(&loc);
|
||||
}
|
||||
let pending = Arc::clone(&pending);
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
if !same_path {
|
||||
*pending.lock().or_poisoned() = Some(tx);
|
||||
}
|
||||
let url = url.clone();
|
||||
let this = this.clone();
|
||||
async move {
|
||||
if !same_path {
|
||||
// if it has been canceled, ignore
|
||||
// otherwise, complete navigation -- i.e., set URL in address bar
|
||||
if rx.await.is_ok() {
|
||||
Self::complete_navigation(&loc);
|
||||
// only update the URL in the browser if this is still the current URL
|
||||
// if we've navigated to another page in the meantime, don't update the
|
||||
// browser URL
|
||||
let curr = url.read_untracked();
|
||||
if curr == new_url {
|
||||
this.complete_navigation(&loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,8 +181,19 @@ impl LocationProvider for BrowserUrl {
|
||||
// handle popstate event (forward/back navigation)
|
||||
let cb = {
|
||||
let url = self.url.clone();
|
||||
let path_stack = self.path_stack.clone();
|
||||
let is_back = self.is_back.clone();
|
||||
move || match Self::current() {
|
||||
Ok(new_url) => url.set(new_url),
|
||||
Ok(new_url) => {
|
||||
let stack = path_stack.read_value();
|
||||
let is_navigating_back = stack.len() == 1
|
||||
|| (stack.len() >= 2
|
||||
&& stack.get(stack.len() - 2) == Some(&new_url));
|
||||
|
||||
is_back.set(is_navigating_back);
|
||||
|
||||
url.set(new_url);
|
||||
}
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("{e:?}");
|
||||
@@ -192,7 +218,7 @@ impl LocationProvider for BrowserUrl {
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_navigation(loc: &LocationChange) {
|
||||
fn complete_navigation(&self, loc: &LocationChange) {
|
||||
let history = window().history().unwrap();
|
||||
|
||||
if loc.replace {
|
||||
@@ -210,6 +236,14 @@ impl LocationProvider for BrowserUrl {
|
||||
.push_state_with_url(state, "", Some(&loc.value))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// add this URL to the "path stack" for detecting back navigations, and
|
||||
// unset "navigating back" state
|
||||
if let Ok(url) = Self::current() {
|
||||
self.path_stack.write_value().push(url);
|
||||
self.is_back.set(false);
|
||||
}
|
||||
|
||||
// scroll to el
|
||||
Self::scroll_to_el(loc.scroll);
|
||||
}
|
||||
@@ -231,6 +265,10 @@ impl LocationProvider for BrowserUrl {
|
||||
leptos::logging::error!("Failed to redirect: {e:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn is_back(&self) -> ReadSignal<bool> {
|
||||
self.is_back.read_only().into()
|
||||
}
|
||||
}
|
||||
|
||||
fn search_params_from_web_url(
|
||||
|
||||
@@ -36,22 +36,42 @@ impl Url {
|
||||
&self.origin
|
||||
}
|
||||
|
||||
pub fn origin_mut(&mut self) -> &mut String {
|
||||
&mut self.origin
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn path_mut(&mut self) -> &mut str {
|
||||
&mut self.path
|
||||
}
|
||||
|
||||
pub fn search(&self) -> &str {
|
||||
&self.search
|
||||
}
|
||||
|
||||
pub fn search_mut(&mut self) -> &mut String {
|
||||
&mut self.search
|
||||
}
|
||||
|
||||
pub fn search_params(&self) -> &ParamsMap {
|
||||
&self.search_params
|
||||
}
|
||||
|
||||
pub fn search_params_mut(&mut self) -> &mut ParamsMap {
|
||||
&mut self.search_params
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> &str {
|
||||
&self.hash
|
||||
}
|
||||
|
||||
pub fn hash_mut(&mut self) -> &mut String {
|
||||
&mut self.hash
|
||||
}
|
||||
|
||||
pub fn provide_server_action_error(&self) {
|
||||
let search_params = self.search_params();
|
||||
if let (Some(err), Some(path)) = (
|
||||
@@ -62,6 +82,19 @@ impl Url {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_full_path(&self) -> String {
|
||||
let mut path = self.path.to_string();
|
||||
if !self.search.is_empty() {
|
||||
path.push('?');
|
||||
path.push_str(&self.search);
|
||||
}
|
||||
if !self.hash.is_empty() {
|
||||
path.push('#');
|
||||
path.push_str(&self.hash);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
pub fn escape(s: &str) -> String {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
@@ -158,7 +191,7 @@ pub trait LocationProvider: Clone + 'static {
|
||||
fn ready_to_complete(&self);
|
||||
|
||||
/// Update the browser's history to reflect a new location.
|
||||
fn complete_navigation(loc: &LocationChange);
|
||||
fn complete_navigation(&self, loc: &LocationChange);
|
||||
|
||||
fn parse(url: &str) -> Result<Url, Self::Error> {
|
||||
Self::parse_with_base(url, BASE)
|
||||
@@ -167,6 +200,9 @@ pub trait LocationProvider: Clone + 'static {
|
||||
fn parse_with_base(url: &str, base: &str) -> Result<Url, Self::Error>;
|
||||
|
||||
fn redirect(loc: &str);
|
||||
|
||||
/// Whether we are currently in a "back" navigation.
|
||||
fn is_back(&self) -> ReadSignal<bool>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use either_of::*;
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
use tachys::view::{any_view::AnyView, Render};
|
||||
use tachys::view::any_view::{AnyView, IntoAny};
|
||||
|
||||
pub trait ChooseView
|
||||
where
|
||||
Self: Send + Clone + 'static,
|
||||
{
|
||||
type Output;
|
||||
|
||||
fn choose(self) -> impl Future<Output = Self::Output>;
|
||||
fn choose(self) -> impl Future<Output = AnyView>;
|
||||
|
||||
fn preload(&self) -> impl Future<Output = ()>;
|
||||
}
|
||||
@@ -16,12 +14,10 @@ where
|
||||
impl<F, View> ChooseView for F
|
||||
where
|
||||
F: Fn() -> View + Send + Clone + 'static,
|
||||
View: Render + Send,
|
||||
View: IntoAny,
|
||||
{
|
||||
type Output = View;
|
||||
|
||||
async fn choose(self) -> Self::Output {
|
||||
self()
|
||||
async fn choose(self) -> AnyView {
|
||||
self().into_any()
|
||||
}
|
||||
|
||||
async fn preload(&self) {}
|
||||
@@ -31,10 +27,8 @@ impl<T> ChooseView for Lazy<T>
|
||||
where
|
||||
T: LazyRoute,
|
||||
{
|
||||
type Output = AnyView;
|
||||
|
||||
async fn choose(self) -> Self::Output {
|
||||
T::data().view().await
|
||||
async fn choose(self) -> AnyView {
|
||||
T::data().view().await.into_any()
|
||||
}
|
||||
|
||||
async fn preload(&self) {
|
||||
@@ -74,9 +68,9 @@ impl<T> Default for Lazy<T> {
|
||||
}
|
||||
|
||||
impl ChooseView for () {
|
||||
type Output = ();
|
||||
|
||||
async fn choose(self) -> Self::Output {}
|
||||
async fn choose(self) -> AnyView {
|
||||
().into_any()
|
||||
}
|
||||
|
||||
async fn preload(&self) {}
|
||||
}
|
||||
@@ -86,12 +80,10 @@ where
|
||||
A: ChooseView,
|
||||
B: ChooseView,
|
||||
{
|
||||
type Output = Either<A::Output, B::Output>;
|
||||
|
||||
async fn choose(self) -> Self::Output {
|
||||
async fn choose(self) -> AnyView {
|
||||
match self {
|
||||
Either::Left(f) => Either::Left(f.choose().await),
|
||||
Either::Right(f) => Either::Right(f.choose().await),
|
||||
Either::Left(f) => f.choose().await.into_any(),
|
||||
Either::Right(f) => f.choose().await.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +101,9 @@ macro_rules! tuples {
|
||||
where
|
||||
$($ty: ChooseView,)*
|
||||
{
|
||||
type Output = $either<$($ty::Output,)*>;
|
||||
|
||||
async fn choose(self ) -> Self::Output {
|
||||
async fn choose(self ) -> AnyView {
|
||||
match self {
|
||||
$($either::$ty(f) => $either::$ty(f.choose().await),)*
|
||||
$($either::$ty(f) => f.choose().await.into_any(),)*
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::{PartialPathMatch, PathSegment};
|
||||
use std::borrow::Cow;
|
||||
mod param_segments;
|
||||
mod static_segment;
|
||||
mod tuples;
|
||||
@@ -13,12 +12,9 @@ pub use static_segment::*;
|
||||
/// as subsequent segments of the URL and tries to match them all. For a "vertical"
|
||||
/// matching that sees a tuple as alternatives to one another, see [`RouteChild`](super::RouteChild).
|
||||
pub trait PossibleRouteMatch {
|
||||
type ParamsIter: IntoIterator<Item = (Cow<'static, str>, String)>;
|
||||
const OPTIONAL: bool = false;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter>>;
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>);
|
||||
}
|
||||
|
||||
@@ -14,14 +14,16 @@ use std::borrow::Cow;
|
||||
///
|
||||
/// // Manual definition
|
||||
/// let manual = (ParamSegment("message"),);
|
||||
/// let (key, value) = manual.test(path)?.params().last()?;
|
||||
/// let params = manual.test(path)?.params();
|
||||
/// let (key, value) = params.last()?;
|
||||
///
|
||||
/// assert_eq!(key, "message");
|
||||
/// assert_eq!(value, "hello");
|
||||
///
|
||||
/// // Macro definition
|
||||
/// let using_macro = path!("/:message");
|
||||
/// let (key, value) = using_macro.test(path)?.params().last()?;
|
||||
/// let params = using_macro.test(path)?.params();
|
||||
/// let (key, value) = params.last()?;
|
||||
///
|
||||
/// assert_eq!(key, "message");
|
||||
/// assert_eq!(value, "hello");
|
||||
@@ -33,12 +35,7 @@ use std::borrow::Cow;
|
||||
pub struct ParamSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for ParamSegment {
|
||||
type ParamsIter = iter::Once<(Cow<'static, str>, String)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let mut matched_len = 0;
|
||||
let mut param_offset = 0;
|
||||
let mut param_len = 0;
|
||||
@@ -66,10 +63,10 @@ impl PossibleRouteMatch for ParamSegment {
|
||||
}
|
||||
|
||||
let (matched, remaining) = path.split_at(matched_len);
|
||||
let param_value = iter::once((
|
||||
let param_value = vec![(
|
||||
Cow::Borrowed(self.0),
|
||||
path[param_offset..param_len + param_offset].to_string(),
|
||||
));
|
||||
)];
|
||||
Some(PartialPathMatch::new(remaining, param_value, matched))
|
||||
}
|
||||
|
||||
@@ -93,14 +90,16 @@ impl PossibleRouteMatch for ParamSegment {
|
||||
///
|
||||
/// // Manual definition
|
||||
/// let manual = (StaticSegment("echo"), WildcardSegment("kitchen_sink"));
|
||||
/// let (key, value) = manual.test(path)?.params().last()?;
|
||||
/// let params = manual.test(path)?.params();
|
||||
/// let (key, value) = params.last()?;
|
||||
///
|
||||
/// assert_eq!(key, "kitchen_sink");
|
||||
/// assert_eq!(value, "send/sync/and/static");
|
||||
///
|
||||
/// // Macro definition
|
||||
/// let using_macro = path!("/echo/*else");
|
||||
/// let (key, value) = using_macro.test(path)?.params().last()?;
|
||||
/// let params = using_macro.test(path)?.params();
|
||||
/// let (key, value) = params.last()?;
|
||||
///
|
||||
/// assert_eq!(key, "else");
|
||||
/// assert_eq!(value, "send/sync/and/static");
|
||||
@@ -122,12 +121,7 @@ impl PossibleRouteMatch for ParamSegment {
|
||||
pub struct WildcardSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for WildcardSegment {
|
||||
type ParamsIter = iter::Once<(Cow<'static, str>, String)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let mut matched_len = 0;
|
||||
let mut param_offset = 0;
|
||||
let mut param_len = 0;
|
||||
@@ -148,7 +142,11 @@ impl PossibleRouteMatch for WildcardSegment {
|
||||
Cow::Borrowed(self.0),
|
||||
path[param_offset..param_len + param_offset].to_string(),
|
||||
));
|
||||
Some(PartialPathMatch::new(remaining, param_value, matched))
|
||||
Some(PartialPathMatch::new(
|
||||
remaining,
|
||||
param_value.into_iter().collect(),
|
||||
matched,
|
||||
))
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
@@ -156,10 +154,64 @@ impl PossibleRouteMatch for WildcardSegment {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct OptionalParamSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for OptionalParamSegment {
|
||||
const OPTIONAL: bool = true;
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let mut matched_len = 0;
|
||||
let mut param_offset = 0;
|
||||
let mut param_len = 0;
|
||||
let mut test = path.chars();
|
||||
|
||||
// match an initial /
|
||||
if let Some('/') = test.next() {
|
||||
matched_len += 1;
|
||||
param_offset = 1;
|
||||
}
|
||||
for char in test {
|
||||
// when we get a closing /, stop matching
|
||||
if char == '/' {
|
||||
break;
|
||||
}
|
||||
// otherwise, push into the matched param
|
||||
else {
|
||||
matched_len += char.len_utf8();
|
||||
param_len += char.len_utf8();
|
||||
}
|
||||
}
|
||||
|
||||
let matched_len = if matched_len == 1 && path.starts_with('/') {
|
||||
0
|
||||
} else {
|
||||
matched_len
|
||||
};
|
||||
let (matched, remaining) = path.split_at(matched_len);
|
||||
let param_value = (matched_len > 0)
|
||||
.then(|| {
|
||||
(
|
||||
Cow::Borrowed(self.0),
|
||||
path[param_offset..param_len + param_offset].to_string(),
|
||||
)
|
||||
})
|
||||
.into_iter()
|
||||
.collect();
|
||||
Some(PartialPathMatch::new(remaining, param_value, matched))
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
path.push(PathSegment::OptionalParam(self.0.into()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PossibleRouteMatch;
|
||||
use crate::{ParamSegment, StaticSegment, WildcardSegment};
|
||||
use crate::{
|
||||
OptionalParamSegment, ParamSegment, StaticSegment, WildcardSegment,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn single_param_match() {
|
||||
@@ -168,7 +220,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("a".into(), "foo".into()));
|
||||
}
|
||||
|
||||
@@ -179,7 +231,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "/");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("a".into(), "foo".into()));
|
||||
}
|
||||
|
||||
@@ -190,7 +242,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("a".into(), "foo".into()));
|
||||
assert_eq!(params[1], ("b".into(), "bar".into()));
|
||||
}
|
||||
@@ -206,7 +258,94 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar/////");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("rest".into(), "////".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_param_can_match() {
|
||||
let path = "/foo";
|
||||
let def = OptionalParamSegment("a");
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("a".into(), "foo".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_param_can_not_match() {
|
||||
let path = "/";
|
||||
let def = OptionalParamSegment("a");
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "");
|
||||
assert_eq!(matched.remaining(), "/");
|
||||
let params = matched.params();
|
||||
assert_eq!(params.first(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_params_match_first() {
|
||||
let path = "/foo";
|
||||
let def = (OptionalParamSegment("a"), OptionalParamSegment("b"));
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("a".into(), "foo".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_params_can_match_both() {
|
||||
let path = "/foo/bar";
|
||||
let def = (OptionalParamSegment("a"), OptionalParamSegment("b"));
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("a".into(), "foo".into()));
|
||||
assert_eq!(params[1], ("b".into(), "bar".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching_after_optional_param() {
|
||||
let path = "/bar";
|
||||
let def = (OptionalParamSegment("a"), StaticSegment("bar"));
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_optional_params_match_first() {
|
||||
let path = "/foo/bar";
|
||||
let def = (
|
||||
OptionalParamSegment("a"),
|
||||
OptionalParamSegment("b"),
|
||||
StaticSegment("bar"),
|
||||
);
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("a".into(), "foo".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_optionals_can_match_both() {
|
||||
let path = "/foo/qux/bar";
|
||||
let def = (
|
||||
OptionalParamSegment("a"),
|
||||
OptionalParamSegment("b"),
|
||||
StaticSegment("bar"),
|
||||
);
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/qux/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params();
|
||||
assert_eq!(params[0], ("a".into(), "foo".into()));
|
||||
assert_eq!(params[1], ("b".into(), "qux".into()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
|
||||
use core::iter;
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
use std::fmt::Debug;
|
||||
|
||||
impl PossibleRouteMatch for () {
|
||||
type ParamsIter = iter::Empty<(Cow<'static, str>, String)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
|
||||
Some(PartialPathMatch::new(path, iter::empty(), ""))
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
Some(PartialPathMatch::new(path, vec![], ""))
|
||||
}
|
||||
|
||||
fn generate_path(&self, _path: &mut Vec<PathSegment>) {}
|
||||
@@ -44,14 +38,14 @@ impl AsPath for &'static str {
|
||||
///
|
||||
/// // Params are empty as we had no `ParamSegement`s or `WildcardSegment`s
|
||||
/// // If you did have additional dynamic segments, this would not be empty.
|
||||
/// assert_eq!(matched.params().count(), 0);
|
||||
/// assert_eq!(matched.params().len(), 0);
|
||||
///
|
||||
/// // Macro definition
|
||||
/// let using_macro = path!("/users");
|
||||
/// let matched = manual.test(path)?;
|
||||
/// assert_eq!(matched.matched(), "/users");
|
||||
///
|
||||
/// assert_eq!(matched.params().count(), 0);
|
||||
/// assert_eq!(matched.params().len(), 0);
|
||||
///
|
||||
/// # Some(())
|
||||
/// # })().unwrap();
|
||||
@@ -60,12 +54,7 @@ impl AsPath for &'static str {
|
||||
pub struct StaticSegment<T: AsPath>(pub T);
|
||||
|
||||
impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
|
||||
type ParamsIter = iter::Empty<(Cow<'static, str>, String)>;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let mut matched_len = 0;
|
||||
let mut test = path.chars().peekable();
|
||||
let mut this = self.0.as_path().chars();
|
||||
@@ -113,8 +102,7 @@ impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
|
||||
// the remaining is built from the path in, with the slice moved
|
||||
// by the length of this match
|
||||
let (matched, remaining) = path.split_at(matched_len);
|
||||
has_matched
|
||||
.then(|| PartialPathMatch::new(remaining, iter::empty(), matched))
|
||||
has_matched.then(|| PartialPathMatch::new(remaining, vec![], matched))
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
@@ -151,7 +139,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
@@ -162,7 +150,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
@@ -187,7 +175,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "/");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
@@ -198,7 +186,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo");
|
||||
assert_eq!(matched.remaining(), "/");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
@@ -209,7 +197,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
@@ -220,7 +208,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
@@ -252,7 +240,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
@@ -270,7 +258,7 @@ mod tests {
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "/foo/bar");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params().collect::<Vec<_>>();
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
|
||||
use core::iter::Chain;
|
||||
|
||||
macro_rules! chain_types {
|
||||
($first:ty, $second:ty, ) => {
|
||||
Chain<
|
||||
$first,
|
||||
<<$second as PossibleRouteMatch>::ParamsIter as IntoIterator>::IntoIter
|
||||
>
|
||||
};
|
||||
($first:ty, $second:ty, $($rest:ty,)+) => {
|
||||
chain_types!(
|
||||
Chain<
|
||||
$first,
|
||||
<<$second as PossibleRouteMatch>::ParamsIter as IntoIterator>::IntoIter,
|
||||
>,
|
||||
$($rest,)+
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tuples {
|
||||
($first:ident => $($ty:ident),*) => {
|
||||
@@ -27,34 +8,69 @@ macro_rules! tuples {
|
||||
$first: PossibleRouteMatch,
|
||||
$($ty: PossibleRouteMatch),*,
|
||||
{
|
||||
type ParamsIter = chain_types!(<<$first>::ParamsIter as IntoIterator>::IntoIter, $($ty,)*);
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
// on the first run, include all optionals
|
||||
let mut include_optionals = {
|
||||
[$first::OPTIONAL, $($ty::OPTIONAL),*].into_iter().filter(|n| *n).count()
|
||||
};
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
|
||||
let mut matched_len = 0;
|
||||
#[allow(non_snake_case)]
|
||||
let ($first, $($ty,)*) = &self;
|
||||
let remaining = path;
|
||||
let PartialPathMatch {
|
||||
remaining,
|
||||
matched,
|
||||
params
|
||||
} = $first.test(remaining)?;
|
||||
matched_len += matched.len();
|
||||
let params_iter = params.into_iter();
|
||||
$(
|
||||
let PartialPathMatch {
|
||||
remaining,
|
||||
matched,
|
||||
params
|
||||
} = $ty.test(remaining)?;
|
||||
matched_len += matched.len();
|
||||
let params_iter = params_iter.chain(params);
|
||||
)*
|
||||
Some(PartialPathMatch {
|
||||
remaining,
|
||||
matched: &path[0..matched_len],
|
||||
params: params_iter
|
||||
})
|
||||
|
||||
loop {
|
||||
let mut nth_field = 0;
|
||||
let mut matched_len = 0;
|
||||
let mut r = path;
|
||||
|
||||
let mut p = Vec::new();
|
||||
let mut m = String::new();
|
||||
|
||||
if !$first::OPTIONAL || nth_field < include_optionals {
|
||||
match $first.test(r) {
|
||||
None => {
|
||||
return None;
|
||||
},
|
||||
Some(PartialPathMatch { remaining, matched, params }) => {
|
||||
p.extend(params.into_iter());
|
||||
m.push_str(matched);
|
||||
r = remaining;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
matched_len += m.len();
|
||||
$(
|
||||
if $ty::OPTIONAL {
|
||||
nth_field += 1;
|
||||
}
|
||||
if !$ty::OPTIONAL || nth_field < include_optionals {
|
||||
let PartialPathMatch {
|
||||
remaining,
|
||||
matched,
|
||||
params
|
||||
} = match $ty.test(r) {
|
||||
None => if $ty::OPTIONAL {
|
||||
return None;
|
||||
} else {
|
||||
if include_optionals == 0 {
|
||||
return None;
|
||||
}
|
||||
include_optionals -= 1;
|
||||
continue;
|
||||
},
|
||||
Some(v) => v,
|
||||
};
|
||||
r = remaining;
|
||||
matched_len += matched.len();
|
||||
p.extend(params);
|
||||
}
|
||||
)*
|
||||
return Some(PartialPathMatch {
|
||||
remaining: r,
|
||||
matched: &path[0..matched_len],
|
||||
params: p
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
@@ -74,12 +90,7 @@ where
|
||||
Self: core::fmt::Debug,
|
||||
A: PossibleRouteMatch,
|
||||
{
|
||||
type ParamsIter = A::ParamsIter;
|
||||
|
||||
fn test<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<PartialPathMatch<'a, Self::ParamsIter>> {
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let remaining = path;
|
||||
let PartialPathMatch {
|
||||
remaining,
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::{static_routes::RegenerationFn, Method, SsrMode};
|
||||
pub use horizontal::*;
|
||||
pub use nested::*;
|
||||
use std::{borrow::Cow, collections::HashSet};
|
||||
use tachys::view::{Render, RenderHtml};
|
||||
pub use vertical::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -95,32 +94,26 @@ pub struct RouteMatchId(pub(crate) u16);
|
||||
|
||||
pub trait MatchInterface {
|
||||
type Child: MatchInterface + MatchParams + 'static;
|
||||
type View: Render + RenderHtml + Send + 'static;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId;
|
||||
|
||||
fn as_matched(&self) -> &str;
|
||||
|
||||
fn into_view_and_child(
|
||||
self,
|
||||
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>);
|
||||
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>);
|
||||
}
|
||||
|
||||
pub trait MatchParams {
|
||||
type Params: IntoIterator<Item = (Cow<'static, str>, String)>;
|
||||
|
||||
fn to_params(&self) -> Self::Params;
|
||||
fn to_params(&self) -> Vec<(Cow<'static, str>, String)>;
|
||||
}
|
||||
|
||||
pub trait MatchNestedRoutes {
|
||||
type Data;
|
||||
type View;
|
||||
type Match: MatchInterface + MatchParams;
|
||||
|
||||
fn match_nested<'a>(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &str);
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &'a str);
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
@@ -260,13 +253,13 @@ mod tests {
|
||||
);
|
||||
|
||||
let matched = routes.match_route("/about").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
let params = matched.to_params();
|
||||
assert!(params.is_empty());
|
||||
let matched = routes.match_route("/blog").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
let params = matched.to_params();
|
||||
assert!(params.is_empty());
|
||||
let matched = routes.match_route("/blog/post/42").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
let params = matched.to_params();
|
||||
assert_eq!(params, vec![("id".into(), "42".into())]);
|
||||
}
|
||||
|
||||
@@ -302,34 +295,34 @@ mod tests {
|
||||
assert!(matched.is_none());
|
||||
|
||||
let matched = routes.match_route("/portfolio/about").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
let params = matched.to_params();
|
||||
assert!(params.is_empty());
|
||||
|
||||
let matched = routes.match_route("/portfolio/blog/post/42").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
let params = matched.to_params();
|
||||
assert_eq!(params, vec![("id".into(), "42".into())]);
|
||||
|
||||
let matched = routes.match_route("/portfolio/contact").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
let params = matched.to_params();
|
||||
assert_eq!(params, vec![("any".into(), "".into())]);
|
||||
|
||||
let matched = routes.match_route("/portfolio/contact/foobar").unwrap();
|
||||
let params = matched.to_params().collect::<Vec<_>>();
|
||||
let params = matched.to_params();
|
||||
assert_eq!(params, vec![("any".into(), "foobar".into())]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PartialPathMatch<'a, ParamsIter> {
|
||||
pub struct PartialPathMatch<'a> {
|
||||
pub(crate) remaining: &'a str,
|
||||
pub(crate) params: ParamsIter,
|
||||
pub(crate) params: Vec<(Cow<'static, str>, String)>,
|
||||
pub(crate) matched: &'a str,
|
||||
}
|
||||
|
||||
impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
|
||||
impl<'a> PartialPathMatch<'a> {
|
||||
pub fn new(
|
||||
remaining: &'a str,
|
||||
params: ParamsIter,
|
||||
params: Vec<(Cow<'static, str>, String)>,
|
||||
matched: &'a str,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -347,7 +340,7 @@ impl<'a, ParamsIter> PartialPathMatch<'a, ParamsIter> {
|
||||
self.remaining
|
||||
}
|
||||
|
||||
pub fn params(self) -> ParamsIter {
|
||||
pub fn params(self) -> Vec<(Cow<'static, str>, String)> {
|
||||
self.params
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ use std::{
|
||||
collections::HashSet,
|
||||
sync::atomic::{AtomicU16, Ordering},
|
||||
};
|
||||
use tachys::view::{Render, RenderHtml};
|
||||
|
||||
mod tuples;
|
||||
|
||||
@@ -97,21 +96,19 @@ impl<Segments, Data, View> NestedRoute<Segments, (), Data, View> {
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct NestedMatch<ParamsIter, Child, View> {
|
||||
pub struct NestedMatch<Child, View> {
|
||||
id: RouteMatchId,
|
||||
/// The portion of the full path matched only by this nested route.
|
||||
matched: String,
|
||||
/// The map of params matched only by this nested route.
|
||||
params: ParamsIter,
|
||||
params: Vec<(Cow<'static, str>, String)>,
|
||||
/// The nested route.
|
||||
child: Option<Child>,
|
||||
view_fn: View,
|
||||
}
|
||||
|
||||
impl<ParamsIter, Child, View> fmt::Debug
|
||||
for NestedMatch<ParamsIter, Child, View>
|
||||
impl<Child, View> fmt::Debug for NestedMatch<Child, View>
|
||||
where
|
||||
ParamsIter: fmt::Debug,
|
||||
Child: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -123,28 +120,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<ParamsIter, Child, View> MatchParams
|
||||
for NestedMatch<ParamsIter, Child, View>
|
||||
where
|
||||
ParamsIter: IntoIterator<Item = (Cow<'static, str>, String)> + Clone,
|
||||
{
|
||||
type Params = ParamsIter;
|
||||
|
||||
impl<Child, View> MatchParams for NestedMatch<Child, View> {
|
||||
#[inline(always)]
|
||||
fn to_params(&self) -> Self::Params {
|
||||
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
|
||||
self.params.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<ParamsIter, Child, View> MatchInterface
|
||||
for NestedMatch<ParamsIter, Child, View>
|
||||
impl<Child, View> MatchInterface for NestedMatch<Child, View>
|
||||
where
|
||||
Child: MatchInterface + MatchParams + 'static,
|
||||
View: ChooseView,
|
||||
View::Output: Render + RenderHtml + Send + 'static,
|
||||
{
|
||||
type Child = Child;
|
||||
type View = View::Output;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
self.id
|
||||
@@ -154,9 +142,7 @@ where
|
||||
&self.matched
|
||||
}
|
||||
|
||||
fn into_view_and_child(
|
||||
self,
|
||||
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>) {
|
||||
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
|
||||
(self.view_fn, self.child)
|
||||
}
|
||||
}
|
||||
@@ -166,23 +152,13 @@ impl<Segments, Children, Data, View> MatchNestedRoutes
|
||||
where
|
||||
Self: 'static,
|
||||
Segments: PossibleRouteMatch + std::fmt::Debug,
|
||||
<<Segments as PossibleRouteMatch>::ParamsIter as IntoIterator>::IntoIter: Clone,
|
||||
Children: MatchNestedRoutes,
|
||||
<<<Children as MatchNestedRoutes>::Match as MatchParams>::Params as IntoIterator>::IntoIter: Clone,
|
||||
Children::Match: MatchParams,
|
||||
Children: 'static,
|
||||
<Children::Match as MatchParams>::Params: Clone,
|
||||
Children::Match: MatchParams,
|
||||
Children: 'static,
|
||||
View: ChooseView + Clone,
|
||||
View::Output: Render + RenderHtml + Send + 'static,
|
||||
{
|
||||
type Data = Data;
|
||||
type View = View::Output;
|
||||
type Match = NestedMatch<iter::Chain<
|
||||
<Segments::ParamsIter as IntoIterator>::IntoIter,
|
||||
Either<iter::Empty::<
|
||||
(Cow<'static, str>, String)
|
||||
>, <<Children::Match as MatchParams>::Params as IntoIterator>::IntoIter>
|
||||
>, Children::Match, View>;
|
||||
type Match = NestedMatch<Children::Match, View>;
|
||||
|
||||
fn match_nested<'a>(
|
||||
&'a self,
|
||||
@@ -193,33 +169,34 @@ where
|
||||
.and_then(
|
||||
|PartialPathMatch {
|
||||
remaining,
|
||||
params,
|
||||
mut params,
|
||||
matched,
|
||||
}| {
|
||||
let (_, inner, remaining) = match &self.children {
|
||||
None => (None, None, remaining),
|
||||
Some(children) => {
|
||||
let (inner, remaining) = children.match_nested(remaining);
|
||||
let (inner, remaining) =
|
||||
children.match_nested(remaining);
|
||||
let (id, inner) = inner?;
|
||||
(Some(id), Some(inner), remaining)
|
||||
(Some(id), Some(inner), remaining)
|
||||
}
|
||||
};
|
||||
let params = params.into_iter();
|
||||
let inner_params = match &inner {
|
||||
None => Either::Left(iter::empty()),
|
||||
Some(inner) => Either::Right(inner.to_params().into_iter())
|
||||
};
|
||||
let inner_params = inner
|
||||
.as_ref()
|
||||
.map(|inner| inner.to_params())
|
||||
.unwrap_or_default();
|
||||
|
||||
let id = RouteMatchId(self.id);
|
||||
|
||||
if remaining.is_empty() || remaining == "/" {
|
||||
params.extend(inner_params);
|
||||
Some((
|
||||
Some((
|
||||
id,
|
||||
NestedMatch {
|
||||
id,
|
||||
matched: matched.to_string(),
|
||||
params: params.chain(inner_params),
|
||||
params,
|
||||
child: inner,
|
||||
view_fn: self.view.clone(),
|
||||
},
|
||||
@@ -245,9 +222,9 @@ where
|
||||
let regenerate = match &ssr_mode {
|
||||
SsrMode::Static(data) => match data.regenerate.as_ref() {
|
||||
None => vec![],
|
||||
Some(regenerate) => vec![regenerate.clone()]
|
||||
}
|
||||
_ => vec![]
|
||||
Some(regenerate) => vec![regenerate.clone()],
|
||||
},
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
match children {
|
||||
@@ -255,32 +232,41 @@ where
|
||||
segments: segment_routes,
|
||||
ssr_mode,
|
||||
methods,
|
||||
regenerate
|
||||
regenerate,
|
||||
})),
|
||||
Some(children) => {
|
||||
Either::Right(children.generate_routes().into_iter().map(move |child| {
|
||||
// extend this route's segments with child segments
|
||||
let segments = segment_routes.clone().into_iter().chain(child.segments).collect();
|
||||
Either::Right(children.generate_routes().into_iter().map(
|
||||
move |child| {
|
||||
// extend this route's segments with child segments
|
||||
let segments = segment_routes
|
||||
.clone()
|
||||
.into_iter()
|
||||
.chain(child.segments)
|
||||
.collect();
|
||||
|
||||
let mut methods = methods.clone();
|
||||
methods.extend(child.methods);
|
||||
let mut methods = methods.clone();
|
||||
methods.extend(child.methods);
|
||||
|
||||
let mut regenerate = regenerate.clone();
|
||||
regenerate.extend(child.regenerate);
|
||||
let mut regenerate = regenerate.clone();
|
||||
regenerate.extend(child.regenerate);
|
||||
|
||||
if child.ssr_mode > ssr_mode {
|
||||
GeneratedRouteData {
|
||||
segments,
|
||||
ssr_mode: child.ssr_mode,
|
||||
methods, regenerate
|
||||
if child.ssr_mode > ssr_mode {
|
||||
GeneratedRouteData {
|
||||
segments,
|
||||
ssr_mode: child.ssr_mode,
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
} else {
|
||||
GeneratedRouteData {
|
||||
segments,
|
||||
ssr_mode: ssr_mode.clone(),
|
||||
methods,
|
||||
regenerate,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
GeneratedRouteData {
|
||||
segments,
|
||||
ssr_mode: ssr_mode.clone(), methods, regenerate
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,13 @@ use either_of::*;
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl MatchParams for () {
|
||||
type Params = iter::Empty<(Cow<'static, str>, String)>;
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
iter::empty()
|
||||
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchInterface for () {
|
||||
type Child = ();
|
||||
type View = ();
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
RouteMatchId(0)
|
||||
@@ -24,16 +21,13 @@ impl MatchInterface for () {
|
||||
""
|
||||
}
|
||||
|
||||
fn into_view_and_child(
|
||||
self,
|
||||
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>) {
|
||||
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
|
||||
((), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchNestedRoutes for () {
|
||||
type Data = ();
|
||||
type View = ();
|
||||
type Match = ();
|
||||
|
||||
fn match_nested<'a>(
|
||||
@@ -57,9 +51,7 @@ impl<A> MatchParams for (A,)
|
||||
where
|
||||
A: MatchParams,
|
||||
{
|
||||
type Params = A::Params;
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
|
||||
self.0.to_params()
|
||||
}
|
||||
}
|
||||
@@ -69,7 +61,6 @@ where
|
||||
A: MatchInterface + 'static,
|
||||
{
|
||||
type Child = A::Child;
|
||||
type View = A::View;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
self.0.as_id()
|
||||
@@ -79,9 +70,7 @@ where
|
||||
self.0.as_matched()
|
||||
}
|
||||
|
||||
fn into_view_and_child(
|
||||
self,
|
||||
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>) {
|
||||
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
|
||||
self.0.into_view_and_child()
|
||||
}
|
||||
}
|
||||
@@ -91,7 +80,6 @@ where
|
||||
A: MatchNestedRoutes + 'static,
|
||||
{
|
||||
type Data = A::Data;
|
||||
type View = A::View;
|
||||
type Match = A::Match;
|
||||
|
||||
fn match_nested<'a>(
|
||||
@@ -113,15 +101,10 @@ where
|
||||
A: MatchParams,
|
||||
B: MatchParams,
|
||||
{
|
||||
type Params = Either<
|
||||
<A::Params as IntoIterator>::IntoIter,
|
||||
<B::Params as IntoIterator>::IntoIter,
|
||||
>;
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
|
||||
match self {
|
||||
Either::Left(i) => Either::Left(i.to_params().into_iter()),
|
||||
Either::Right(i) => Either::Right(i.to_params().into_iter()),
|
||||
Either::Left(i) => i.to_params(),
|
||||
Either::Right(i) => i.to_params(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +115,6 @@ where
|
||||
B: MatchInterface,
|
||||
{
|
||||
type Child = Either<A::Child, B::Child>;
|
||||
type View = Either<A::View, B::View>;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
match self {
|
||||
@@ -148,9 +130,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn into_view_and_child(
|
||||
self,
|
||||
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>) {
|
||||
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
|
||||
match self {
|
||||
Either::Left(i) => {
|
||||
let (view, child) = i.into_view_and_child();
|
||||
@@ -170,7 +150,6 @@ where
|
||||
B: MatchNestedRoutes,
|
||||
{
|
||||
type Data = (A::Data, B::Data);
|
||||
type View = Either<A::View, B::View>;
|
||||
type Match = Either<A::Match, B::Match>;
|
||||
|
||||
fn match_nested<'a>(
|
||||
@@ -220,13 +199,9 @@ macro_rules! tuples {
|
||||
where
|
||||
$($ty: MatchParams),*,
|
||||
{
|
||||
type Params = $either<$(
|
||||
<$ty::Params as IntoIterator>::IntoIter,
|
||||
)*>;
|
||||
|
||||
fn to_params(&self) -> Self::Params {
|
||||
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
|
||||
match self {
|
||||
$($either::$ty(i) => $either::$ty(i.to_params().into_iter()),)*
|
||||
$($either::$ty(i) => i.to_params(),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +211,6 @@ macro_rules! tuples {
|
||||
$($ty: MatchInterface + 'static),*,
|
||||
{
|
||||
type Child = $either<$($ty::Child,)*>;
|
||||
type View = $either<$($ty::View,)*>;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
match self {
|
||||
@@ -253,7 +227,7 @@ macro_rules! tuples {
|
||||
fn into_view_and_child(
|
||||
self,
|
||||
) -> (
|
||||
impl ChooseView<Output = Self::View>,
|
||||
impl ChooseView,
|
||||
Option<Self::Child>,
|
||||
) {
|
||||
match self {
|
||||
@@ -270,7 +244,6 @@ macro_rules! tuples {
|
||||
$($ty: MatchNestedRoutes + 'static),*,
|
||||
{
|
||||
type Data = ($($ty::Data,)*);
|
||||
type View = $either<$($ty::View,)*>;
|
||||
type Match = $either<$($ty::Match,)*>;
|
||||
|
||||
fn match_nested<'a>(&'a self, path: &'a str) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
|
||||
|
||||
@@ -5,6 +5,7 @@ pub enum PathSegment {
|
||||
Unit,
|
||||
Static(Cow<'static, str>),
|
||||
Param(Cow<'static, str>),
|
||||
OptionalParam(Cow<'static, str>),
|
||||
Splat(Cow<'static, str>),
|
||||
}
|
||||
|
||||
@@ -14,7 +15,98 @@ impl PathSegment {
|
||||
PathSegment::Unit => "",
|
||||
PathSegment::Static(i) => i,
|
||||
PathSegment::Param(i) => i,
|
||||
PathSegment::OptionalParam(i) => i,
|
||||
PathSegment::Splat(i) => i,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExpandOptionals {
|
||||
fn expand_optionals(&self) -> Vec<Vec<PathSegment>>;
|
||||
}
|
||||
|
||||
impl ExpandOptionals for Vec<PathSegment> {
|
||||
fn expand_optionals(&self) -> Vec<Vec<PathSegment>> {
|
||||
let mut segments = vec![self.to_vec()];
|
||||
let mut checked = Vec::new();
|
||||
while let Some(next_to_check) = segments.pop() {
|
||||
let mut had_optional = false;
|
||||
for (idx, segment) in next_to_check.iter().enumerate() {
|
||||
if let PathSegment::OptionalParam(name) = segment {
|
||||
had_optional = true;
|
||||
let mut unit_variant = next_to_check.to_vec();
|
||||
unit_variant.remove(idx);
|
||||
let mut param_variant = next_to_check.to_vec();
|
||||
param_variant[idx] = PathSegment::Param(name.clone());
|
||||
segments.push(unit_variant);
|
||||
segments.push(param_variant);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !had_optional {
|
||||
checked.push(next_to_check.to_vec());
|
||||
}
|
||||
}
|
||||
checked
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{ExpandOptionals, PathSegment};
|
||||
|
||||
#[test]
|
||||
fn expand_optionals_on_plain() {
|
||||
let plain = vec![
|
||||
PathSegment::Static("a".into()),
|
||||
PathSegment::Param("b".into()),
|
||||
];
|
||||
assert_eq!(plain.expand_optionals(), vec![plain]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_optionals_once() {
|
||||
let plain = vec![
|
||||
PathSegment::OptionalParam("a".into()),
|
||||
PathSegment::Static("b".into()),
|
||||
];
|
||||
assert_eq!(
|
||||
plain.expand_optionals(),
|
||||
vec![
|
||||
vec![
|
||||
PathSegment::Param("a".into()),
|
||||
PathSegment::Static("b".into())
|
||||
],
|
||||
vec![PathSegment::Static("b".into())]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_optionals_twice() {
|
||||
let plain = vec![
|
||||
PathSegment::OptionalParam("a".into()),
|
||||
PathSegment::OptionalParam("b".into()),
|
||||
PathSegment::Static("c".into()),
|
||||
];
|
||||
assert_eq!(
|
||||
plain.expand_optionals(),
|
||||
vec![
|
||||
vec![
|
||||
PathSegment::Param("a".into()),
|
||||
PathSegment::Param("b".into()),
|
||||
PathSegment::Static("c".into()),
|
||||
],
|
||||
vec![
|
||||
PathSegment::Param("a".into()),
|
||||
PathSegment::Static("c".into()),
|
||||
],
|
||||
vec![
|
||||
PathSegment::Param("b".into()),
|
||||
PathSegment::Static("c".into()),
|
||||
],
|
||||
vec![PathSegment::Static("c".into())]
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
use super::PartialPathMatch;
|
||||
|
||||
pub trait ChooseRoute {
|
||||
fn choose_route<'a>(
|
||||
&self,
|
||||
path: &'a str,
|
||||
) -> Option<
|
||||
PartialPathMatch<'a, impl IntoIterator<Item = (&'a str, &'a str)>>,
|
||||
>;
|
||||
fn choose_route<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ use crate::{
|
||||
location::{LocationProvider, Url},
|
||||
matching::Routes,
|
||||
params::ParamsMap,
|
||||
view_transition::start_view_transition,
|
||||
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
|
||||
RouteList, RouteListing, RouteMatchId,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use either_of::{Either, EitherOf3};
|
||||
use futures::{future::join_all, FutureExt};
|
||||
use futures::{channel::oneshot, future::join_all, FutureExt};
|
||||
use leptos::{component, oco::Oco};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
@@ -16,6 +17,7 @@ use reactive_graph::{
|
||||
owner::{provide_context, use_context, Owner},
|
||||
signal::{ArcRwSignal, ArcTrigger},
|
||||
traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track},
|
||||
transition::AsyncTransition,
|
||||
wrappers::write::SignalSetter,
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
@@ -47,8 +49,8 @@ pub(crate) struct NestedRoutesView<Loc, Defs, FalFn> {
|
||||
pub current_url: ArcRwSignal<Url>,
|
||||
pub base: Option<Oco<'static, str>>,
|
||||
pub fallback: FalFn,
|
||||
#[allow(unused)] // TODO
|
||||
pub set_is_routing: Option<SignalSetter<bool>>,
|
||||
pub transition: bool,
|
||||
}
|
||||
|
||||
pub struct NestedRouteViewState<Fal>
|
||||
@@ -155,22 +157,48 @@ where
|
||||
state.outlets.clear();
|
||||
}
|
||||
Some(route) => {
|
||||
let mut loaders = Vec::new();
|
||||
route.rebuild_nested_route(
|
||||
if let Some(set_is_routing) = self.set_is_routing {
|
||||
set_is_routing.set(true);
|
||||
}
|
||||
|
||||
let mut preloaders = Vec::new();
|
||||
let mut full_loaders = Vec::new();
|
||||
let different_level = route.rebuild_nested_route(
|
||||
&self.current_url.read_untracked(),
|
||||
self.base,
|
||||
&mut 0,
|
||||
&mut loaders,
|
||||
&mut preloaders,
|
||||
&mut full_loaders,
|
||||
&mut state.outlets,
|
||||
&self.outer_owner,
|
||||
self.set_is_routing.is_some(),
|
||||
0,
|
||||
);
|
||||
|
||||
let location = self.location.clone();
|
||||
let is_back = location
|
||||
.as_ref()
|
||||
.map(|nav| nav.is_back().get_untracked())
|
||||
.unwrap_or(false);
|
||||
Executor::spawn_local(async move {
|
||||
let triggers = join_all(loaders).await;
|
||||
let triggers = join_all(preloaders).await;
|
||||
// tell each one of the outlet triggers that it's ready
|
||||
for trigger in triggers {
|
||||
trigger.notify();
|
||||
let notify = move || {
|
||||
for trigger in triggers {
|
||||
trigger.notify();
|
||||
}
|
||||
};
|
||||
if self.transition {
|
||||
start_view_transition(different_level, is_back, notify);
|
||||
} else {
|
||||
notify();
|
||||
}
|
||||
});
|
||||
|
||||
Executor::spawn_local(async move {
|
||||
join_all(full_loaders).await;
|
||||
if let Some(set_is_routing) = self.set_is_routing {
|
||||
set_is_routing.set(false);
|
||||
}
|
||||
if let Some(loc) = location {
|
||||
loc.ready_to_complete();
|
||||
@@ -426,9 +454,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
type OutletViewFn = Box<
|
||||
dyn Fn() -> Suspend<Pin<Box<dyn Future<Output = AnyView> + Send>>> + Send,
|
||||
>;
|
||||
type OutletViewFn = Box<dyn Fn() -> Suspend<AnyView> + Send>;
|
||||
|
||||
pub(crate) struct RouteContext {
|
||||
id: RouteMatchId,
|
||||
@@ -486,15 +512,19 @@ trait AddNestedRoute {
|
||||
parent: &Owner,
|
||||
);
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn rebuild_nested_route(
|
||||
self,
|
||||
url: &Url,
|
||||
base: Option<Oco<'static, str>>,
|
||||
items: &mut usize,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
full_loaders: &mut Vec<oneshot::Receiver<()>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
parent: &Owner,
|
||||
);
|
||||
set_is_routing: bool,
|
||||
level: u8,
|
||||
) -> u8;
|
||||
}
|
||||
|
||||
impl<Match> AddNestedRoute for Match
|
||||
@@ -629,15 +659,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn rebuild_nested_route(
|
||||
self,
|
||||
url: &Url,
|
||||
base: Option<Oco<'static, str>>,
|
||||
items: &mut usize,
|
||||
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
|
||||
full_loaders: &mut Vec<oneshot::Receiver<()>>,
|
||||
outlets: &mut Vec<RouteContext>,
|
||||
parent: &Owner,
|
||||
) {
|
||||
set_is_routing: bool,
|
||||
level: u8,
|
||||
) -> u8 {
|
||||
let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets
|
||||
.iter()
|
||||
.take(*items)
|
||||
@@ -647,7 +681,8 @@ where
|
||||
match current {
|
||||
// if there's nothing currently in the routes at this point, build from here
|
||||
None => {
|
||||
self.build_nested_route(url, base, loaders, outlets, parent);
|
||||
self.build_nested_route(url, base, preloaders, outlets, parent);
|
||||
level
|
||||
}
|
||||
Some(current) => {
|
||||
// a unique ID for each route, which allows us to compare when we get new matches
|
||||
@@ -716,11 +751,14 @@ where
|
||||
let old_owner =
|
||||
mem::replace(&mut current.owner, parent.child());
|
||||
let owner = current.owner.clone();
|
||||
let (full_tx, full_rx) = oneshot::channel();
|
||||
let full_tx = Mutex::new(Some(full_tx));
|
||||
full_loaders.push(full_rx);
|
||||
|
||||
// send the new view, with the new owner, through the channel to the Outlet,
|
||||
// and notify the trigger so that the reactive view inside the Outlet tracking
|
||||
// the trigger runs again
|
||||
loaders.push(Box::pin(owner.with(|| {
|
||||
preloaders.push(Box::pin(owner.with(|| {
|
||||
ScopedFuture::new({
|
||||
let owner = owner.clone();
|
||||
let trigger = current.trigger.clone();
|
||||
@@ -736,15 +774,26 @@ where
|
||||
Box::new(move || {
|
||||
let owner = owner.clone();
|
||||
let view = view.clone();
|
||||
let full_tx =
|
||||
full_tx.lock().or_poisoned().take();
|
||||
Suspend::new(Box::pin(async move {
|
||||
let view = SendWrapper::new(
|
||||
owner.with(|| {
|
||||
ScopedFuture::new(
|
||||
view.choose(),
|
||||
async move {
|
||||
if set_is_routing {
|
||||
AsyncTransition::run(|| view.choose()).await
|
||||
} else {
|
||||
view.choose().await
|
||||
}
|
||||
}
|
||||
)
|
||||
}),
|
||||
);
|
||||
let view = view.await;
|
||||
if let Some(tx) = full_tx {
|
||||
_ = tx.send(());
|
||||
}
|
||||
owner.with(|| {
|
||||
OwnedView::new(view).into_any()
|
||||
})
|
||||
@@ -766,11 +815,11 @@ where
|
||||
// if this children has matches, then rebuild the lower section of the tree
|
||||
if let Some(child) = child {
|
||||
child.build_nested_route(
|
||||
url, base, loaders, outlets, &owner,
|
||||
url, base, preloaders, outlets, &owner,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
return level;
|
||||
}
|
||||
|
||||
// otherwise, set the params and URL signals,
|
||||
@@ -782,8 +831,18 @@ where
|
||||
let owner = current.owner.clone();
|
||||
*items += 1;
|
||||
child.rebuild_nested_route(
|
||||
url, base, items, loaders, outlets, &owner,
|
||||
);
|
||||
url,
|
||||
base,
|
||||
items,
|
||||
preloaders,
|
||||
full_loaders,
|
||||
outlets,
|
||||
&owner,
|
||||
set_is_routing,
|
||||
level + 1,
|
||||
)
|
||||
} else {
|
||||
level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ impl ParamsMap {
|
||||
}
|
||||
|
||||
/// Inserts a value into the map.
|
||||
///
|
||||
/// If a value with that key already exists, the new value will be added to it.
|
||||
/// To replace the value instead, see [`replace`].
|
||||
pub fn insert(&mut self, key: impl Into<Cow<'static, str>>, value: String) {
|
||||
let value = unescape(&value);
|
||||
|
||||
@@ -32,6 +35,23 @@ impl ParamsMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a value into the map, replacing any existing value for that key.
|
||||
pub fn replace(
|
||||
&mut self,
|
||||
key: impl Into<Cow<'static, str>>,
|
||||
value: String,
|
||||
) {
|
||||
let value = unescape(&value);
|
||||
|
||||
let key = key.into();
|
||||
if let Some(prev) = self.0.iter_mut().find(|(k, _)| k == &key) {
|
||||
prev.1.clear();
|
||||
prev.1.push(value);
|
||||
} else {
|
||||
self.0.push((key, vec![value]));
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the most-recently-added value of this param from the map.
|
||||
pub fn get(&self, key: &str) -> Option<String> {
|
||||
self.get_str(key).map(ToOwned::to_owned)
|
||||
|
||||
@@ -247,6 +247,7 @@ impl StaticPath {
|
||||
}
|
||||
paths = new_paths;
|
||||
}
|
||||
OptionalParam(_) => todo!(),
|
||||
}
|
||||
}
|
||||
paths
|
||||
@@ -353,7 +354,7 @@ impl ResolvedStaticPath {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
}
|
||||
drop(owner);
|
||||
owner.unset();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router_macro"
|
||||
version = "0.7.0-gamma"
|
||||
version = "0.7.0-rc0"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -18,4 +18,4 @@ proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos_router = { version = "0.7.0-beta" }
|
||||
leptos_router = { path = "../router" }
|
||||
|
||||
@@ -14,12 +14,16 @@ const RFC3986_PCHAR_OTHER: [char; 1] = ['@'];
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use leptos_router::{path, ParamSegment, StaticSegment, WildcardSegment};
|
||||
/// use leptos_router::{
|
||||
/// path, OptionalParamSegment, ParamSegment, StaticSegment,
|
||||
/// WildcardSegment,
|
||||
/// };
|
||||
///
|
||||
/// let path = path!("/foo/:bar/*any");
|
||||
/// let path = path!("/foo/:bar/:baz?/*any");
|
||||
/// let output = (
|
||||
/// StaticSegment("foo"),
|
||||
/// ParamSegment("bar"),
|
||||
/// OptionalParamSegment("baz"),
|
||||
/// WildcardSegment("any"),
|
||||
/// );
|
||||
///
|
||||
@@ -41,6 +45,7 @@ struct Segments(pub Vec<Segment>);
|
||||
enum Segment {
|
||||
Static(String),
|
||||
Param(String),
|
||||
OptionalParam(String),
|
||||
Wildcard(String),
|
||||
}
|
||||
|
||||
@@ -93,7 +98,11 @@ impl SegmentParser {
|
||||
|
||||
for segment in current_str.split('/') {
|
||||
if let Some(segment) = segment.strip_prefix(':') {
|
||||
segments.push(Segment::Param(segment.to_string()));
|
||||
if let Some(segment) = segment.strip_suffix('?') {
|
||||
segments.push(Segment::OptionalParam(segment.to_string()));
|
||||
} else {
|
||||
segments.push(Segment::Param(segment.to_string()));
|
||||
}
|
||||
} else if let Some(segment) = segment.strip_prefix('*') {
|
||||
segments.push(Segment::Wildcard(segment.to_string()));
|
||||
} else {
|
||||
@@ -156,6 +165,10 @@ impl ToTokens for Segment {
|
||||
Segment::Param(p) => {
|
||||
tokens.extend(quote! { leptos_router::ParamSegment(#p) });
|
||||
}
|
||||
Segment::OptionalParam(p) => {
|
||||
tokens
|
||||
.extend(quote! { leptos_router::OptionalParamSegment(#p) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use leptos_router::{ParamSegment, StaticSegment, WildcardSegment};
|
||||
use leptos_router::{
|
||||
OptionalParamSegment, ParamSegment, StaticSegment, WildcardSegment,
|
||||
};
|
||||
use leptos_router_macro::path;
|
||||
|
||||
#[test]
|
||||
@@ -86,6 +88,12 @@ fn parses_single_param() {
|
||||
assert_eq!(output, (ParamSegment("id"),));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_optional_param() {
|
||||
let output = path!("/:id?");
|
||||
assert_eq!(output, (OptionalParamSegment("id"),));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_static_and_param() {
|
||||
let output = path!("/home/:id");
|
||||
@@ -144,9 +152,22 @@ fn parses_consecutive_param() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_consecutive_optional_param() {
|
||||
let output = path!("/:foo?/:bar?/:baz?");
|
||||
assert_eq!(
|
||||
output,
|
||||
(
|
||||
OptionalParamSegment("foo"),
|
||||
OptionalParamSegment("bar"),
|
||||
OptionalParamSegment("baz")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_complex() {
|
||||
let output = path!("/home/:id/foo/:bar/*any");
|
||||
let output = path!("/home/:id/foo/:bar/:baz?/*any");
|
||||
assert_eq!(
|
||||
output,
|
||||
(
|
||||
@@ -154,6 +175,7 @@ fn parses_complex() {
|
||||
ParamSegment("id"),
|
||||
StaticSegment("foo"),
|
||||
ParamSegment("bar"),
|
||||
OptionalParamSegment("baz"),
|
||||
WildcardSegment("any"),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -500,6 +500,17 @@ pub mod axum {
|
||||
.map(|item| (item.path(), item.method()))
|
||||
}
|
||||
|
||||
/// Removes any server functions with an included path from the map of
|
||||
/// registered server functions.
|
||||
///
|
||||
/// Calling this will mean that these server functions are not found unless you provide
|
||||
/// alternate handlers for them in your application.
|
||||
pub fn unregister_server_fns(paths: &[String]) {
|
||||
if !paths.is_empty() {
|
||||
REGISTERED_SERVER_FUNCTIONS.retain(|(p, _), _| !paths.contains(p));
|
||||
}
|
||||
}
|
||||
|
||||
/// An Axum handler that responds to a server function request.
|
||||
pub async fn handle_server_fn(req: Request<Body>) -> Response<Body> {
|
||||
let path = req.uri().path();
|
||||
@@ -588,6 +599,17 @@ pub mod actix {
|
||||
.map(|item| (item.path(), item.method()))
|
||||
}
|
||||
|
||||
/// Removes any server functions with an included path from the map of
|
||||
/// registered server functions.
|
||||
///
|
||||
/// Calling this will mean that these server functions are not found unless you provide
|
||||
/// alternate handlers for them in your application.
|
||||
pub fn unregister_server_fns(paths: &[String]) {
|
||||
if !paths.is_empty() {
|
||||
REGISTERED_SERVER_FUNCTIONS.retain(|(p, _), _| !paths.contains(p));
|
||||
}
|
||||
}
|
||||
|
||||
/// An Actix handler that responds to a server function request.
|
||||
pub async fn handle_server_fn(
|
||||
req: HttpRequest,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tachys"
|
||||
version = "0.1.0-gamma"
|
||||
version = "0.1.0-rc0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -17,6 +17,7 @@ either_of = { workspace = true }
|
||||
next_tuple = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
reactive_graph = { workspace = true, optional = true }
|
||||
reactive_stores = { workspace = true, optional = true }
|
||||
slotmap = { version = "1.0", optional = true }
|
||||
oco_ref = { workspace = true, optional = true }
|
||||
once_cell = "1.19"
|
||||
@@ -175,11 +176,10 @@ oco = ["dep:oco_ref"]
|
||||
nightly = ["reactive_graph/nightly"]
|
||||
testing = ["dep:slotmap"]
|
||||
reactive_graph = ["dep:reactive_graph", "dep:any_spawner"]
|
||||
reactive_stores = ["reactive_graph", "dep:reactive_stores"]
|
||||
sledgehammer = ["dep:sledgehammer_bindgen", "dep:sledgehammer_utils"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing", "sledgehammer"]
|
||||
skip_feature_sets = [
|
||||
["ssr", "hydrate"],
|
||||
]
|
||||
skip_feature_sets = [["ssr", "hydrate"], ["hydrate", "islands"], ["ssr", "delegation"]]
|
||||
|
||||
@@ -67,107 +67,121 @@ where
|
||||
|
||||
let value = Box::new(self) as Box<dyn Any + Send>;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html = |value: Box<dyn Any>,
|
||||
buf: &mut String,
|
||||
class: &mut String,
|
||||
style: &mut String,
|
||||
inner_html: &mut String| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyAttribute::to_html could not be downcast");
|
||||
value.to_html(buf, class, style, inner_html);
|
||||
};
|
||||
let build = |value: Box<dyn Any>,
|
||||
match value.downcast::<AnyAttribute>() {
|
||||
// if it's already an AnyAttribute, we don't need to double-wrap it
|
||||
Ok(any_attribute) => *any_attribute,
|
||||
Err(value) => {
|
||||
#[cfg(feature = "ssr")]
|
||||
let to_html =
|
||||
|value: Box<dyn Any>,
|
||||
buf: &mut String,
|
||||
class: &mut String,
|
||||
style: &mut String,
|
||||
inner_html: &mut String| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::to_html could not be downcast",
|
||||
);
|
||||
value.to_html(buf, class, style, inner_html);
|
||||
};
|
||||
let build =
|
||||
|value: Box<dyn Any>,
|
||||
el: &crate::renderer::types::Element| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyAttribute::build couldn't downcast");
|
||||
let state = Box::new(value.build(el));
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyAttribute::build couldn't downcast");
|
||||
let state = Box::new(value.build(el));
|
||||
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_server =
|
||||
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::hydrate_from_server couldn't downcast",
|
||||
);
|
||||
let state = Box::new(value.hydrate::<true>(el));
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_server =
|
||||
|value: Box<dyn Any>,
|
||||
el: &crate::renderer::types::Element| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::hydrate_from_server couldn't \
|
||||
downcast",
|
||||
);
|
||||
let state = Box::new(value.hydrate::<true>(el));
|
||||
|
||||
AnyAttributeState {
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_template =
|
||||
|value: Box<dyn Any>,
|
||||
el: &crate::renderer::types::Element| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::hydrate_from_server couldn't \
|
||||
downcast",
|
||||
);
|
||||
let state = Box::new(value.hydrate::<true>(el));
|
||||
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
let rebuild =
|
||||
|new_type_id: TypeId,
|
||||
value: Box<dyn Any>,
|
||||
state: &mut AnyAttributeState| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::rebuild couldn't downcast value",
|
||||
);
|
||||
if new_type_id == state.type_id {
|
||||
let state = state.state.downcast_mut().expect(
|
||||
"AnyAttribute::rebuild couldn't downcast state",
|
||||
);
|
||||
value.rebuild(state);
|
||||
} else {
|
||||
let new = value.into_any_attr().build(&state.el);
|
||||
*state = new;
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
|
||||
let value = value
|
||||
.downcast_mut::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
value.dry_resolve();
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let resolve = |value: Box<dyn Any>| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
Box::pin(
|
||||
async move { value.resolve().await.into_any_attr() },
|
||||
)
|
||||
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
|
||||
};
|
||||
AnyAttribute {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
html_len,
|
||||
value,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html,
|
||||
build,
|
||||
rebuild,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_server,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_template,
|
||||
#[cfg(feature = "ssr")]
|
||||
resolve,
|
||||
#[cfg(feature = "ssr")]
|
||||
dry_resolve,
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "hydrate")]
|
||||
let hydrate_from_template =
|
||||
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
|
||||
let value = value.downcast::<T>().expect(
|
||||
"AnyAttribute::hydrate_from_server couldn't downcast",
|
||||
);
|
||||
let state = Box::new(value.hydrate::<true>(el));
|
||||
|
||||
AnyAttributeState {
|
||||
type_id: TypeId::of::<T>(),
|
||||
state,
|
||||
el: el.clone(),
|
||||
}
|
||||
};
|
||||
let rebuild = |new_type_id: TypeId,
|
||||
value: Box<dyn Any>,
|
||||
state: &mut AnyAttributeState| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyAttribute::rebuild couldn't downcast value");
|
||||
if new_type_id == state.type_id {
|
||||
let state = state
|
||||
.state
|
||||
.downcast_mut()
|
||||
.expect("AnyAttribute::rebuild couldn't downcast state");
|
||||
value.rebuild(state);
|
||||
} else {
|
||||
let new = value.into_any_attr().build(&state.el);
|
||||
*state = new;
|
||||
}
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
|
||||
let value = value
|
||||
.downcast_mut::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
value.dry_resolve();
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let resolve = |value: Box<dyn Any>| {
|
||||
let value = value
|
||||
.downcast::<T>()
|
||||
.expect("AnyView::resolve could not be downcast");
|
||||
Box::pin(async move { value.resolve().await.into_any_attr() })
|
||||
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
|
||||
};
|
||||
AnyAttribute {
|
||||
type_id: TypeId::of::<T>(),
|
||||
html_len,
|
||||
value,
|
||||
#[cfg(feature = "ssr")]
|
||||
to_html,
|
||||
build,
|
||||
rebuild,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_server,
|
||||
#[cfg(feature = "hydrate")]
|
||||
hydrate_from_template,
|
||||
#[cfg(feature = "ssr")]
|
||||
resolve,
|
||||
#[cfg(feature = "ssr")]
|
||||
dry_resolve,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,84 @@ pub trait IntoClass: Send {
|
||||
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send;
|
||||
}
|
||||
|
||||
impl<T: IntoClass> IntoClass for Option<T> {
|
||||
type AsyncOutput = Option<T::AsyncOutput>;
|
||||
type State = (crate::renderer::types::Element, Option<T::State>);
|
||||
type Cloneable = Option<T::Cloneable>;
|
||||
type CloneableOwned = Option<T::CloneableOwned>;
|
||||
|
||||
fn html_len(&self) -> usize {
|
||||
self.as_ref().map_or(0, IntoClass::html_len)
|
||||
}
|
||||
|
||||
fn to_html(self, class: &mut String) {
|
||||
if let Some(t) = self {
|
||||
t.to_html(class);
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
) -> Self::State {
|
||||
if let Some(t) = self {
|
||||
(el.clone(), Some(t.hydrate::<FROM_SERVER>(el)))
|
||||
} else {
|
||||
(el.clone(), None)
|
||||
}
|
||||
}
|
||||
|
||||
fn build(self, el: &crate::renderer::types::Element) -> Self::State {
|
||||
if let Some(t) = self {
|
||||
(el.clone(), Some(t.build(el)))
|
||||
} else {
|
||||
(el.clone(), None)
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let el = &state.0;
|
||||
let prev_state = &mut state.1;
|
||||
let maybe_next_t_state = match (prev_state, self) {
|
||||
(Some(_prev_t_state), None) => {
|
||||
Rndr::remove_attribute(el, "class");
|
||||
Some(None)
|
||||
}
|
||||
(None, Some(t)) => Some(Some(t.build(el))),
|
||||
(Some(prev_t_state), Some(t)) => {
|
||||
t.rebuild(prev_t_state);
|
||||
None
|
||||
}
|
||||
(None, None) => Some(None),
|
||||
};
|
||||
if let Some(next_t_state) = maybe_next_t_state {
|
||||
state.1 = next_t_state;
|
||||
}
|
||||
}
|
||||
|
||||
fn into_cloneable(self) -> Self::Cloneable {
|
||||
self.map(|t| t.into_cloneable())
|
||||
}
|
||||
|
||||
fn into_cloneable_owned(self) -> Self::CloneableOwned {
|
||||
self.map(|t| t.into_cloneable_owned())
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
if let Some(t) = self {
|
||||
t.dry_resolve();
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
if let Some(t) = self {
|
||||
Some(t.resolve().await)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoClass for &'a str {
|
||||
type AsyncOutput = Self;
|
||||
type State = (crate::renderer::types::Element, Self);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user