mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
<Transition/>
component
This commit is contained in:
parent
644d097cb6
commit
a2c5855362
6 changed files with 203 additions and 7 deletions
|
@ -11,7 +11,7 @@ serde = { version = "1", features = ["derive"] }
|
|||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
gloo-timers = { version = "0.2", features = ["futures"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -9,6 +10,10 @@ pub struct Cat {
|
|||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
// artificial delay
|
||||
// the cat API is too fast to show the transition
|
||||
TimeoutFuture::new(500).await;
|
||||
|
||||
if count > 0 {
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={}",
|
||||
|
@ -32,8 +37,9 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
|||
pub fn fetch_example(cx: Scope) -> web_sys::Element {
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
view! { cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<label>
|
||||
"How many cats would you like?"
|
||||
|
@ -45,16 +51,22 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
|
|||
}
|
||||
/>
|
||||
</label>
|
||||
{move || pending().then(|| view! { cx, <p>"Loading more cats..."</p> })}
|
||||
<div>
|
||||
<Suspense fallback={"Loading (Suspense Fallback)...".to_string()}>
|
||||
// <Transition/> holds the previous value while new async data is being loaded
|
||||
// Switch the <Transition/> to <Suspense/> to fall back to "Loading..." every time
|
||||
<Transition
|
||||
fallback={"Loading (Suspense Fallback)...".to_string()}
|
||||
on_pending=set_pending
|
||||
>
|
||||
{move || {
|
||||
cats.read().map(|data| match data {
|
||||
Err(_) => view! { cx, <pre>"Error"</pre> },
|
||||
Ok(cats) => view! { cx,
|
||||
Ok(cats) => view! { cx,
|
||||
<div>{
|
||||
cats.iter()
|
||||
.map(|src| {
|
||||
view! { cx,
|
||||
view! { cx,
|
||||
<img src={src}/>
|
||||
}
|
||||
})
|
||||
|
@ -64,7 +76,7 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
|
|||
})
|
||||
}
|
||||
}
|
||||
</Suspense>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.
|
|||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.19" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }
|
||||
log = "0.4"
|
||||
typed-builder = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos", default-features = false, version = "0.0" }
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
mod for_component;
|
||||
mod map;
|
||||
mod suspense;
|
||||
mod transition;
|
||||
|
||||
pub use for_component::*;
|
||||
pub use map::*;
|
||||
pub use suspense::*;
|
||||
pub use transition::*;
|
||||
|
||||
/// Describes the properties of a component. This is typically generated by the `Prop` derive macro
|
||||
/// as part of the `#[component]` macro.
|
||||
|
|
|
@ -20,7 +20,10 @@ where
|
|||
|
||||
/// 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`.
|
||||
/// it will render the `children`. If data begin loading again, falls back to `fallback` again.
|
||||
///
|
||||
/// If you’d rather continue displaying the previous `children` while loading new data, see
|
||||
/// [`Transition`](crate::Transition).
|
||||
///
|
||||
/// 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
|
||||
|
|
178
leptos_core/src/transition.rs
Normal file
178
leptos_core/src/transition.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
use leptos_dom::{Child, IntoChild};
|
||||
use leptos_reactive::{provide_context, Scope, SuspenseContext};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// Props for the [Suspense](crate::Suspense) component, which shows a fallback
|
||||
/// while [Resource](leptos_reactive::Resource)s are being read.
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct TransitionProps<F, E, G>
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E,
|
||||
{
|
||||
/// Will be displayed while resources are pending.
|
||||
pub fallback: F,
|
||||
/// 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`).
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
pub on_pending: Option<Box<dyn Fn(bool)>>,
|
||||
/// Will be displayed once all resources have resolved.
|
||||
pub children: Box<dyn Fn() -> Vec<G>>,
|
||||
}
|
||||
|
||||
/// 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_core::*;
|
||||
/// # use leptos_macro::*;
|
||||
/// # use leptos_dom::*; use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if cfg!(not(any(feature = "csr", feature = "hydrate", feature = "ssr"))) {
|
||||
/// async fn fetch_cats(how_many: u32) -> Result<Vec<String>, ()> { Ok(vec![]) }
|
||||
///
|
||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
/// let (pending, set_pending) = create_signal(cx, false);
|
||||
///
|
||||
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <div>
|
||||
/// <Transition
|
||||
/// fallback={"Loading...".to_string()}
|
||||
/// on_pending=set_pending
|
||||
/// >
|
||||
/// {move || {
|
||||
/// cats.read().map(|data| match data {
|
||||
/// Err(_) => view! { cx, <pre>"Error"</pre> },
|
||||
/// Ok(cats) => view! { cx,
|
||||
/// <div>{
|
||||
/// cats.iter()
|
||||
/// .map(|src| {
|
||||
/// view! { cx,
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect::<Vec<_>>()
|
||||
/// }</div>
|
||||
/// },
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// </Transition>
|
||||
/// </div>
|
||||
/// };
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Transition<F, E, G>(cx: Scope, props: TransitionProps<F, E, G>) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E + 'static,
|
||||
{
|
||||
let context = SuspenseContext::new(cx);
|
||||
|
||||
// provide this SuspenseContext to any resources below it
|
||||
provide_context(cx, context);
|
||||
|
||||
let child = (props.children)().swap_remove(0);
|
||||
|
||||
render_transition(cx, context, props.fallback, child, props.on_pending)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
fn render_transition<'a, F, E, G>(
|
||||
cx: Scope,
|
||||
context: SuspenseContext,
|
||||
fallback: F,
|
||||
child: G,
|
||||
on_pending: Option<Box<dyn Fn(bool)>>,
|
||||
) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E,
|
||||
{
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
let has_rendered_once = Cell::new(false);
|
||||
let prev_child = RefCell::new(Child::Null);
|
||||
|
||||
move || {
|
||||
if context.ready() {
|
||||
has_rendered_once.set(true);
|
||||
let current_child = (child)().into_child(cx);
|
||||
*prev_child.borrow_mut() = current_child.clone();
|
||||
if let Some(pending) = &on_pending {
|
||||
pending(false);
|
||||
}
|
||||
current_child
|
||||
} else if has_rendered_once.get() {
|
||||
if let Some(pending) = &on_pending {
|
||||
pending(true);
|
||||
}
|
||||
prev_child.borrow().clone()
|
||||
} else {
|
||||
if let Some(pending) = &on_pending {
|
||||
pending(true);
|
||||
}
|
||||
let fallback = fallback.clone().into_child(cx);
|
||||
*prev_child.borrow_mut() = fallback.clone();
|
||||
fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
fn render_transition<'a, F, E, G>(
|
||||
cx: Scope,
|
||||
context: SuspenseContext,
|
||||
fallback: F,
|
||||
orig_child: G,
|
||||
on_pending: Option<Box<dyn Fn(bool)>>,
|
||||
) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E + 'static,
|
||||
{
|
||||
use leptos_dom::IntoAttribute;
|
||||
use leptos_macro::view;
|
||||
|
||||
_ = on_pending;
|
||||
|
||||
let initial = {
|
||||
// run the child; we'll probably throw this away, but it will register resource reads
|
||||
let mut child = orig_child().into_child(cx);
|
||||
while let Child::Fn(f) = child {
|
||||
child = (f.borrow_mut())();
|
||||
}
|
||||
|
||||
// no resources were read under this, so just return the child
|
||||
if context.pending_resources.get() == 0 {
|
||||
child
|
||||
}
|
||||
// show the fallback, but also prepare to stream HTML
|
||||
else {
|
||||
let key = cx.current_fragment_key();
|
||||
cx.register_suspense(context, &key, move || {
|
||||
orig_child().into_child(cx).as_child_string()
|
||||
});
|
||||
|
||||
// return the fallback for now, wrapped in fragment identifer
|
||||
Child::Node(view! { cx, <div data-fragment-id=key>{fallback.into_child(cx)}</div> })
|
||||
}
|
||||
};
|
||||
move || initial.clone()
|
||||
}
|
Loading…
Reference in a new issue