Matt-Esch / virtual-dom

A Virtual DOM and diffing algorithm
MIT License
11.65k stars 780 forks source link

How would one do animations using virtual-dom? #112

Open paldepind opened 9 years ago

paldepind commented 9 years ago

I've seen talk about hooks and discussions about the possibility for a tombstone feature. But what is the current suggested practice with regards to animations? I don't expect virtual-dom to do the heavy lifting. But is there a clean way to control enter animations, delayed removal, reorder animations, etc. by hand?

TimBeyer commented 9 years ago

IMO the best way to do animations is to use CSS animations triggered by class changes on re-render. The DOM node will be diffed, updated, the class attached and the animation starts.

paldepind commented 9 years ago

Sure! CSS animations triggered by class adding and removing classes – that's how I'd do it by hand.

So one should simply to do something like this if wanting to remove an element from a list:

  1. Add a, lets say ,fade-out-animation class to the element to be removed.
  2. Setup a callback listening for the transitionend event.
  3. In the callback actually remove the element from the virtual-dom, diff and patch.

But there is nothing in virtual-dom to assist with animations (not that having such a thing is a necessity)?

Matt-Esch commented 9 years ago

I think the concerns of animation sit beyond virtual-dom, simply because there is often state and timing associated with it. This state would be modeled independently of the virtual-dom rendering artifact. It's probably worth building a library to make animations easier.

@Raynos and I tried to get animations working in the 2048 example in mercury. I don't think it's quite there yet, and I concluded that we needed animation hooks to get it working properly. But I think it's also important for us to optimize hooks so that they aren't always executed globally (see the discussions around performance).

paldepind commented 9 years ago

My main concern is that one of the huge advantages to using virtual-dom is how it enables me to simply update my data/models, forget about it, throw the data into a function building a new virtual-dom, do a diff and a patch. Then I know that the DOM is kept in sync and I wont have to worry exactly about which changes I made to the data. It's wildly convenient!

But if I manually have to trigger all animations and keep track of which data changes I need to apply delayed then I once again have to keep track of all my data changes and act on them in a view layer. So suddenly the advantage virtual-dom provided is gone and might as well just do the actual creation and deletion of elements myself as well since I'm handling all the animations anyway.

There might be something that I'm overlooking or an advantage with virtual-dom that I'm not getting. But I know how other libraries that handles the DOM for you, like React and Angular, provides tools for doing animations. I've used the animation system in Angular and while I'm no thrilled by it it does make certain types of animations trivial and having something is essential when you've given up direct control of updating the DOM.

Raynos commented 9 years ago
function tween(observ, opts) {
    var beginValue = observ();
    var endValue = opts.endValue;
    var duration = opts.duration;
    var onFinish = opts.onFinish;

    var delta = (beginValue - endValue)  / (1000 / 16);
    var currValue = beginValue;

    var timer = setInterval(function () {
        duration = duration - 16;
        currValue = currValue - delta;

        observ.set(currValue);

        if (duration <= 0) {
            clearInterval(timer);
            onFinish();
        }
    }, 16);
}

function Modal() {
    var state = hg.struct({
        visible: hg.value(false),
        opacity: hg.value(1),
        handles: hg.value(null)
    });

    state.handles.set(hg.handles({
        close: function (state) {
            tween(state.opacity, {
                endValue: 0,
                duration: 1000,
                onFinish: function () {
                    state.visible.set(false);
                }
            });
        }
    }, state));
}

Modal.show = function (state) {
    state.visible.set(true);
};

Modal.render = function (state, opts) {
    var handles = state.handles;

    h('div.modal', {
        style: {
            display: state.visible ? 'block' : 'none',
            opacity: String(state.opacity)
        },
        'ev-click': hg.event(handles.close)
    }, [
        opts.content
    ]);
};

If you want an animation library to handle things for you. I recommend you write and maintain a tweening function. This makes animation some properties a lot easier.

Especially if you use the onFinish trick to do cleanup once you are done animating..

paldepind commented 9 years ago

Doing animations with setInterval is not efficient – that doesn't achieve smooth animations on mobile. But obviously the tween function could be modified to use CSS transitions instead.

Your example above is how you'd do it in mercury as far I can see? But you're mixing view updating with state updating. Are you fine with that? From the mercury readme: "mercury encourages zero dom manipulation in your application code". But including an opacity value – that is strictly view related – along with the state seems awfully close to modifying the DOM just with a bit of indirection.

Obviously I'm not very familiar with mercury.

Raynos commented 9 years ago

@paldepind the state is not "model state". Its application state. its all the state needed to render the visual scene.

This includes visibility booleans, this includes color strings, this includes opacity strings. You cannot escape the need to express the visual scene.

It should also be noted that the state I show here is the view state or the view model. in a large application your expected to have a separate set of state that is your model or your domain. This would have nothing to do with the UI.

I have toyed around with a convention that all temporary UI state should be nested under a viewState key or something similar to make things more obvouis.

paldepind commented 9 years ago

Ok. Thanks for clearing that up. Yes, you'll indeed have to save the visual state somewhere. In my opinion keeping it separated under a viewState key sounds nice :)

I'll take a look at the source for 2024 in the 2024 branch!

tcoats commented 9 years ago

Creating a Widget class seems to have enough extension points to manage your own transitions. If you want to do it independently of state changes and don't want to use css animations.

That's what I'm doing in odojs's hook component that can be used for animation as shown in the hook documentation. It's odojs specific but is built completely off virtual-dom so the concept could be extracted.

Unfortunately this puts a 'kink' in a nice vdom tree that stringify and other tree walks can't see through.