Compare commits

...

47 Commits

Author SHA1 Message Date
Greg Johnston
7b8cd90a6e 0.7.0-rc0 2024-10-21 21:16:20 -04:00
Greg Johnston
d0ef7b904d feat: add OptionalParamSegment (closes #2896) (#3140) 2024-10-21 21:15:14 -04:00
Greg Johnston
7904e0c395 fix: unregister server functions whose paths are in excluded routes (closes #2735) (#3138) 2024-10-21 09:14:36 -04:00
Greg Johnston
7b4c470155 perf: type erasure in router (#3107) 2024-10-20 20:07:14 -04:00
Greg Johnston
98eccc9eb8 perf: make LeptosOptions lighter-weight to clone (closes #3036) (#3136) 2024-10-20 20:05:29 -04:00
Greg Johnston
70d06e2716 feat: Action::clear() to clear action value (closes #2364) (#3135) 2024-10-20 16:29:05 -04:00
PenguinWithATie
67c3bf2478 chore: add tachys::view::fragment::Fragment to prelude (#3125) 2024-10-20 14:15:15 -04:00
Corvus
f3aaae857a feat: allow axum to serve precompressed files (#3133) 2024-10-19 20:47:35 -04:00
Greg Johnston
d727e53dd6 chore(ci): reduce tachys feature set combinations (#3131) 2024-10-19 20:45:49 -04:00
zakstucke
e4543ab5df feat: new nostrip: prop prefix to pass Option<T> directly when prop(optional) (#3105) 2024-10-19 15:41:51 -04:00
Greg Johnston
1ca0f4430c feat: use View Transition API for router animations (#3112) 2024-10-19 15:41:20 -04:00
Joaquim Pedro França Simão
b59fa11853 feat: add two-way data binding support for stores (#3115) 2024-10-19 15:39:45 -04:00
Greg Johnston
e55f08e017 feat: expose use_matched() (closes #3124) (#3126) 2024-10-18 16:12:41 -04:00
zakstucke
fa1939e5b2 chore: From<ArcResource> for ArcResource and AsyncDerived (#3121) 2024-10-18 16:12:11 -04:00
zakstucke
8b2f0eaf44 fix: do not warn when reading resources in effect outside Suspense (#3118) 2024-10-18 15:24:09 -04:00
Chris
b118d69281 fix: remove unused Params attribute params (#3123)
See 1966 for original PR on older version
2024-10-18 15:20:45 -04:00
stefnotch
ee66f6c395 Add support for user-supplied executors (#3091) 2024-10-16 06:24:07 -07:00
Greg Johnston
eba08ad592 fix: don't render empty string as a space in unescaped elements (closes #3120) (#3122) 2024-10-15 18:57:09 -04:00
Greg Johnston
4833b4e287 fix: avoid double-polling synchronously-available Suspend (closes #3113) (#3114) 2024-10-15 08:49:40 -04:00
Greg Johnston
9d1be64e4d chore: publish stores (#3110) 2024-10-14 10:18:38 -04:00
benwis
d6e6cd3be0 v0.7.0gamma3 2024-10-14 05:01:19 -07:00
stefnotch
70476f9277 feat: add support for async-executor from smol-rs (#3090) 2024-10-14 07:57:19 -04:00
zakstucke
d8ddfc26e9 perf: use the Track trait for the Signal wrapper. (#3076) 2024-10-12 20:29:03 -04:00
stefnotch
c8acc3e8bd fix: correctly support local pools for futures-executor (#3089) 2024-10-12 20:13:50 -04:00
zakstucke
547442243b impl IntoClass for Option<impl IntoClass> (#3104) 2024-10-12 05:03:53 -07:00
Greg Johnston
6e58266f54 feat: support set_is_routing/RoutingProgress for nested routes (#3101) 2024-10-11 19:05:33 -04:00
Greg Johnston
f0cd0fb41d feat: condense Router/Routes base prop into one (#3100) 2024-10-11 14:06:11 -04:00
Daniil Polyakov
7585faf57e fix: use full path to Result in Params derive (#3096) 2024-10-10 15:20:38 -04:00
zakstucke
da7f6a34e8 chore: expose AnyView in prelude (#3099) 2024-10-10 15:20:24 -04:00
Greg Johnston
4f7fa41262 fix: don't on WASM server targets unless you actually try to generate static routes (closes #3094) (#3097) 2024-10-10 15:20:04 -04:00
Greg Johnston
4becfa39ca correct version number 2024-10-10 09:13:39 -04:00
Greg Johnston
f8388b122d fix: avoid reentering lock when initializing nested keyed store fields (closes #3086) (#3087) 2024-10-10 08:53:28 -04:00
Greg Johnston
f57a57b92b feat: restore AnimatedShow for 0.7 (#3084) 2024-10-10 08:53:05 -04:00
vsuryamurthy
f0bcbd9cfe remove unused dependencies leptos_axum and leptos_router (#2960)
* remove unused dependencies leptos_axum and leptos_router

* cargo fmt

* Restore http::Uri under default feature

* use axum re-exported headers instead of http directly

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2024-10-10 04:29:11 -07:00
Greg Johnston
115477ef1d chore: remove unused code from leptos package (#3085) 2024-10-10 04:23:37 -07:00
Greg Johnston
832b9cb321 chore: pin wasm-bindgen to 0.2.93 to fix example builds (#3088) 2024-10-09 22:56:05 -04:00
Greg Johnston
b0150ceeec fix: missing Copy/Clone implementations for OnceResource (#3080) 2024-10-09 19:33:33 -04:00
dependabot[bot]
af8df34360 chore(deps): bump denoland/setup-deno from 1 to 2 (#3081)
Bumps [denoland/setup-deno](https://github.com/denoland/setup-deno) from 1 to 2.
- [Release notes](https://github.com/denoland/setup-deno/releases)
- [Commits](https://github.com/denoland/setup-deno/compare/v1...v2)

---
updated-dependencies:
- dependency-name: denoland/setup-deno
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 18:50:31 -04:00
Greg Johnston
b2e6185b22 fix: don't escape script/style/textarea in InertHtml (closes #3078) (#3079) 2024-10-09 10:07:40 -04:00
Greg Johnston
d2bfb3080b Merge pull request #3077 from leptos-rs/router-fixes
Router fixes
2024-10-09 07:33:24 -04:00
Greg Johnston
72ebd17042 fix: only set browser URL if it matches current router URL (closes #2979( 2024-10-08 22:12:18 -04:00
Greg Johnston
e2f0b4deeb fix: prevent simultaneous \query_signal\ writes from canceling each other (closes #2369) 2024-10-08 22:12:02 -04:00
Greg Johnston
57c07e9aec feat: enable faster compile times with RUSTFLAGS="--cfg erase_components (#2905) 2024-10-08 17:03:40 -04:00
Greg Johnston
0835066bc0 chore: re-add regression tests from #2639 (#3073) 2024-10-08 17:02:18 -04:00
webmstk
656e83fe24 docs: fix comment for set_interval helper (#3074) 2024-10-08 13:30:17 -04:00
Greg Johnston
ad0252ecfd fix: inconsistencies in check for latest version in actions (#3070) 2024-10-07 21:02:02 -04:00
Greg Johnston
77f05c6f4e fix: add HEAD support for Actix in leptos_routes (closes #2885) (#3069) 2024-10-07 21:01:46 -04:00
108 changed files with 2801 additions and 1797 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()

View File

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

View File

@@ -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)?

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)?

View File

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

View File

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

View File

@@ -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)?

View File

@@ -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)?

View File

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

View File

@@ -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!()
},
)
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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::*;

View File

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

View File

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

View File

@@ -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()

View File

@@ -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()

View File

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

View File

@@ -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)'] }

View File

@@ -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 youd 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 youd \
like to return (), you can do it it explicitly by \
returning () as the last item from the component."
);
}
}
//body.sig.ident = format_ident!("__{}", body.sig.ident);
@@ -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;

View File

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

View File

@@ -32,7 +32,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let gen = quote! {
impl Params for #name {
fn from_map(map: &::leptos_router::params::ParamsMap) -> Result<Self, ::leptos_router::params::ParamsError> {
fn from_map(map: &::leptos_router::params::ParamsMap) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
Ok(Self {
#(#fields,)*
})

View File

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

View File

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

View File

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

View File

@@ -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
/>
};
}

View File

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

View File

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

View File

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

View File

@@ -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.)",
));
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ impl<T: Serialize + 'static, St: Storage<T>> Serialize for ArcMemo<T, St> {
impl<T, St> Serialize for MaybeSignal<T, St>
where
T: Send + Sync + Serialize,
T: Clone + Send + Sync + Serialize,
St: Storage<SignalTypes<T, St>> + Storage<T>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>

View File

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

View File

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

View File

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

View File

@@ -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.
}

View File

@@ -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
View File

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

View File

@@ -10,6 +10,7 @@ use reactive_graph::{
Write,
},
};
pub use reactive_stores_macro::*;
use rustc_hash::FxHashMap;
use std::{
any::Any,
@@ -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,

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.7.0-gamma"
version = "0.7.0-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"

View File

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

View File

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

View File

@@ -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
}
}
}
*/

View File

@@ -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();
}
}
}
}

View File

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

View File

@@ -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)]

View File

@@ -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(),)*
}
}

View File

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

View File

@@ -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()));
}
}

View File

@@ -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());
}
}

View File

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

View File

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

View File

@@ -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
}
}
}))
},
))
}
}
}

View File

@@ -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) {

View File

@@ -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())]
]
);
}
}

View File

@@ -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>>;
}

View File

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

View File

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

View File

@@ -247,6 +247,7 @@ impl StaticPath {
}
paths = new_paths;
}
OptionalParam(_) => todo!(),
}
}
paths
@@ -353,7 +354,7 @@ impl ResolvedStaticPath {
eprintln!("{e}");
}
}
drop(owner);
owner.unset();
}
}
});

View File

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

View File

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

View File

@@ -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"),
)
);

View File

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

View File

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

View File

@@ -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,
}
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More