aholachek / react-flip-toolkit

A lightweight magic-move library for configurable layout transitions
MIT License
4.09k stars 138 forks source link

New items entering? #95

Open jonathantrevor opened 5 years ago

jonathantrevor commented 5 years ago

Does the flip toolkit support animating new items entering (and exiting)?

If I have an array that goes from 0 elements to 10 elements they simply "appear" without any animation. However, if I "shuffle" the array to reorder the existing items it animates very nicely.

I'd really like to get a simple "stacked" animation for new items, perhaps with a direction of appearance (from bottom) or "grow in place" (scale) or even just opacity (or some combination).

aholachek commented 5 years ago

Hello, yes those callbacks (onAppear and onExit) are documented here: https://github.com/aholachek/react-flip-toolkit#callback-props You will have to manage the animations yourself however, the easiest way to do it is to toggle a class with a keyframe animation on the element

jonathantrevor commented 5 years ago

Ahh, thank you! I thought it might be part of the default behavior I could turn on but the example is very helpful.

Is there a good way of sequencing the onEnter and onExit animations (such as fadeIn or fadeOut) so that the flip animation for "existing" elements happens before or after either? The onExit is particularly jarring since the flipped items move "over" the exiting items, and then they exit, which (for a simple list) looks wrong.

zero298 commented 5 years ago

@jonathantrevor Not sure if you still need this, but I ended up coming up with a way to deal with transitioning deletion/insertion. Hopefully either you or someone who comes along later can use it as a starting point.

The key here was to use the Flipper#handleEnterUpdateDelete. You can add additional items using the "Add" button and can swap between a vertical and horizontal layout using the "Rotate" button. Clicking on each item will remove it from the list with an animation:

import React, {useState} from "react";
import {Flipper, Flipped} from "react-flip-toolkit";

/**
 * Thin wrapper around Element.animate() that returns a Promise
 * @param el Element to animate
 * @param keyframes The keyframes to use when animating
 * @param options Either the duration of the animation or an options argument detailing how the animation should be performed
 * @returns A promise that will resolve after the animation completes or is cancelled
 */
export function animate(
    el: HTMLElement,
    keyframes: Keyframe[] | PropertyIndexedKeyframes,
    options?: number | KeyframeAnimationOptions
): Promise<void> {
    return new Promise(resolve => {
        const anim = el.animate(keyframes, options);
        anim.addEventListener("finish", () => resolve());
        anim.addEventListener("cancel", () => resolve());
    });
}

const AddRemoveTest = () => {
    const [items, setItems] = useState([
        {id: 0},
        {id: 1},
        {id: 2},
        {id: 3},
        {id: 4},
        {id: 5},
        {id: 6},
        {id: 7}
    ]);

    const [nextId, setNextId] = useState(8);
    const [vertical, setVertical] = useState(false);

    async function onAppear(el: HTMLElement) {
        await animate(el, [
            {opacity: 0},
            {opacity: 1}
        ], {
            duration: 200
        });
        el.style.opacity = "1";
    }

    async function onExit(el: HTMLElement, _idx: number, onComplete: () => void) {
        await animate(el, [
            {opacity: 1},
            {opacity: 0}
        ], {
            duration: 200
        });
        onComplete();
    }

    function clickAddHandler() {
        setItems([...items, {id: nextId}]);
        setNextId(nextId + 1);
    }

    return (
        <div>
            <button onClick={clickAddHandler}>Add</button>
            <button onClick={() => setVertical(!vertical)}>Rotate</button>
            <Flipper
                flipKey={items.reduce((acc, {id}) => `${acc}-${id}`, "items:")}
                handleEnterUpdateDelete={async ({
                    hideEnteringElements,
                    animateEnteringElements,
                    animateExitingElements,
                    animateFlippedElements
                }) => {
                    hideEnteringElements();
                    await animateExitingElements();
                    await animateFlippedElements();
                    animateEnteringElements();
                }}
            >
                {items.map(({id}) => (
                    <Flipped
                        key={`item:${id}`}
                        flipId={`item:${id}`}
                        onAppear={onAppear}
                        onExit={onExit}
                    >
                        <button
                            style={{
                                width: 50,
                                height: 50,
                                display: vertical ? "block" : "inline-block"
                            }}
                            onClick={() => setItems(items.filter(({id: itemId}) => itemId !== id))}
                        >{id}</button>
                    </Flipped>
                ))}
            </Flipper>
        </div>
    );
}

export default AddRemoveTest;
Grafikart commented 5 years ago

I don't know if it's the smae problem. I wrote this

<Flipped
          key={file}
          flipId={file}
          onExit={onExit}

And onExit wasn't called.

I was using an object as a key and as flipId (but flipId has to be a string). Maybe a check could be added to throw an error if flipId is not a string (or I should be more carefull when reading prop documentation) ?