mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
Suspense/Transition components
This commit is contained in:
parent
b8b77138ea
commit
88b93f40f9
16 changed files with 300 additions and 468 deletions
|
@ -36,6 +36,7 @@ members = [
|
|||
"router",
|
||||
"routing",
|
||||
"is_server",
|
||||
"routing_macro",
|
||||
]
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
|
|
|
@ -8,13 +8,17 @@ codegen-units = 1
|
|||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos = { path = "../../leptos", features = ["csr", "tracing"] }
|
||||
reqwasm = "0.5"
|
||||
gloo-timers = { version = "0.3", features = ["futures"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
thiserror = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
tracing-subscriber-wasm = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
|
|
@ -5,7 +5,7 @@ use leptos::{
|
|||
computed::AsyncDerived,
|
||||
signal::{signal, RwSignal},
|
||||
},
|
||||
view, IntoView,
|
||||
view, IntoView, Transition,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
@ -46,7 +46,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
|||
}
|
||||
|
||||
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
|
||||
// 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! {
|
||||
<div>
|
||||
<label>
|
||||
|
@ -102,7 +86,17 @@ pub fn fetch_example() -> impl IntoView {
|
|||
}
|
||||
/>
|
||||
</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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,20 @@ use fetch::fetch_example;
|
|||
use leptos::*;
|
||||
|
||||
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();
|
||||
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
|
||||
/// to enable optional props in for example `<Show>` and `<Suspense>`.
|
||||
#[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 {
|
||||
fn default() -> Self {
|
||||
|
@ -168,7 +168,7 @@ impl Default for ViewFn {
|
|||
|
||||
impl<F, C> From<F> for ViewFn
|
||||
where
|
||||
F: Fn() -> C + 'static,
|
||||
F: Fn() -> C + Send + Sync + 'static,
|
||||
C: RenderHtml<Dom> + 'static,
|
||||
{
|
||||
fn from(value: F) -> Self {
|
||||
|
|
|
@ -161,7 +161,9 @@ mod hydration_scripts;
|
|||
#[cfg(feature = "nonce")]
|
||||
pub mod nonce;
|
||||
mod show;
|
||||
mod suspense_component;
|
||||
pub mod text_prop;
|
||||
mod transition;
|
||||
pub use for_loop::*;
|
||||
pub use hydration_scripts::*;
|
||||
pub use leptos_macro::*;
|
||||
|
@ -171,6 +173,8 @@ pub use reactive_graph::{
|
|||
};
|
||||
pub use server_fn::{self, error};
|
||||
pub use show::*;
|
||||
pub use suspense_component::*;
|
||||
pub use transition::*;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder;
|
||||
#[doc(hidden)]
|
||||
|
@ -266,7 +270,7 @@ pub use serde;
|
|||
pub use serde_json;
|
||||
pub use show::*;
|
||||
//pub use suspense_component::*;
|
||||
//mod suspense_component;
|
||||
mod suspense_component;
|
||||
//mod transition;
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
#[doc(hidden)]
|
||||
|
|
|
@ -1,272 +1,54 @@
|
|||
use leptos::ViewFn;
|
||||
use leptos_dom::{DynChild, HydrationCtx, IntoView};
|
||||
use crate::{children::ToChildren};
|
||||
use tachys::prelude::FutureViewExt;
|
||||
use crate::{children::ViewFn, IntoView};
|
||||
use leptos_macro::component;
|
||||
#[allow(unused)]
|
||||
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;
|
||||
use std::{future::Future, sync::Arc};
|
||||
|
||||
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
/// it will render the `children`.
|
||||
///
|
||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
||||
/// those resources are read under the suspense), so you cannot assume that resources have
|
||||
/// `Some` value in `children`.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_macro::*;
|
||||
/// # 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 cats = create_resource(move || cat_count.get(), |count| fetch_cats(count));
|
||||
///
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
|
||||
/// {move || {
|
||||
/// cats.get().map(|data| match data {
|
||||
/// None => view! { <pre>"Error"</pre> }.into_view(),
|
||||
/// Some(cats) => cats
|
||||
/// .iter()
|
||||
/// .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)
|
||||
)]
|
||||
/// An async, typed equivalent to [`Children`], which takes a generic but preserves
|
||||
/// type information to allow the compiler to optimize the view more effectively.
|
||||
pub struct AsyncChildren<T, F, Fut>(pub(crate) F)
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = T>;
|
||||
|
||||
impl<T, F, Fut> AsyncChildren<T, F, Fut>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = T>,
|
||||
{
|
||||
pub fn into_inner(self) -> F {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F, Fut> ToChildren<F> for AsyncChildren<T, F, Fut>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = T>,
|
||||
{
|
||||
fn to_children(f: F) -> Self {
|
||||
AsyncChildren(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO docs!
|
||||
#[component]
|
||||
pub fn Suspense<V>(
|
||||
/// 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,
|
||||
/// Children will be displayed once all `async` [`Resource`](leptos_reactive::Resource)s have resolved.
|
||||
children: Rc<dyn Fn() -> V>,
|
||||
pub fn Suspense<Chil, ChilFn, ChilFut>(
|
||||
#[prop(optional, into)] fallback: ViewFn,
|
||||
children: AsyncChildren<Chil, ChilFn, ChilFut>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
V: IntoView + 'static,
|
||||
Chil: IntoView + 'static,
|
||||
ChilFn: Fn() -> ChilFut + Clone + 'static,
|
||||
ChilFut: Future<Output = Chil> + Send + Sync + 'static,
|
||||
{
|
||||
#[cfg(all(
|
||||
feature = "experimental-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 || {
|
||||
// pull lazy memo before checking if context is ready
|
||||
let children_rendered = children.get_untracked();
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
{
|
||||
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)
|
||||
let children = Arc::new(children.into_inner());
|
||||
// TODO check this against islands
|
||||
move || {
|
||||
children()
|
||||
.suspend()
|
||||
.with_fallback(fallback.run())
|
||||
.track()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,177 +1,25 @@
|
|||
use leptos::ViewFn;
|
||||
use leptos_dom::{Fragment, HydrationCtx, IntoView, View};
|
||||
use crate::{children::ViewFn, AsyncChildren, IntoView};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{
|
||||
create_isomorphic_effect, create_rw_signal, use_context, RwSignal,
|
||||
SignalGet, SignalGetUntracked, SignalSet, SignalSetter, SuspenseContext,
|
||||
};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{future::Future, sync::Arc};
|
||||
use tachys::prelude::FutureViewExt;
|
||||
|
||||
/// If any [`Resource`](leptos_reactive::Resource)s are read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
/// it will render the `children`. Unlike [`Suspense`](crate::Suspense), this will not fall
|
||||
/// back to the `fallback` state if there are further changes after the initial load.
|
||||
///
|
||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
||||
/// those resources are read under the suspense), so you cannot assume that resources have
|
||||
/// `Some` value in `children`.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_macro::*;
|
||||
/// # 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 || {
|
||||
let suspense_context = use_context::<SuspenseContext>()
|
||||
.expect("there to be a SuspenseContext");
|
||||
|
||||
let was_first_run =
|
||||
cfg!(feature = "csr") && first_run.get();
|
||||
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,
|
||||
}
|
||||
/// TODO docs!
|
||||
#[component]
|
||||
pub fn Transition<Chil, ChilFn, ChilFut>(
|
||||
#[prop(optional, into)] fallback: ViewFn,
|
||||
children: AsyncChildren<Chil, ChilFn, ChilFut>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
Chil: IntoView + 'static,
|
||||
ChilFn: Fn() -> ChilFut + Clone + 'static,
|
||||
ChilFut: Future<Output = Chil> + Send + Sync + 'static,
|
||||
{
|
||||
let children = children.into_inner();
|
||||
move || {
|
||||
children()
|
||||
.suspend()
|
||||
.transition()
|
||||
.with_fallback(fallback.run())
|
||||
.track()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,9 +37,23 @@ impl ReactiveNode for RwLock<ArcAsyncDerivedInner> {
|
|||
}
|
||||
|
||||
fn update_if_necessary(&self) -> bool {
|
||||
// always return false, because the async work will not be ready yet
|
||||
// we'll mark subscribers dirty again when it resolves
|
||||
false
|
||||
// if update_is_necessary is being called, that mean that a subscriber
|
||||
// wants to know if our latest value has changed
|
||||
//
|
||||
// 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
|
||||
|
||||
[dependencies]
|
||||
leptos = { workspace = true }
|
||||
any_spawner = { workspace = true }
|
||||
either_of = { 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;
|
||||
pub mod location;
|
||||
mod matching;
|
||||
|
@ -7,7 +7,7 @@ mod params;
|
|||
mod router;
|
||||
mod ssr_mode;
|
||||
mod static_route;
|
||||
//pub use reactive::*;
|
||||
|
||||
pub use generate_route_list::*;
|
||||
pub use matching::*;
|
||||
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>
|
||||
for Suspend<TRANSITION, Fal, Fut>
|
||||
where
|
||||
Fal: RenderHtml<Rndr> + Send + Sync + 'static,
|
||||
Fal: RenderHtml<Rndr> + 'static,
|
||||
Fut: Future + Send + Sync + 'static,
|
||||
Fut::Output: RenderHtml<Rndr>,
|
||||
Rndr: Renderer + 'static,
|
||||
|
|
Loading…
Reference in a new issue