made <Show> accept signals in addition to closures (#4236)

This commit is contained in:
Marc-Stefan Cassola
2025-08-25 16:50:20 -05:00
committed by GitHub
parent 1d0f668dc3
commit db9f323f8d
2 changed files with 122 additions and 21 deletions

View File

@@ -1,14 +1,18 @@
use crate::{children::ChildrenFn, component, control_flow::Show, IntoView};
use crate::{
children::ChildrenFn, component, control_flow::Show, show::IntoCondition,
IntoView,
};
use core::time::Duration;
use leptos_dom::helpers::TimeoutHandle;
use leptos_macro::view;
use reactive_graph::{
diagnostics::SpecialNonReactiveZone,
effect::RenderEffect,
owner::{on_cleanup, StoredValue},
signal::RwSignal,
traits::{Get, GetUntracked, GetValue, Set, SetValue},
wrappers::read::Signal,
traits::{GetValue, Set, SetValue},
};
use std::marker::PhantomData;
use tachys::prelude::*;
/// A component that will show its children when the `when` condition is `true`.
@@ -46,14 +50,16 @@ use tachys::prelude::*;
/// }
/// # }
/// ```
///
/// Please note, that unlike `Show`, `AnimatedShow` does not support a `fallback` prop.
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn AnimatedShow(
pub fn AnimatedShow<M>(
/// The components Show wraps
children: ChildrenFn,
/// If the component should show or not
#[prop(into)]
when: Signal<bool>,
/// When true the children are shown.
/// It accepts a closure that returns a boolean value as well as a boolean signal or plain boolean value.
when: impl IntoCondition<M>,
/// Optional CSS class to apply if `when == true`
#[prop(optional)]
show_class: &'static str,
@@ -62,17 +68,26 @@ pub fn AnimatedShow(
hide_class: &'static str,
/// The timeout after which the component will be unmounted if `when == false`
hide_delay: Duration,
/// Marker for generic parameters. Ignore this.
#[prop(optional)]
_marker: PhantomData<M>,
) -> impl IntoView {
let when = when.into_condition();
// Silence warnings about using signals in non-reactive contexts.
#[cfg(debug_assertions)]
let z = SpecialNonReactiveZone::enter();
let handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
let cls = RwSignal::new(if when.get_untracked() {
show_class
} else {
hide_class
});
let show = RwSignal::new(when.get_untracked());
let cls = RwSignal::new(if when.run() { show_class } else { hide_class });
let show = RwSignal::new(when.run());
#[cfg(debug_assertions)]
drop(z);
let eff = RenderEffect::new(move |_| {
if when.get() {
if when.run() {
// clear any possibly active timer
if let Some(h) = handle.get_value() {
h.clear();
@@ -100,8 +115,8 @@ pub fn AnimatedShow(
});
view! {
<Show when=move || show.get() fallback=|| ()>
<div class=move || cls.get()>{children()}</div>
<Show when=show>
<div class=cls>{children()}</div>
</Show>
}
}

View File

@@ -1,26 +1,71 @@
use crate::{
children::{TypedChildrenFn, ViewFn},
prelude::{FunctionMarker, SignalMarker},
IntoView,
};
use leptos_macro::component;
use reactive_graph::{computed::ArcMemo, traits::Get};
use std::{marker::PhantomData, sync::Arc};
use tachys::either::Either;
/// Shows its children whenever the condition `when` prop is `true`.
/// Otherwise it renders the `fallback` prop, which defaults to the empty view.
///
/// The prop `when` can be a closure that returns a bool, a signal of type bool, or a boolean value.
///
/// ## Usage
///
/// ```
/// # use leptos::prelude::*;
/// #
/// # #[component]
/// # pub fn Demo() -> impl IntoView {
/// let (condition, set_condition) = signal(true);
///
/// view! {
/// <Show when=condition>
/// <p>"Hello, world!"</p>
/// </Show>
/// }
/// # }
/// ```
///
/// Or with a closure as the `when` condition:
///
/// ```
/// # use leptos::prelude::*;
/// #
/// # #[component]
/// # pub fn Demo() -> impl IntoView {
/// let (condition, set_condition) = signal(true);
///
/// view! {
/// <Show when=move || condition.get()>
/// <p>"Hello, world!"</p>
/// </Show>
/// }
/// # }
/// ```
#[component]
pub fn Show<W, C>(
pub fn Show<M, C>(
/// The children will be shown whenever the condition in the `when` closure returns `true`.
children: TypedChildrenFn<C>,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// When true the children are shown, otherwise the fallback.
/// It accepts a closure that returns a boolean value as well as a boolean signal or plain boolean value.
when: impl IntoCondition<M>,
/// A closure that returns what gets rendered if the when statement is false. By default this is the empty view.
#[prop(optional, into)]
fallback: ViewFn,
/// Marker for generic parameters. Ignore this.
#[prop(optional)]
_marker: PhantomData<M>,
) -> impl IntoView
where
W: Fn() -> bool + Send + Sync + 'static,
C: IntoView + 'static,
{
let memoized_when = ArcMemo::new(move |_| when());
let when = when.into_condition();
let memoized_when = ArcMemo::new(move |_| when.run());
let children = children.into_inner();
move || match memoized_when.get() {
@@ -28,3 +73,44 @@ where
false => Either::Right(fallback.run()),
}
}
/// A closure that returns a bool. Can be converted from a closure, a signal, or a boolean value.
pub struct Condition(Arc<dyn Fn() -> bool + Send + Sync + 'static>);
impl Condition {
/// Evaluates the condition and returns its result.
pub fn run(&self) -> bool {
(self.0)()
}
}
/// Trait to convert various types into a `Condition`.
/// Implemented for closures, signals, and boolean values.
pub trait IntoCondition<M> {
/// Does the conversion
fn into_condition(self) -> Condition;
}
impl<S> IntoCondition<SignalMarker> for S
where
S: Get<Value = bool> + Send + Sync + 'static,
{
fn into_condition(self) -> Condition {
Condition(Arc::new(move || self.get()))
}
}
impl<F> IntoCondition<FunctionMarker> for F
where
F: Fn() -> bool + Send + Sync + 'static,
{
fn into_condition(self) -> Condition {
Condition(Arc::new(self))
}
}
impl IntoCondition<Condition> for Condition {
fn into_condition(self) -> Condition {
self
}
}