mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-15 00:57:13 +00:00
Suspense/Transition components
This commit is contained in:
parent
c51e8f3569
commit
eacaaaec90
16 changed files with 300 additions and 468 deletions
|
@ -36,6 +36,7 @@ members = [
|
||||||
"router",
|
"router",
|
||||||
"routing",
|
"routing",
|
||||||
"is_server",
|
"is_server",
|
||||||
|
"routing_macro",
|
||||||
]
|
]
|
||||||
exclude = ["benchmarks", "examples"]
|
exclude = ["benchmarks", "examples"]
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,17 @@ codegen-units = 1
|
||||||
lto = true
|
lto = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = { path = "../../leptos", features = ["csr"] }
|
leptos = { path = "../../leptos", features = ["csr", "tracing"] }
|
||||||
reqwasm = "0.5"
|
reqwasm = "0.5"
|
||||||
|
gloo-timers = { version = "0.3", features = ["futures"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
console_log = "1"
|
console_log = "1"
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
tracing-subscriber-wasm = "0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3"
|
wasm-bindgen-test = "0.3"
|
||||||
|
|
|
@ -5,7 +5,7 @@ use leptos::{
|
||||||
computed::AsyncDerived,
|
computed::AsyncDerived,
|
||||||
signal::{signal, RwSignal},
|
signal::{signal, RwSignal},
|
||||||
},
|
},
|
||||||
view, IntoView,
|
view, IntoView, Transition,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -46,7 +46,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_example() -> impl IntoView {
|
pub fn fetch_example() -> impl IntoView {
|
||||||
let (cat_count, set_cat_count) = signal::<CatCount>(0);
|
let (cat_count, set_cat_count) = signal::<CatCount>(3);
|
||||||
|
|
||||||
// we use new_unsync here because the reqwasm request type isn't Send
|
// we use new_unsync here because the reqwasm request type isn't Send
|
||||||
// if we were doing SSR, then
|
// if we were doing SSR, then
|
||||||
|
@ -73,22 +73,6 @@ pub fn fetch_example() -> impl IntoView {
|
||||||
}
|
}
|
||||||
};*/
|
};*/
|
||||||
|
|
||||||
let cats_view = move || {
|
|
||||||
async move {
|
|
||||||
cats.await
|
|
||||||
.map(|cats| {
|
|
||||||
cats.into_iter()
|
|
||||||
.map(|s| view! { <p><img src={s}/></p> })
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.catch(|err| view! { <p class="error">{err.to_string()}</p> })
|
|
||||||
}
|
|
||||||
.suspend()
|
|
||||||
.transition()
|
|
||||||
.track()
|
|
||||||
.with_fallback(|| view! { <div>"Loading..."</div>})
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
|
@ -102,7 +86,17 @@ pub fn fetch_example() -> impl IntoView {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{cats_view}
|
<Transition fallback=|| view! { <div>"Loading..."</div> }>
|
||||||
|
{async move {
|
||||||
|
cats.await
|
||||||
|
.map(|cats| {
|
||||||
|
cats.into_iter()
|
||||||
|
.map(|s| view! { <p><img src={s}/></p> })
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}}
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,20 @@ use fetch::fetch_example;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
_ = console_log::init_with_level(log::Level::Debug);
|
use tracing_subscriber::fmt;
|
||||||
|
use tracing_subscriber_wasm::MakeConsoleWriter;
|
||||||
|
|
||||||
|
fmt()
|
||||||
|
.with_writer(
|
||||||
|
// To avoide trace events in the browser from showing their
|
||||||
|
// JS backtrace, which is very annoying, in my opinion
|
||||||
|
MakeConsoleWriter::default()
|
||||||
|
.map_trace_level_to(tracing::Level::DEBUG),
|
||||||
|
)
|
||||||
|
// For some reason, if we don't do this in the browser, we get
|
||||||
|
// a runtime error.
|
||||||
|
.without_time()
|
||||||
|
.init();
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
mount_to_body(fetch_example)
|
mount_to_body(fetch_example)
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,7 @@ where
|
||||||
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
|
/// 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>`.
|
/// to enable optional props in for example `<Show>` and `<Suspense>`.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ViewFn(Arc<dyn Fn() -> AnyView<Dom>>);
|
pub struct ViewFn(Arc<dyn Fn() -> AnyView<Dom> + Send + Sync + 'static>);
|
||||||
|
|
||||||
impl Default for ViewFn {
|
impl Default for ViewFn {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
@ -168,7 +168,7 @@ impl Default for ViewFn {
|
||||||
|
|
||||||
impl<F, C> From<F> for ViewFn
|
impl<F, C> From<F> for ViewFn
|
||||||
where
|
where
|
||||||
F: Fn() -> C + 'static,
|
F: Fn() -> C + Send + Sync + 'static,
|
||||||
C: RenderHtml<Dom> + 'static,
|
C: RenderHtml<Dom> + 'static,
|
||||||
{
|
{
|
||||||
fn from(value: F) -> Self {
|
fn from(value: F) -> Self {
|
||||||
|
|
|
@ -161,7 +161,9 @@ mod hydration_scripts;
|
||||||
#[cfg(feature = "nonce")]
|
#[cfg(feature = "nonce")]
|
||||||
pub mod nonce;
|
pub mod nonce;
|
||||||
mod show;
|
mod show;
|
||||||
|
mod suspense_component;
|
||||||
pub mod text_prop;
|
pub mod text_prop;
|
||||||
|
mod transition;
|
||||||
pub use for_loop::*;
|
pub use for_loop::*;
|
||||||
pub use hydration_scripts::*;
|
pub use hydration_scripts::*;
|
||||||
pub use leptos_macro::*;
|
pub use leptos_macro::*;
|
||||||
|
@ -171,6 +173,8 @@ pub use reactive_graph::{
|
||||||
};
|
};
|
||||||
pub use server_fn::{self, error};
|
pub use server_fn::{self, error};
|
||||||
pub use show::*;
|
pub use show::*;
|
||||||
|
pub use suspense_component::*;
|
||||||
|
pub use transition::*;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use typed_builder;
|
pub use typed_builder;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
@ -266,7 +270,7 @@ pub use serde;
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
pub use show::*;
|
pub use show::*;
|
||||||
//pub use suspense_component::*;
|
//pub use suspense_component::*;
|
||||||
//mod suspense_component;
|
mod suspense_component;
|
||||||
//mod transition;
|
//mod transition;
|
||||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|
|
@ -1,272 +1,54 @@
|
||||||
use leptos::ViewFn;
|
use crate::{children::ToChildren};
|
||||||
use leptos_dom::{DynChild, HydrationCtx, IntoView};
|
use tachys::prelude::FutureViewExt;
|
||||||
|
use crate::{children::ViewFn, IntoView};
|
||||||
use leptos_macro::component;
|
use leptos_macro::component;
|
||||||
#[allow(unused)]
|
use std::{future::Future, sync::Arc};
|
||||||
use leptos_reactive::SharedContext;
|
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
|
||||||
use leptos_reactive::SignalGet;
|
|
||||||
use leptos_reactive::{
|
|
||||||
create_memo, provide_context, SignalGetUntracked, SuspenseContext,
|
|
||||||
};
|
|
||||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
|
||||||
use leptos_reactive::{with_owner, Owner};
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
|
/// An async, typed equivalent to [`Children`], which takes a generic but preserves
|
||||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
/// type information to allow the compiler to optimize the view more effectively.
|
||||||
/// it will render the `children`.
|
pub struct AsyncChildren<T, F, Fut>(pub(crate) F)
|
||||||
///
|
where
|
||||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
F: Fn() -> Fut,
|
||||||
/// those resources are read under the suspense), so you cannot assume that resources have
|
Fut: Future<Output = T>;
|
||||||
/// `Some` value in `children`.
|
|
||||||
///
|
impl<T, F, Fut> AsyncChildren<T, F, Fut>
|
||||||
/// ```
|
where
|
||||||
/// # use leptos_reactive::*;
|
F: Fn() -> Fut,
|
||||||
/// # use leptos_macro::*;
|
Fut: Future<Output = T>,
|
||||||
/// # use leptos_dom::*; use leptos::*;
|
{
|
||||||
/// # if false {
|
pub fn into_inner(self) -> F {
|
||||||
/// # let runtime = create_runtime();
|
self.0
|
||||||
/// async fn fetch_cats(how_many: u32) -> Option<Vec<String>> { Some(vec![]) }
|
}
|
||||||
///
|
}
|
||||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(1);
|
|
||||||
///
|
impl<T, F, Fut> ToChildren<F> for AsyncChildren<T, F, Fut>
|
||||||
/// let cats = create_resource(move || cat_count.get(), |count| fetch_cats(count));
|
where
|
||||||
///
|
F: Fn() -> Fut,
|
||||||
/// view! {
|
Fut: Future<Output = T>,
|
||||||
/// <div>
|
{
|
||||||
/// <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
|
fn to_children(f: F) -> Self {
|
||||||
/// {move || {
|
AsyncChildren(f)
|
||||||
/// cats.get().map(|data| match data {
|
}
|
||||||
/// None => view! { <pre>"Error"</pre> }.into_view(),
|
}
|
||||||
/// Some(cats) => cats
|
|
||||||
/// .iter()
|
/// TODO docs!
|
||||||
/// .map(|src| {
|
|
||||||
/// view! {
|
|
||||||
/// <img src={src}/>
|
|
||||||
/// }
|
|
||||||
/// })
|
|
||||||
/// .collect_view(),
|
|
||||||
/// })
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// </Suspense>
|
|
||||||
/// </div>
|
|
||||||
/// };
|
|
||||||
/// # runtime.dispose();
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[cfg_attr(
|
|
||||||
any(debug_assertions, feature = "ssr"),
|
|
||||||
tracing::instrument(level = "trace", skip_all)
|
|
||||||
)]
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Suspense<V>(
|
pub fn Suspense<Chil, ChilFn, ChilFut>(
|
||||||
/// Returns a fallback UI that will be shown while `async` [`Resource`](leptos_reactive::Resource)s are still loading. By default this is the empty view.
|
#[prop(optional, into)] fallback: ViewFn,
|
||||||
#[prop(optional, into)]
|
children: AsyncChildren<Chil, ChilFn, ChilFut>,
|
||||||
fallback: ViewFn,
|
|
||||||
/// Children will be displayed once all `async` [`Resource`](leptos_reactive::Resource)s have resolved.
|
|
||||||
children: Rc<dyn Fn() -> V>,
|
|
||||||
) -> impl IntoView
|
) -> impl IntoView
|
||||||
where
|
where
|
||||||
V: IntoView + 'static,
|
Chil: IntoView + 'static,
|
||||||
|
ChilFn: Fn() -> ChilFut + Clone + 'static,
|
||||||
|
ChilFut: Future<Output = Chil> + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
#[cfg(all(
|
let children = Arc::new(children.into_inner());
|
||||||
feature = "experimental-islands",
|
// TODO check this against islands
|
||||||
not(any(feature = "csr", feature = "hydrate"))
|
|
||||||
))]
|
|
||||||
let no_hydrate = SharedContext::no_hydrate();
|
|
||||||
let orig_children = children;
|
|
||||||
let context = SuspenseContext::new();
|
|
||||||
|
|
||||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
|
||||||
let owner =
|
|
||||||
Owner::current().expect("<Suspense/> created with no reactive owner");
|
|
||||||
|
|
||||||
let current_id = HydrationCtx::next_component();
|
|
||||||
|
|
||||||
// provide this SuspenseContext to any resources below it
|
|
||||||
// run in a memo so the children are children of this parent
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
let children = create_memo({
|
|
||||||
let orig_children = Rc::clone(&orig_children);
|
|
||||||
move |_| {
|
|
||||||
provide_context(context);
|
|
||||||
orig_children().into_view()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
let children = create_memo({
|
|
||||||
let orig_children = Rc::clone(&orig_children);
|
|
||||||
move |_| {
|
|
||||||
provide_context(context);
|
|
||||||
if SharedContext::fragment_has_local_resources(
|
|
||||||
¤t_id.to_string(),
|
|
||||||
) {
|
|
||||||
HydrationCtx::with_hydration_off({
|
|
||||||
let orig_children = Rc::clone(&orig_children);
|
|
||||||
move || orig_children().into_view()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
orig_children().into_view()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// likewise for the fallback
|
|
||||||
let fallback = create_memo({
|
|
||||||
move |_| {
|
|
||||||
provide_context(context);
|
|
||||||
fallback.run()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
|
||||||
let ready = context.ready();
|
|
||||||
|
|
||||||
let child = DynChild::new({
|
|
||||||
move || {
|
move || {
|
||||||
// pull lazy memo before checking if context is ready
|
children()
|
||||||
let children_rendered = children.get_untracked();
|
.suspend()
|
||||||
|
.with_fallback(fallback.run())
|
||||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
.track()
|
||||||
{
|
|
||||||
if ready.get() {
|
|
||||||
children_rendered
|
|
||||||
} else {
|
|
||||||
fallback.get_untracked()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
|
||||||
{
|
|
||||||
use leptos_reactive::signal_prelude::*;
|
|
||||||
|
|
||||||
// run the child; we'll probably throw this away, but it will register resource reads
|
|
||||||
//let after_original_child = HydrationCtx::peek();
|
|
||||||
|
|
||||||
{
|
|
||||||
// no resources were read under this, so just return the child
|
|
||||||
if context.none_pending() {
|
|
||||||
with_owner(owner, move || {
|
|
||||||
//HydrationCtx::continue_from(current_id);
|
|
||||||
DynChild::new(move || children_rendered.clone())
|
|
||||||
.into_view()
|
|
||||||
})
|
|
||||||
} else if context.has_any_local() {
|
|
||||||
SharedContext::register_local_fragment(
|
|
||||||
current_id.to_string(),
|
|
||||||
);
|
|
||||||
fallback.get_untracked()
|
|
||||||
}
|
|
||||||
// show the fallback, but also prepare to stream HTML
|
|
||||||
else {
|
|
||||||
HydrationCtx::continue_from(current_id);
|
|
||||||
let runtime = leptos_reactive::current_runtime();
|
|
||||||
|
|
||||||
SharedContext::register_suspense(
|
|
||||||
context,
|
|
||||||
¤t_id.to_string(),
|
|
||||||
// out-of-order streaming
|
|
||||||
{
|
|
||||||
let orig_children = Rc::clone(&orig_children);
|
|
||||||
move || {
|
|
||||||
leptos_reactive::set_current_runtime(
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(feature = "experimental-islands")]
|
|
||||||
let prev_no_hydrate =
|
|
||||||
SharedContext::no_hydrate();
|
|
||||||
#[cfg(feature = "experimental-islands")]
|
|
||||||
{
|
|
||||||
SharedContext::set_no_hydrate(
|
|
||||||
no_hydrate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let rendered = with_owner(owner, {
|
|
||||||
move || {
|
|
||||||
HydrationCtx::continue_from(
|
|
||||||
current_id,
|
|
||||||
);
|
|
||||||
DynChild::new({
|
|
||||||
move || {
|
|
||||||
orig_children().into_view()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_view()
|
|
||||||
.render_to_string()
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(feature = "experimental-islands")]
|
|
||||||
SharedContext::set_no_hydrate(
|
|
||||||
prev_no_hydrate,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[allow(clippy::let_and_return)]
|
|
||||||
rendered
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// in-order streaming
|
|
||||||
{
|
|
||||||
let orig_children = Rc::clone(&orig_children);
|
|
||||||
move || {
|
|
||||||
leptos_reactive::set_current_runtime(
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(feature = "experimental-islands")]
|
|
||||||
let prev_no_hydrate =
|
|
||||||
SharedContext::no_hydrate();
|
|
||||||
#[cfg(feature = "experimental-islands")]
|
|
||||||
{
|
|
||||||
SharedContext::set_no_hydrate(
|
|
||||||
no_hydrate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let rendered = with_owner(owner, {
|
|
||||||
move || {
|
|
||||||
HydrationCtx::continue_from(
|
|
||||||
current_id,
|
|
||||||
);
|
|
||||||
DynChild::new({
|
|
||||||
move || {
|
|
||||||
orig_children().into_view()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_view()
|
|
||||||
.into_stream_chunks()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
#[cfg(feature = "experimental-islands")]
|
|
||||||
SharedContext::set_no_hydrate(
|
|
||||||
prev_no_hydrate,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[allow(clippy::let_and_return)]
|
|
||||||
rendered
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// return the fallback for now, wrapped in fragment identifier
|
|
||||||
fallback.get_untracked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_view();
|
|
||||||
let core_component = match child {
|
|
||||||
leptos_dom::View::CoreComponent(repr) => repr,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
HydrationCtx::continue_from(current_id);
|
|
||||||
HydrationCtx::next_component();
|
|
||||||
|
|
||||||
leptos_dom::View::Suspense(current_id, core_component)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,177 +1,25 @@
|
||||||
use leptos::ViewFn;
|
use crate::{children::ViewFn, AsyncChildren, IntoView};
|
||||||
use leptos_dom::{Fragment, HydrationCtx, IntoView, View};
|
|
||||||
use leptos_macro::component;
|
use leptos_macro::component;
|
||||||
use leptos_reactive::{
|
use std::{future::Future, sync::Arc};
|
||||||
create_isomorphic_effect, create_rw_signal, use_context, RwSignal,
|
use tachys::prelude::FutureViewExt;
|
||||||
SignalGet, SignalGetUntracked, SignalSet, SignalSetter, SuspenseContext,
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
cell::{Cell, RefCell},
|
|
||||||
rc::Rc,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// If any [`Resource`](leptos_reactive::Resource)s are read in the `children` of this
|
/// TODO docs!
|
||||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
#[component]
|
||||||
/// it will render the `children`. Unlike [`Suspense`](crate::Suspense), this will not fall
|
pub fn Transition<Chil, ChilFn, ChilFut>(
|
||||||
/// back to the `fallback` state if there are further changes after the initial load.
|
#[prop(optional, into)] fallback: ViewFn,
|
||||||
///
|
children: AsyncChildren<Chil, ChilFn, ChilFut>,
|
||||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
) -> impl IntoView
|
||||||
/// those resources are read under the suspense), so you cannot assume that resources have
|
where
|
||||||
/// `Some` value in `children`.
|
Chil: IntoView + 'static,
|
||||||
///
|
ChilFn: Fn() -> ChilFut + Clone + 'static,
|
||||||
/// ```
|
ChilFut: Future<Output = Chil> + Send + Sync + 'static,
|
||||||
/// # use leptos_reactive::*;
|
{
|
||||||
/// # use leptos_macro::*;
|
let children = children.into_inner();
|
||||||
/// # use leptos_dom::*;
|
|
||||||
/// # use leptos::*;
|
|
||||||
/// # if false {
|
|
||||||
/// # let runtime = create_runtime();
|
|
||||||
/// async fn fetch_cats(how_many: u32) -> Option<Vec<String>> {
|
|
||||||
/// Some(vec![])
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(1);
|
|
||||||
/// let (pending, set_pending) = create_signal(false);
|
|
||||||
///
|
|
||||||
/// let cats =
|
|
||||||
/// create_resource(move || cat_count.get(), |count| fetch_cats(count));
|
|
||||||
///
|
|
||||||
/// view! {
|
|
||||||
/// <div>
|
|
||||||
/// <Transition
|
|
||||||
/// fallback=move || view! { <p>"Loading..."</p>}
|
|
||||||
/// set_pending
|
|
||||||
/// >
|
|
||||||
/// {move || {
|
|
||||||
/// cats.read().map(|data| match data {
|
|
||||||
/// None => view! { <pre>"Error"</pre> }.into_view(),
|
|
||||||
/// Some(cats) => cats
|
|
||||||
/// .iter()
|
|
||||||
/// .map(|src| {
|
|
||||||
/// view! {
|
|
||||||
/// <img src={src}/>
|
|
||||||
/// }
|
|
||||||
/// })
|
|
||||||
/// .collect_view(),
|
|
||||||
/// })
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// </Transition>
|
|
||||||
/// </div>
|
|
||||||
/// };
|
|
||||||
/// # runtime.dispose();
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[cfg_attr(
|
|
||||||
any(debug_assertions, feature = "ssr"),
|
|
||||||
tracing::instrument(level = "trace", skip_all)
|
|
||||||
)]
|
|
||||||
#[component(transparent)]
|
|
||||||
pub fn Transition(
|
|
||||||
/// Will be displayed while resources are pending. By default this is the empty view.
|
|
||||||
#[prop(optional, into)]
|
|
||||||
fallback: ViewFn,
|
|
||||||
/// A function that will be called when the component transitions into or out of
|
|
||||||
/// the `pending` state, with its argument indicating whether it is pending (`true`)
|
|
||||||
/// or not pending (`false`).
|
|
||||||
#[prop(optional, into)]
|
|
||||||
set_pending: Option<SignalSetter<bool>>,
|
|
||||||
/// Will be displayed once all resources have resolved.
|
|
||||||
children: Box<dyn Fn() -> Fragment>,
|
|
||||||
) -> impl IntoView {
|
|
||||||
let prev_children = Rc::new(RefCell::new(None::<View>));
|
|
||||||
|
|
||||||
let first_run = create_rw_signal(true);
|
|
||||||
let child_runs = Cell::new(0);
|
|
||||||
let held_suspense_context = Rc::new(RefCell::new(None::<SuspenseContext>));
|
|
||||||
|
|
||||||
crate::Suspense(
|
|
||||||
crate::SuspenseProps::builder()
|
|
||||||
.fallback({
|
|
||||||
let prev_child = Rc::clone(&prev_children);
|
|
||||||
move || {
|
move || {
|
||||||
let suspense_context = use_context::<SuspenseContext>()
|
children()
|
||||||
.expect("there to be a SuspenseContext");
|
.suspend()
|
||||||
|
.transition()
|
||||||
let was_first_run =
|
.with_fallback(fallback.run())
|
||||||
cfg!(feature = "csr") && first_run.get();
|
.track()
|
||||||
let is_first_run =
|
|
||||||
is_first_run(first_run, &suspense_context);
|
|
||||||
if was_first_run {
|
|
||||||
first_run.set(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(prev_children) = &*prev_child.borrow() {
|
|
||||||
if is_first_run || was_first_run {
|
|
||||||
fallback.run()
|
|
||||||
} else {
|
|
||||||
prev_children.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fallback.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.children(Rc::new(move || {
|
|
||||||
let frag = children().into_view();
|
|
||||||
|
|
||||||
if let Some(suspense_context) = use_context::<SuspenseContext>()
|
|
||||||
{
|
|
||||||
*held_suspense_context.borrow_mut() =
|
|
||||||
Some(suspense_context);
|
|
||||||
}
|
|
||||||
let suspense_context = held_suspense_context.borrow().unwrap();
|
|
||||||
|
|
||||||
if cfg!(feature = "hydrate")
|
|
||||||
|| !first_run.get_untracked()
|
|
||||||
|| (cfg!(feature = "csr") && first_run.get())
|
|
||||||
{
|
|
||||||
*prev_children.borrow_mut() = Some(frag.clone());
|
|
||||||
}
|
|
||||||
if is_first_run(first_run, &suspense_context) {
|
|
||||||
let has_local_only = suspense_context.has_local_only()
|
|
||||||
|| cfg!(feature = "csr")
|
|
||||||
|| !HydrationCtx::is_hydrating();
|
|
||||||
if (!has_local_only || child_runs.get() > 0)
|
|
||||||
&& !cfg!(feature = "csr")
|
|
||||||
{
|
|
||||||
first_run.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
child_runs.set(child_runs.get() + 1);
|
|
||||||
|
|
||||||
create_isomorphic_effect(move |_| {
|
|
||||||
if let Some(set_pending) = set_pending {
|
|
||||||
set_pending.set(!suspense_context.none_pending())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
frag
|
|
||||||
}))
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_first_run(
|
|
||||||
first_run: RwSignal<bool>,
|
|
||||||
suspense_context: &SuspenseContext,
|
|
||||||
) -> bool {
|
|
||||||
if cfg!(feature = "csr")
|
|
||||||
|| (cfg!(feature = "hydrate") && !HydrationCtx::is_hydrating())
|
|
||||||
{
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
match (
|
|
||||||
first_run.get_untracked(),
|
|
||||||
cfg!(feature = "hydrate"),
|
|
||||||
suspense_context.has_local_only(),
|
|
||||||
) {
|
|
||||||
(false, _, _) => false,
|
|
||||||
// SSR and has non-local resources (so, has streamed)
|
|
||||||
(_, false, false) => false,
|
|
||||||
// SSR but with only local resources (so, has not streamed)
|
|
||||||
(_, false, true) => true,
|
|
||||||
// hydrate: it's the first run
|
|
||||||
(first_run, true, _) => HydrationCtx::is_hydrating() || first_run,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,23 @@ impl ReactiveNode for RwLock<ArcAsyncDerivedInner> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_if_necessary(&self) -> bool {
|
fn update_if_necessary(&self) -> bool {
|
||||||
// always return false, because the async work will not be ready yet
|
// if update_is_necessary is being called, that mean that a subscriber
|
||||||
// we'll mark subscribers dirty again when it resolves
|
// wants to know if our latest value has changed
|
||||||
false
|
//
|
||||||
|
// this could be the case either because
|
||||||
|
// 1) we have updated, and asynchronously woken the subscriber back up
|
||||||
|
// 2) a different source has woken up the subscriber, and it's now asking us
|
||||||
|
// if we've changed
|
||||||
|
//
|
||||||
|
// if we return `false` it will short-circuit that subscriber
|
||||||
|
// if we return `true` it means "yes, we may have changed"
|
||||||
|
//
|
||||||
|
// returning `true` here means that an AsyncDerived behaves like a signal (it always says
|
||||||
|
// "sure, I"ve changed) and not like a memo (checks whether it has *actually* changed)
|
||||||
|
//
|
||||||
|
// TODO is there a dirty-checking mechanism that would work here? we would need a
|
||||||
|
// memoization process like a memo has, to ensure we don't over-notify
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ edition = "2021"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
leptos = { workspace = true }
|
||||||
any_spawner = { workspace = true }
|
any_spawner = { workspace = true }
|
||||||
either_of = { workspace = true }
|
either_of = { workspace = true }
|
||||||
reactive_graph = { workspace = true }
|
reactive_graph = { workspace = true }
|
||||||
|
|
70
routing/src/components.rs
Normal file
70
routing/src/components.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::{
|
||||||
|
location::BrowserUrl, matching, router, FlatRouter, NestedRoute, RouteData,
|
||||||
|
Router, Routes,
|
||||||
|
};
|
||||||
|
use leptos::{children::ToChildren, component};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use tachys::renderer::dom::Dom;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RouteChildren<Children>(Children);
|
||||||
|
|
||||||
|
impl<Children> RouteChildren<Children> {
|
||||||
|
pub fn into_inner(self) -> Children {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, Children> ToChildren<F> for RouteChildren<Children>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Children,
|
||||||
|
{
|
||||||
|
fn to_children(f: F) -> Self {
|
||||||
|
RouteChildren(f())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn FlatRouter<Children, FallbackFn, Fallback>(
|
||||||
|
#[prop(optional, into)] base: Option<Cow<'static, str>>,
|
||||||
|
fallback: FallbackFn,
|
||||||
|
children: RouteChildren<Children>,
|
||||||
|
) -> FlatRouter<Dom, BrowserUrl, Children, FallbackFn>
|
||||||
|
where
|
||||||
|
FallbackFn: Fn() -> Fallback,
|
||||||
|
{
|
||||||
|
let children = Routes::new(children.into_inner());
|
||||||
|
if let Some(base) = base {
|
||||||
|
FlatRouter::new_with_base(base, children, fallback)
|
||||||
|
} else {
|
||||||
|
FlatRouter::new(children, fallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Router<Children, FallbackFn, Fallback>(
|
||||||
|
#[prop(optional, into)] base: Option<Cow<'static, str>>,
|
||||||
|
fallback: FallbackFn,
|
||||||
|
children: RouteChildren<Children>,
|
||||||
|
) -> Router<Dom, BrowserUrl, Children, FallbackFn>
|
||||||
|
where
|
||||||
|
FallbackFn: Fn() -> Fallback,
|
||||||
|
{
|
||||||
|
let children = Routes::new(children.into_inner());
|
||||||
|
if let Some(base) = base {
|
||||||
|
Router::new_with_base(base, children, fallback)
|
||||||
|
} else {
|
||||||
|
Router::new(children, fallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Route<Segments, View, ViewFn>(
|
||||||
|
path: Segments,
|
||||||
|
view: ViewFn,
|
||||||
|
) -> NestedRoute<Segments, (), (), ViewFn, Dom>
|
||||||
|
where
|
||||||
|
ViewFn: Fn(RouteData<Dom>) -> View,
|
||||||
|
{
|
||||||
|
NestedRoute::new(path, view)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
//mod reactive;
|
pub mod components;
|
||||||
mod generate_route_list;
|
mod generate_route_list;
|
||||||
pub mod location;
|
pub mod location;
|
||||||
mod matching;
|
mod matching;
|
||||||
|
@ -7,7 +7,7 @@ mod params;
|
||||||
mod router;
|
mod router;
|
||||||
mod ssr_mode;
|
mod ssr_mode;
|
||||||
mod static_route;
|
mod static_route;
|
||||||
//pub use reactive::*;
|
|
||||||
pub use generate_route_list::*;
|
pub use generate_route_list::*;
|
||||||
pub use matching::*;
|
pub use matching::*;
|
||||||
pub use method::*;
|
pub use method::*;
|
||||||
|
|
16
routing_macro/Cargo.toml
Normal file
16
routing_macro/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "routing_macro"
|
||||||
|
edition = "2021"
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro-error = { version = "1", default-features = false }
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
routing = { workspace = true }
|
76
routing_macro/src/lib.rs
Normal file
76
routing_macro/src/lib.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use proc_macro::{TokenStream, TokenTree};
|
||||||
|
use quote::{quote, ToTokens};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use syn::{
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
parse_macro_input,
|
||||||
|
token::Token,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[proc_macro_error::proc_macro_error]
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn path(tokens: TokenStream) -> TokenStream {
|
||||||
|
let mut parser = SegmentParser::new(tokens);
|
||||||
|
parser.parse_all();
|
||||||
|
let segments = Segments(parser.segments);
|
||||||
|
segments.into_token_stream().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
struct Segments(pub Vec<Segment>);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum Segment {
|
||||||
|
Static(Cow<'static, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SegmentParser {
|
||||||
|
input: proc_macro::token_stream::IntoIter,
|
||||||
|
current_str: Option<String>,
|
||||||
|
segments: Vec<Segment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SegmentParser {
|
||||||
|
pub fn new(input: TokenStream) -> Self {
|
||||||
|
Self {
|
||||||
|
input: input.into_iter(),
|
||||||
|
current_str: None,
|
||||||
|
segments: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SegmentParser {
|
||||||
|
pub fn parse_all(&mut self) {
|
||||||
|
for input in self.input.by_ref() {
|
||||||
|
match input {
|
||||||
|
TokenTree::Literal(lit) => {
|
||||||
|
Self::parse_str(
|
||||||
|
lit.to_string()
|
||||||
|
.trim_start_matches(['"', '/'])
|
||||||
|
.trim_end_matches(['"', '/']),
|
||||||
|
&mut self.segments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
TokenTree::Group(_) => todo!(),
|
||||||
|
TokenTree::Ident(_) => todo!(),
|
||||||
|
TokenTree::Punct(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_str(current_str: &str, segments: &mut Vec<Segment>) {
|
||||||
|
let mut chars = current_str.chars();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for Segments {
|
||||||
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
|
let children = quote! {};
|
||||||
|
if self.0.len() != 1 {
|
||||||
|
tokens.extend(quote! { (#children) });
|
||||||
|
} else {
|
||||||
|
tokens.extend(children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
routing_macro/tests/path.rs
Normal file
9
routing_macro/tests/path.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use routing::StaticSegment;
|
||||||
|
use routing_macro::path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_empty_list() {
|
||||||
|
let output = path!("");
|
||||||
|
assert_eq!(output, ());
|
||||||
|
//let segments: Segments = syn::parse(path.into()).unwrap();
|
||||||
|
}
|
|
@ -139,7 +139,7 @@ where
|
||||||
impl<const TRANSITION: bool, Fal, Fut, Rndr> RenderHtml<Rndr>
|
impl<const TRANSITION: bool, Fal, Fut, Rndr> RenderHtml<Rndr>
|
||||||
for Suspend<TRANSITION, Fal, Fut>
|
for Suspend<TRANSITION, Fal, Fut>
|
||||||
where
|
where
|
||||||
Fal: RenderHtml<Rndr> + Send + Sync + 'static,
|
Fal: RenderHtml<Rndr> + 'static,
|
||||||
Fut: Future + Send + Sync + 'static,
|
Fut: Future + Send + Sync + 'static,
|
||||||
Fut::Output: RenderHtml<Rndr>,
|
Fut::Output: RenderHtml<Rndr>,
|
||||||
Rndr: Renderer + 'static,
|
Rndr: Renderer + 'static,
|
||||||
|
|
Loading…
Reference in a new issue