Compare commits

..

2 Commits

Author SHA1 Message Date
Greg Johnston
faa73ff4db fmt 2024-10-18 16:21:02 -04:00
Penguinwithatie
7291efc077 add tachys::view::fragment::Fragment to prelude 2024-10-16 12:07:42 -06:00
30 changed files with 377 additions and 754 deletions

View File

@@ -21,16 +21,10 @@ 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,42 +50,30 @@ 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 >"
@@ -95,10 +83,14 @@ 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()
@@ -113,78 +105,54 @@ 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,21 +28,18 @@ 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">
@@ -51,7 +48,6 @@ pub fn Story() -> impl IntoView {
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
@@ -59,7 +55,7 @@ pub fn Story() -> impl IntoView {
key=|comment| comment.id
let:comment
>
<Comment comment/>
<Comment comment />
</For>
</ul>
</div>
@@ -68,7 +64,6 @@ pub fn Story() -> impl IntoView {
}
}
}))).build())
.into_any()
}
#[component]
@@ -77,65 +72,43 @@ 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,48 +18,30 @@ 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

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

@@ -50,7 +50,7 @@ pub fn RouterExample() -> impl IntoView {
}>{move || if logged_in.get() { "Log Out" } else { "Log In" }}</button>
</nav>
<main>
<Routes transition=true fallback=|| "This page could not be found.">
<Routes 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/>

View File

@@ -1,8 +1,3 @@
.routing-progress {
width: 100%;
height: 20px;
}
a[aria-current] {
font-weight: bold;
}
@@ -17,8 +12,12 @@ a[aria-current] {
padding: 1rem;
}
.contact {
view-transition-name: contact;
.fadeIn {
animation: 0.5s fadeIn forwards;
}
.fadeOut {
animation: 0.5s fadeOut forwards;
}
@keyframes fadeIn {
@@ -41,44 +40,12 @@ a[aria-current] {
}
}
.router-outlet-0 main {
view-transition-name: main;
.slideIn {
animation: 0.25s slideIn 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;
}
.slideOut {
animation: 0.25s slideOut forwards;
}
@keyframes slideIn {
@@ -99,6 +66,14 @@ a[aria-current] {
}
}
.slideInBack {
animation: 0.25s slideInBack forwards;
}
.slideOutBack {
animation: 0.25s slideOutBack forwards;
}
@keyframes slideInBack {
from {
transform: translate(-100vw, 0);

View File

@@ -1993,7 +1993,7 @@ where
move |uri: Uri, State(options): State<S>, req: Request<Body>| {
Box::pin(async move {
let options = LeptosOptions::from_ref(&options);
let res = get_static_file(uri, &options.site_root, req.headers());
let res = get_static_file(uri, &options.site_root);
let res = res.await.unwrap();
if res.status() == StatusCode::OK {
@@ -2027,26 +2027,14 @@ where
async fn get_static_file(
uri: Uri,
root: &str,
headers: &HeaderMap<HeaderValue>,
) -> Result<Response<Body>, (StatusCode, String)> {
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();
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root)
.precompressed_gzip()
.precompressed_br()
.oneshot(req)
.await
{
match ServeDir::new(root).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", "reactive_stores", "oco"] }
tachys = { workspace = true, features = ["reactive_graph", "oco"] }
thiserror = "1.0"
tracing = { version = "0.1.40", optional = true }
typed-builder = "0.19.1"

View File

@@ -926,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)]
#[proc_macro_derive(Params, attributes(params))]
pub fn params_derive(
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {

View File

@@ -1,6 +1,4 @@
use super::{
fragment_to_tokens, utils::is_nostrip_optional_and_update_key, TagType,
};
use super::{fragment_to_tokens, TagType};
use crate::view::{attribute_absolute, utils::filter_prefixed_attrs};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
@@ -46,10 +44,9 @@ pub(crate) fn component_to_tokens(
})
.unwrap_or_else(|| node.attributes().len());
// 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()
let attrs = node
.attributes()
.iter()
.filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
Some(node)
@@ -57,46 +54,39 @@ pub(crate) fn component_to_tokens(
None
}
})
.cloned()
.collect::<Vec<_>>();
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 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 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 });
if optional {
optional_props.push(quote! {
props.#name = { #value }.map(Into::into);
})
} else {
required_props.push(quote! {
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()
@@ -274,20 +264,14 @@ 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,
{
let mut props = ::leptos::component::component_props_builder(&#name #generics)
#(#required_props)*
#(#slots)*
#children
.build();
#(#optional_props)*
props
}
::leptos::component::component_props_builder(&#name #generics)
#(#props)*
#(#slots)*
#children
.build()
)
#spreads
}

View File

@@ -1,7 +1,7 @@
use proc_macro2::Ident;
use quote::format_ident;
use rstml::node::{KeyedAttribute, NodeName};
use syn::{spanned::Spanned, ExprPath};
use rstml::node::KeyedAttribute;
use syn::spanned::Spanned;
pub fn filter_prefixed_attrs<'a, A>(attrs: A, prefix: &str) -> Vec<Ident>
where
@@ -17,37 +17,3 @@ 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,7 +4,6 @@ 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,
@@ -12,7 +11,6 @@ fn Component(
impl_trait: impl Fn() -> i32 + 'static,
) -> impl IntoView {
_ = optional;
_ = optional_into;
_ = optional_no_strip;
_ = strip_option;
_ = default;
@@ -28,29 +26,9 @@ 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

@@ -76,38 +76,6 @@ 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)]
@@ -161,23 +129,22 @@ where
#[cfg(all(feature = "hydration", debug_assertions))]
{
use reactive_graph::{
computed::suspense::SuspenseContext, effect::in_effect_scope,
owner::use_context,
computed::suspense::SuspenseContext, owner::use_context,
};
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
{
let suspense = use_context::<SuspenseContext>();
if suspense.is_none() {
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(
"At {location}, you are reading a resource in `hydrate` \
mode outside a <Suspense/> or <Transition/> or effect. \
This can cause hydration mismatch errors and loses out \
on a significant performance optimization. To fix this \
issue, you can either: \n1. Wrap the place where you \
read the resource in a <Suspense/> or <Transition/> \
component, or \n2. Switch to using \
ArcLocalResource::new(), which will wait to load the \
resource until the app is hydrated on the client side. \
(This will have worse performance in most cases.)",
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.)",
));
}
}
@@ -674,23 +641,22 @@ where
#[cfg(all(feature = "hydration", debug_assertions))]
{
use reactive_graph::{
computed::suspense::SuspenseContext, effect::in_effect_scope,
owner::use_context,
computed::suspense::SuspenseContext, owner::use_context,
};
if !in_effect_scope() && use_context::<SuspenseContext>().is_none()
{
let suspense = use_context::<SuspenseContext>();
if suspense.is_none() {
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(
"At {location}, you are reading a resource in `hydrate` \
mode outside a <Suspense/> or <Transition/> or effect. \
This can cause hydration mismatch errors and loses out \
on a significant performance optimization. To fix this \
issue, you can either: \n1. Wrap the place where you \
read the resource in a <Suspense/> or <Transition/> \
component, or \n2. Switch to using LocalResource::new(), \
which will wait to load the resource until the app is \
hydrated on the client side. (This will have worse \
performance in most cases.)",
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.)",
));
}
}

View File

@@ -109,19 +109,6 @@ 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::{atomic::AtomicBool, Arc, RwLock},
sync::{Arc, RwLock},
};
/// Effects run a certain chunk of code whenever the signals they depend on change.
@@ -109,29 +109,6 @@ 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>,
@@ -180,9 +157,7 @@ impl Effect<LocalStorage> {
let old_value =
mem::take(&mut *value.write().or_poisoned());
let new_value = owner.with_cleanup(|| {
subscriber.with_observer(|| {
run_in_effect_scope(|| fun.run(old_value))
})
subscriber.with_observer(|| fun.run(old_value))
});
*value.write().or_poisoned() = Some(new_value);
}
@@ -400,9 +375,7 @@ impl Effect<SyncStorage> {
let old_value =
mem::take(&mut *value.write().or_poisoned());
let new_value = owner.with_cleanup(|| {
subscriber.with_observer(|| {
run_in_effect_scope(|| fun.run(old_value))
})
subscriber.with_observer(|| fun.run(old_value))
});
*value.write().or_poisoned() = Some(new_value);
}
@@ -446,9 +419,7 @@ impl Effect<SyncStorage> {
let old_value =
mem::take(&mut *value.write().or_poisoned());
let new_value = owner.with_cleanup(|| {
subscriber.with_observer(|| {
run_in_effect_scope(|| fun.run(old_value))
})
subscriber.with_observer(|| fun.run(old_value))
});
*value.write().or_poisoned() = Some(new_value);
}

View File

@@ -69,16 +69,16 @@ where
Chil: IntoView,
{
#[cfg(feature = "ssr")]
let (location_provider, current_url, redirect_hook) = {
let (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);
(None, current_url, Box::new(move |_: &str| {}))
(current_url, Box::new(move |_: &str| {}))
};
#[cfg(not(feature = "ssr"))]
let (location_provider, current_url, redirect_hook) = {
let (current_url, redirect_hook) = {
let location =
BrowserUrl::new().expect("could not access browser navigation"); // TODO options here
location.init(base.clone());
@@ -87,7 +87,7 @@ where
let redirect_hook = Box::new(|loc: &str| BrowserUrl::redirect(loc));
(Some(location), current_url, redirect_hook)
(current_url, redirect_hook)
};
// provide router context
let state = ArcRwSignal::new(State::new(None));
@@ -103,7 +103,6 @@ where
state,
set_is_routing,
query_mutations: Default::default(),
location_provider,
});
let children = children.into_inner();
@@ -119,7 +118,6 @@ pub(crate) struct RouterContext {
pub set_is_routing: Option<SignalSetter<bool>>,
pub query_mutations:
ArcStoredValue<Vec<(Oco<'static, str>, Option<String>)>>,
pub location_provider: Option<BrowserUrl>,
}
impl RouterContext {
@@ -181,14 +179,12 @@ impl RouterContext {
self.current_url.set(url);
}
if let Some(location_provider) = &self.location_provider {
location_provider.complete_navigation(&LocationChange {
value,
replace: options.replace,
scroll: options.scroll,
state: options.state,
});
}
BrowserUrl::complete_navigation(&LocationChange {
value,
replace: options.replace,
scroll: options.scroll,
state: options.state,
});
}
pub fn resolve_path<'a>(
@@ -232,9 +228,6 @@ where
#[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
@@ -274,7 +267,6 @@ where
base: base.clone(),
fallback: fallback.clone(),
set_is_routing,
transition,
}
}
}
@@ -282,9 +274,6 @@ where
#[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
@@ -328,7 +317,6 @@ where
fallback: fallback.clone(),
outer_owner: outer_owner.clone(),
set_is_routing,
transition,
}
}
}

View File

@@ -3,18 +3,17 @@ 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;
use either_of::{Either, EitherOf3};
use futures::FutureExt;
use reactive_graph::{
computed::{ArcMemo, ScopedFuture},
owner::{provide_context, Owner},
signal::ArcRwSignal,
traits::{GetUntracked, ReadUntracked, Set},
traits::{ReadUntracked, Set},
transition::AsyncTransition,
wrappers::write::SignalSetter,
};
@@ -24,9 +23,8 @@ use tachys::{
reactive_graph::OwnedView,
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
any_view::{AnyView, AnyViewState, IntoAny},
Mountable, Position, PositionState, Render, RenderHtml,
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml,
},
};
@@ -37,21 +35,28 @@ 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 {
pub struct FlatRoutesViewState<Defs, Fal>
where
Defs: MatchNestedRoutes + 'static,
Fal: Render + 'static,
{
#[allow(clippy::type_complexity)]
view: AnyViewState,
view: <EitherOf3<(), Fal, OwnedView<<Defs::Match as MatchInterface>::View>> as Render>::State,
id: Option<RouteMatchId>,
owner: Owner,
params: ArcRwSignal<ParamsMap>,
path: String,
url: ArcRwSignal<Url>,
matched: ArcRwSignal<String>,
matched: ArcRwSignal<String>
}
impl Mountable for FlatRoutesViewState {
impl<Defs, Fal> Mountable for FlatRoutesViewState<Defs, Fal>
where
Defs: MatchNestedRoutes + 'static,
Fal: Render + 'static,
{
fn unmount(&mut self) {
self.view.unmount();
}
@@ -74,9 +79,9 @@ where
Loc: LocationProvider,
Defs: MatchNestedRoutes + 'static,
FalFn: FnOnce() -> Fal + Send,
Fal: IntoAny,
Fal: Render + 'static,
{
type State = Rc<RefCell<FlatRoutesViewState>>;
type State = Rc<RefCell<FlatRoutesViewState<Defs, Fal>>>;
fn build(self) -> Self::State {
let FlatRoutesView {
@@ -116,7 +121,7 @@ where
match new_match {
None => Rc::new(RefCell::new(FlatRoutesViewState {
view: fallback().into_any().build(),
view: EitherOf3::B(fallback()).build(),
id,
owner,
params,
@@ -149,7 +154,7 @@ where
match view.as_mut().now_or_never() {
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
view: view.into_any().build(),
view: EitherOf3::C(view).build(),
id,
owner,
params,
@@ -160,7 +165,7 @@ where
None => {
let state =
Rc::new(RefCell::new(FlatRoutesViewState {
view: ().into_any().build(),
view: EitherOf3::A(()).build(),
id,
owner,
params,
@@ -173,7 +178,7 @@ where
let state = Rc::clone(&state);
async move {
let view = view.await;
view.into_any()
EitherOf3::C(view)
.rebuild(&mut state.borrow_mut().view);
}
});
@@ -193,7 +198,6 @@ where
fallback,
outer_owner,
set_is_routing,
transition,
} = self;
let url_snapshot = current_url.read_untracked();
@@ -263,7 +267,8 @@ where
provide_context(url);
provide_context(params_memo);
provide_context(Matched(ArcMemo::from(new_matched)));
fallback().into_any().rebuild(&mut state.borrow_mut().view)
EitherOf3::B(fallback())
.rebuild(&mut state.borrow_mut().view)
});
}
Some(new_match) => {
@@ -278,10 +283,6 @@ 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);
@@ -309,15 +310,8 @@ where
if current_url.read_untracked().path()
== spawned_path
{
let rebuild = move || {
view.into_any()
.rebuild(&mut state.borrow_mut().view);
};
if transition {
start_view_transition(0, is_back, rebuild);
} else {
rebuild();
}
EitherOf3::C(view)
.rebuild(&mut state.borrow_mut().view);
}
if let Some(location) = location {
@@ -363,7 +357,9 @@ where
FalFn: FnOnce() -> Fal + Send,
Fal: RenderHtml + 'static,
{
fn choose_ssr(self) -> OwnedView<AnyView> {
fn choose_ssr(
self,
) -> OwnedView<Either<Fal, <Defs::Match as MatchInterface>::View>> {
let current_url = self.current_url.read_untracked();
let new_match = self.routes.match_route(current_url.path());
let owner = self.outer_owner.child();
@@ -386,7 +382,7 @@ where
drop(current_url);
let view = match new_match {
None => (self.fallback)().into_any(),
None => Either::Left((self.fallback)()),
Some(new_match) => {
let (view, _) = new_match.into_view_and_child();
let view = owner
@@ -400,7 +396,7 @@ where
})
.now_or_never()
.expect("async route used in SSR");
view.into_any()
Either::Right(view)
}
};
@@ -417,7 +413,10 @@ where
{
type AsyncOutput = Self;
const MIN_LENGTH: usize = <Either<Fal, AnyView> as RenderHtml>::MIN_LENGTH;
const MIN_LENGTH: usize = <Either<
Fal,
<Defs::Match as MatchInterface>::View,
> as RenderHtml>::MIN_LENGTH;
fn dry_resolve(&mut self) {}
@@ -547,8 +546,7 @@ where
match new_match {
None => Rc::new(RefCell::new(FlatRoutesViewState {
view: fallback()
.into_any()
view: EitherOf3::B(fallback())
.hydrate::<FROM_SERVER>(cursor, position),
id,
owner,
@@ -582,8 +580,7 @@ where
match view.as_mut().now_or_never() {
Some(view) => Rc::new(RefCell::new(FlatRoutesViewState {
view: view
.into_any()
view: EitherOf3::C(view)
.hydrate::<FROM_SERVER>(cursor, position),
id,
owner,

View File

@@ -272,12 +272,37 @@ pub fn use_navigate() -> impl Fn(&str, NavigateOptions) + Clone {
move |path: &str, options: NavigateOptions| cx.navigate(path, options)
}
/// 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()
/*
/// 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()
}
*/
/* 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,71 +24,3 @@ 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,9 +22,7 @@ use web_sys::{Event, UrlSearchParams};
#[derive(Clone)]
pub struct BrowserUrl {
url: ArcRwSignal<Url>,
pub(crate) pending_navigation: Arc<Mutex<Option<oneshot::Sender<()>>>>,
pub(crate) path_stack: ArcStoredValue<Vec<Url>>,
pub(crate) is_back: ArcRwSignal<bool>,
pending_navigation: Arc<Mutex<Option<oneshot::Sender<()>>>>,
}
impl fmt::Debug for BrowserUrl {
@@ -61,14 +59,10 @@ impl LocationProvider for BrowserUrl {
fn new() -> Result<Self, JsValue> {
let url = ArcRwSignal::new(Self::current()?);
let path_stack = ArcStoredValue::new(
Self::current().map(|n| vec![n]).unwrap_or_default(),
);
let pending_navigation = Default::default();
Ok(Self {
url,
pending_navigation: Default::default(),
path_stack,
is_back: Default::default(),
pending_navigation,
})
}
@@ -120,7 +114,6 @@ 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();
@@ -130,7 +123,7 @@ impl LocationProvider for BrowserUrl {
url.set(new_url.clone());
if same_path {
this.complete_navigation(&loc);
Self::complete_navigation(&loc);
}
let pending = Arc::clone(&pending);
let (tx, rx) = oneshot::channel::<()>();
@@ -138,7 +131,6 @@ impl LocationProvider for BrowserUrl {
*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
@@ -149,7 +141,7 @@ impl LocationProvider for BrowserUrl {
// browser URL
let curr = url.read_untracked();
if curr == new_url {
this.complete_navigation(&loc);
Self::complete_navigation(&loc);
}
}
}
@@ -181,19 +173,8 @@ 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) => {
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);
}
Ok(new_url) => url.set(new_url),
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("{e:?}");
@@ -218,7 +199,7 @@ impl LocationProvider for BrowserUrl {
}
}
fn complete_navigation(&self, loc: &LocationChange) {
fn complete_navigation(loc: &LocationChange) {
let history = window().history().unwrap();
if loc.replace {
@@ -236,14 +217,6 @@ 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);
}
@@ -265,10 +238,6 @@ 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

@@ -191,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(&self, loc: &LocationChange);
fn complete_navigation(loc: &LocationChange);
fn parse(url: &str) -> Result<Url, Self::Error> {
Self::parse_with_base(url, BASE)
@@ -200,9 +200,6 @@ 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,12 +1,14 @@
use either_of::*;
use std::{future::Future, marker::PhantomData};
use tachys::view::any_view::{AnyView, IntoAny};
use tachys::view::{any_view::AnyView, Render};
pub trait ChooseView
where
Self: Send + Clone + 'static,
{
fn choose(self) -> impl Future<Output = AnyView>;
type Output;
fn choose(self) -> impl Future<Output = Self::Output>;
fn preload(&self) -> impl Future<Output = ()>;
}
@@ -14,10 +16,12 @@ where
impl<F, View> ChooseView for F
where
F: Fn() -> View + Send + Clone + 'static,
View: IntoAny,
View: Render + Send,
{
async fn choose(self) -> AnyView {
self().into_any()
type Output = View;
async fn choose(self) -> Self::Output {
self()
}
async fn preload(&self) {}
@@ -27,8 +31,10 @@ impl<T> ChooseView for Lazy<T>
where
T: LazyRoute,
{
async fn choose(self) -> AnyView {
T::data().view().await.into_any()
type Output = AnyView;
async fn choose(self) -> Self::Output {
T::data().view().await
}
async fn preload(&self) {
@@ -68,9 +74,9 @@ impl<T> Default for Lazy<T> {
}
impl ChooseView for () {
async fn choose(self) -> AnyView {
().into_any()
}
type Output = ();
async fn choose(self) -> Self::Output {}
async fn preload(&self) {}
}
@@ -80,10 +86,12 @@ where
A: ChooseView,
B: ChooseView,
{
async fn choose(self) -> AnyView {
type Output = Either<A::Output, B::Output>;
async fn choose(self) -> Self::Output {
match self {
Either::Left(f) => f.choose().await.into_any(),
Either::Right(f) => f.choose().await.into_any(),
Either::Left(f) => Either::Left(f.choose().await),
Either::Right(f) => Either::Right(f.choose().await),
}
}
@@ -101,9 +109,11 @@ macro_rules! tuples {
where
$($ty: ChooseView,)*
{
async fn choose(self ) -> AnyView {
type Output = $either<$($ty::Output,)*>;
async fn choose(self ) -> Self::Output {
match self {
$($either::$ty(f) => f.choose().await.into_any(),)*
$($either::$ty(f) => $either::$ty(f.choose().await),)*
}
}

View File

@@ -10,6 +10,7 @@ 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)]
@@ -94,12 +95,15 @@ 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, Option<Self::Child>);
fn into_view_and_child(
self,
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>);
}
pub trait MatchParams {
@@ -110,12 +114,13 @@ pub trait MatchParams {
pub trait MatchNestedRoutes {
type Data;
type View;
type Match: MatchInterface + MatchParams;
fn match_nested<'a>(
&'a self,
path: &'a str,
) -> (Option<(RouteMatchId, Self::Match)>, &'a str);
) -> (Option<(RouteMatchId, Self::Match)>, &str);
fn generate_routes(
&self,

View File

@@ -10,6 +10,7 @@ use std::{
collections::HashSet,
sync::atomic::{AtomicU16, Ordering},
};
use tachys::view::{Render, RenderHtml};
mod tuples;
@@ -140,8 +141,10 @@ impl<ParamsIter, Child, View> MatchInterface
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
@@ -151,7 +154,9 @@ where
&self.matched
}
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
fn into_view_and_child(
self,
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>) {
(self.view_fn, self.child)
}
}
@@ -168,8 +173,10 @@ where
Children: 'static,
<Children::Match as MatchParams>::Params: Clone,
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::<

View File

@@ -14,6 +14,7 @@ impl MatchParams for () {
impl MatchInterface for () {
type Child = ();
type View = ();
fn as_id(&self) -> RouteMatchId {
RouteMatchId(0)
@@ -23,13 +24,16 @@ impl MatchInterface for () {
""
}
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
fn into_view_and_child(
self,
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>) {
((), None)
}
}
impl MatchNestedRoutes for () {
type Data = ();
type View = ();
type Match = ();
fn match_nested<'a>(
@@ -65,6 +69,7 @@ where
A: MatchInterface + 'static,
{
type Child = A::Child;
type View = A::View;
fn as_id(&self) -> RouteMatchId {
self.0.as_id()
@@ -74,7 +79,9 @@ where
self.0.as_matched()
}
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
fn into_view_and_child(
self,
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>) {
self.0.into_view_and_child()
}
}
@@ -84,6 +91,7 @@ where
A: MatchNestedRoutes + 'static,
{
type Data = A::Data;
type View = A::View;
type Match = A::Match;
fn match_nested<'a>(
@@ -124,6 +132,7 @@ where
B: MatchInterface,
{
type Child = Either<A::Child, B::Child>;
type View = Either<A::View, B::View>;
fn as_id(&self) -> RouteMatchId {
match self {
@@ -139,7 +148,9 @@ where
}
}
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
fn into_view_and_child(
self,
) -> (impl ChooseView<Output = Self::View>, Option<Self::Child>) {
match self {
Either::Left(i) => {
let (view, child) = i.into_view_and_child();
@@ -159,6 +170,7 @@ 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>(
@@ -224,6 +236,7 @@ macro_rules! tuples {
$($ty: MatchInterface + 'static),*,
{
type Child = $either<$($ty::Child,)*>;
type View = $either<$($ty::View,)*>;
fn as_id(&self) -> RouteMatchId {
match self {
@@ -240,7 +253,7 @@ macro_rules! tuples {
fn into_view_and_child(
self,
) -> (
impl ChooseView,
impl ChooseView<Output = Self::View>,
Option<Self::Child>,
) {
match self {
@@ -257,6 +270,7 @@ 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

@@ -3,7 +3,6 @@ use crate::{
location::{LocationProvider, Url},
matching::Routes,
params::ParamsMap,
view_transition::start_view_transition,
ChooseView, MatchInterface, MatchNestedRoutes, MatchParams, PathSegment,
RouteList, RouteListing, RouteMatchId,
};
@@ -50,7 +49,6 @@ pub(crate) struct NestedRoutesView<Loc, Defs, FalFn> {
pub base: Option<Oco<'static, str>>,
pub fallback: FalFn,
pub set_is_routing: Option<SignalSetter<bool>>,
pub transition: bool,
}
pub struct NestedRouteViewState<Fal>
@@ -163,7 +161,7 @@ where
let mut preloaders = Vec::new();
let mut full_loaders = Vec::new();
let different_level = route.rebuild_nested_route(
route.rebuild_nested_route(
&self.current_url.read_untracked(),
self.base,
&mut 0,
@@ -172,26 +170,14 @@ where
&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(preloaders).await;
// tell each one of the outlet triggers that it's ready
let notify = move || {
for trigger in triggers {
trigger.notify();
}
};
if self.transition {
start_view_transition(different_level, is_back, notify);
} else {
notify();
for trigger in triggers {
trigger.notify();
}
});
@@ -523,8 +509,7 @@ trait AddNestedRoute {
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
level: u8,
) -> u8;
);
}
impl<Match> AddNestedRoute for Match
@@ -670,8 +655,7 @@ where
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
level: u8,
) -> u8 {
) {
let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets
.iter()
.take(*items)
@@ -682,7 +666,6 @@ where
// if there's nothing currently in the routes at this point, build from here
None => {
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
@@ -819,7 +802,7 @@ where
);
}
return level;
return;
}
// otherwise, set the params and URL signals,
@@ -839,10 +822,7 @@ where
outlets,
&owner,
set_is_routing,
level + 1,
)
} else {
level
);
}
}
}

View File

@@ -353,7 +353,7 @@ impl ResolvedStaticPath {
eprintln!("{e}");
}
}
owner.unset();
drop(owner);
}
}
});

View File

@@ -17,7 +17,6 @@ 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"
@@ -176,10 +175,9 @@ 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"], ["hydrate", "islands"], ["ssr", "delegation"]]
skip_feature_sets = [["ssr", "hydrate"]]

View File

@@ -14,8 +14,6 @@ use reactive_graph::{
traits::{Get, Update},
wrappers::read::Signal,
};
#[cfg(feature = "reactive_stores")]
use reactive_stores::{KeyedSubfield, Subfield};
use send_wrapper::SendWrapper;
use wasm_bindgen::JsValue;
@@ -344,35 +342,6 @@ where
}
}
#[cfg(feature = "reactive_stores")]
impl<Inner, Prev, T> IntoSplitSignal for Subfield<Inner, Prev, T>
where
Self: Get<Value = T> + Update<Value = T> + Clone,
{
type Value = T;
type Read = Self;
type Write = Self;
fn into_split_signal(self) -> (Self::Read, Self::Write) {
(self.clone(), self.clone())
}
}
#[cfg(feature = "reactive_stores")]
impl<Inner, Prev, K, T> IntoSplitSignal for KeyedSubfield<Inner, Prev, K, T>
where
Self: Get<Value = T> + Update<Value = T> + Clone,
for<'a> &'a T: IntoIterator,
{
type Value = T;
type Read = Self;
type Write = Self;
fn into_split_signal(self) -> (Self::Read, Self::Write) {
(self.clone(), self.clone())
}
}
/// Returns self from an event target.
pub trait FromEventTarget {
/// Returns self from an event target.