leptos-rs / leptos

Build fast web applications with Rust.
https://leptos.dev
MIT License
16.17k stars 636 forks source link

`<ShowAnimate>` or similar components #1370

Closed sebadob closed 1 year ago

sebadob commented 1 year ago

I have used svelte quite a lot in the past and one thing, that is really great about it, is the built in animations. Using these, you can get something "pretty" really quickly.

I now that leptos has no opinion about css, but a basic wrapper component built in would be really nice. Since I don't know if leptos even wants to include such things, I created an issue and not a PR directly. I am using components like these a lot to provide a smoother UX and the most basic one is already created, so you could actually just copy & paste it maybe.

The following code is based on the start-axum template and you can copy & paste it into the app.rs to test it. The idea is, that you can have easy unmounting animations with it while still providing your own css classes or solutions. I have not written an inline style because some people might not want to allow unsafe-inline for css in their CSP (which actually is an issue for svelte too and they do have an open issue to fix that).

The <ShowAnimate> component from this code could maybe be implemented into leptos directly, if you want to go that route. There is the possibility for other basic default transitions in the future, which could be realized this way too. Just the same way that svelte does it, which is actually awesome.

use leptos::*;
use leptos::leptos_dom::helpers::TimeoutHandle;
use leptos_meta::*;

#[component]
pub fn App(cx: Scope) -> impl IntoView {
    provide_meta_context(cx);

    // add the following content to the `main.scss`:
    //
    //body { font-family: sans-serif; text-align: center; }
    //@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
    //@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
    //.fadeIn1000 { color: green; animation: 1000ms fadeIn forwards; }
    //.fadeOut1000 { color: red; animation: 1000ms fadeOut forwards; }

    let is_hover = create_rw_signal(cx, false);

    view! { cx,
        <Title text="AnimateMe" />

        <Stylesheet id="leptos" href="/pkg/start-axum.css"/>

        <div
            on:mouseenter=move |_| is_hover.set(true)
            on:mouseleave=move |_| is_hover.set(false)
        >
            "Hover Me"
        </div>

        <ShowAnimate
            when=is_hover
            show_class="fadeIn1000"
            hide_class="fadeOut1000"
            hide_delay=core::time::Duration::from_millis(1000)
        >
            "Here I Am!"
        </ShowAnimate>
    }
}

#[component]
pub fn ShowAnimate(
    cx: Scope,
    #[prop(into)] when: MaybeSignal<bool>,
    show_class: &'static str,
    hide_class: &'static str,
    hide_delay: core::time::Duration,
    children: ChildrenFn,
) -> impl IntoView {
    let handle: StoredValue<Option<TimeoutHandle>> = store_value(cx, None);
    let cls = create_rw_signal(
        cx,
        if when.get_untracked() {
            show_class
        } else {
            hide_class
        },
    );
    let show = create_rw_signal(cx, when.get_untracked());

    create_effect(cx, move |_| {
        if when.get() {
            // clear any possibly active timer
            if let Some(h) = handle.get_value() {
                h.clear();
            }

            cls.set(show_class);
            show.set(true);
        } else {
            cls.set(hide_class);

            let h = set_timeout_with_handle(
                move || {
                    show.set(false);
                },
                hide_delay,
            )
                .expect("set timeout");
            handle.set_value(Some(h));
        }
    });

    on_cleanup(cx, move || {
        if let Some(h) = handle.get_value() {
            h.clear();
        }
    });

    view! { cx,
        <Show when=move || show.get() fallback=|_| ()>
            <div>
                <div class=move || cls.get()>
                    {children(cx)}
                </div>
            </div>
        </Show>
    }
}

I can open a PR about this, if you like this idea.

sebadob commented 1 year ago

I added a sliding animation to my own code now, and with these CSS classes, you could easily have fading or a sliding container, which can of course be customized as needed.

@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
.fadeIn500 { animation: 500ms fadeIn forwards; }
.fadeOut500 { animation: 500ms fadeOut forwards; }
.fadeIn250 { animation: 250ms fadeIn forwards; }
.fadeOut250 { animation: 250ms fadeOut forwards; }

@keyframes slideDown { from { height: 0; } to { height: 100%; } }
@keyframes slideUp { from { height: 100%; } to { height: 0; } }
.slideDown500 { overflow: hidden; animation: 500ms slideDown forwards; }
.slideUp500 { overflow: hidden; animation: 500ms slideUp forwards; }
.slideDown250 { overflow: hidden; animation: 250ms slideDown forwards; }
.slideUp250 { overflow: hidden; animation: 250ms slideUp forwards; }

Maybe leptos could, if you decide to include such a feature, have an animation feature that could be activated and if this is the case, provide such components and maybe add some default animations on top of the css import.

You could do this more easily with inline styles, but this would become a problem with a possible CSP as already mentioned.

<ShowAnimate
    when=is_expanded
    show_class="slideDown500"
    hide_class="slideUp500"
    hide_delay=core::time::Duration::from_millis(500)
>
    <div class="nav-sub-menu">
        {children(cx)}
    </div>
</ShowAnimate>