No way to distinguish between enter and exit when switching out a dynamic component
This is not really a bug, but not a feature request either...
I have a dynamic component. When it rerenders with new data, I want to animate the old one out, and animate the new one in. I'm coming from Vue and was very happy (and lucky timingwise) to read that you added a "vue inspired mode out-in" component for switching out dynamic components.
I am wondering how you intend to define a different animation for exiting and entering with javascript.
The only way I found animating with gsap and react-transition-group comes from here, with a demo provided here on stackblitz, where they use the addEndListener hook to inject their animations.
Here is my own attempt at this: https://codesandbox.io/s/transitiontest-vvkk1
Please take a look at Questionnaire.jsx, where I want to dynamically transition between different Question.jsxs.
Since I don't have a show-boolean, because the component is always shown, I cannot decide if the component should get the exiting- or entering animation.
<SwitchTransition mode="out-in">
<Transition
key={questionObject.id}
addEndListener={(node, done) => {
/**
* this is where the problem lies...
* how do you intend to tell this function
* if this is an animation for an exiting
* or entering animation if there is no `show` boolean?
*/
// animation for leaving
TweenMax.to(node, 0.5, {
x: 50,
onComplete: done
});
// animation for entering
//TweenMax.from(node, 0.5, {
//x: 50,
//opacity: 0,
//onComplete: done
//});
}}
>
<Question questionObject={questionObject} />
</Transition>
</SwitchTransition>
Is there a way to notate javascript animations for each phase individually? (also with node and done etc.)
I mean, there must be, right? Isn't this the whole purpose for the <SwitchTransition>-component?
In general addEndListener isn't for running animations, but for adding custom logic to report when the started animation finishes, which is why it doesn't have access to the current phase. That said i think it should have access to it, but i don't know that it will solve your problem since it's a bit too late to start a tween
If you look at my demo, it's working almost perfectly; the old component gets an animation on its way out, THEN the new one mounts and gets an animation aswell. Timingwise this is perfect. The only problem is, that they both get the exact same animation, which is obviously not what I want.
I don't like using addEndListener for this either, but it's what I found is suggested by the GSAP makers themselves, which is why I think it's the only way.
The way I understand onEnter etc. is also not suited for injecting javascript animations here, right? I mean, you would need something like done() to tell the transition that the exit/enter is over in order to make mode="out-in" work properly, right?
Yeah I mean the alternative is also awkward, i'm not saying it's ideal, but you should be able to do something like:
class TweenTransition {
onEnter(node) {
TweenMax.to(node, 0.5, {
x: 50,
onComplete: () => {
this.callback();
},
});
}
onExit(node) {
TweenMax.to(node, 0.5, {
x: 0,
onComplete: () => {
this.callback();
},
});
}
addEndListener(_, done) {
this.callback = done;
}
render() {
return (
<Transition
{...this.props}
onEnter={this.onEnter}
onExit={this.onExit}
addEndListener={this.addEndListener}
/>
);
}
}
which would technically start and end the transitions at the point the inner Transition expects it.
That said i'm totally open to passing through the current phase to addEndListener if it otherwise works fine.
Passing through the current phase to addEndListener would be real handy for now; if you do that I'll also inform the GSAP makers to update their demos to reflect that handy change.
I understand correctly that you are the owner of that package? If so, blindly asking: Why not go full "transition like in vue"? and skip the child-<Transition> and make something like this (very similar to vue) possible:
<SwitchTransition
mode={"out-in"}
beforeEnter={ this.beforeEnterAnimation }
enter={ this.enterAnimation }
beforeExit={ () => { ... } }
exit={ (node, done) => TweenMax... }
>
<MyComponent />
</SwitchTransition>
mostly b/c it's more code, and less flexible. Under the hood the Switch component is implemented as a TransitionGroup which depends on Transition children. The other advantage here is you can reuse encapsulated Transitions. So if you had a Fade component you could use it by itself as well as in the context of a Switch
Oh, speaking of the Fade component; could you provide a working demo, maybe on codesandbox, for the provided example here: It is frustrating to see an example and not knowing how this Fade-transition is supposed to work, since you also don't use a show boolean here.
Can I myself pass the animation phase to addEndListener or is this something you must do?
it would look a lot like the code above. There isn't much to encapsulating a component, you specify the props specific to your animation and pass through everything else.
Sorry, I can't follow you :D
Can you add passing the animation-phase parameter to addEndListener and update the repo? That would be the easiest, I guess :D
Thanks!
I'm not likely to get to it in the near future, but if you want to get it in quickly please send over a PR
I am too interested in something like that and have hit the same wall.
Okay; I could try to pass through the parameter, but could you give me a hint/point me through where to do that?
I actually managed to work around these problems.
I am fading out a page and then fading the second page in, while both fade-in and fade-out are different animations handled by gasp timelines. The element wrapped inside the Transition/TransitionGroup components is a Switch component from react-router-dom, allowing me to transition between pages on location change.
I followed this tutorial but had to make some changes:
For some reason, fade-in and fade-out are called at the same time (not sure if this is an actual issue when wrapping a switch with the transition component or just some mistake on my end). I managed to work around that problem by starting my fade-out animation timeline through the onExit callback, and delaying the fade-in animation called by the onEnter callback by the amount of time the fade-out took.
Another problem that came with onEnter/onExit being called at the same time was that the new page was being added to the DOM from the very beginning of the transition, "above" the fade-out animation, obstructing it. I solved that by setting the node opacity to 0 in the beginning of the fade-in animation, and after the delay (when the fade-out has finished) setting it to 1 and then proceed with the actual fade-in animation.
@jquense I am not sure if this is any help to you, but since you are heading into a similar direction these might be some issues you now know how to handle if you encounter them.
Okay; I could try to pass through the parameter, but could you give me a hint/point me through where to do that?
Yeah definitely, the logic is all in the Base Transition component, you want to see where the addEndListener is called and work backwards to where you know what "phase" your in and pass it through
I did it! :) – luckily for me it was way easier than I expected :D – The biggest trouble (since my lack of npm experience) was figuring out how to get the changes into my project.
look at the pull request for more info :) https://github.com/reactjs/react-transition-group/pull/523
@flo-wolf
A more robust workaround, where you can still do npm i without having to re-edit the source files, is to make your own component and simply change the onTransitionEnd function of the prototype.
// your ChangedTransition.js
import { Transition as T, } from 'react-transition-group';
const Transition = T;
Transition.prototype.onTransitionEnd = function onTransitionEnd(node, timeout, handler) {
this.setNextCallback(handler);
const doesNotHaveTimeoutOrListener = timeout === null && !this.props.addEndListener;
if (!node || doesNotHaveTimeoutOrListener) {
setTimeout(this.nextCallback, 0);
return;
}
if (this.props.addEndListener) {
this.props.addEndListener(node, this.nextCallback, this.state.status);
}
if (timeout !== null) {
setTimeout(this.nextCallback, timeout);
}
};
export default Transition;
using it in
// MyComponent.jsx
import { Transition } from "./ChangedTransition.js"
...
@jquense – slightly pushing this again :)
@jquense – is there a reason you haven't merged #523 into the latest release from 3 days ago?
@jquense I honestly don't understand why you encouraged me to make a PR and then went AWOL
This is something I do in my spare time, in addition to my full time job, family, and myriad of other OSS projects I maintain. I need to pull down your PR try it and confirm it works well and have not had the time. I don't work for you and you do not have a right to my time. I understand that it can be frustrating to not have things work at your preferred pace but please remember that everyone here has a life outside this project and your are using it for free
I'm sorry if I offended you; I definitely didn't mean to.
Though I'm still wondering why you encouraged me to add this functionality via PR, when you can't find the time to check this "relatively minor change" for half a year and counting.
Still nothing, huh?