How to trigger exit animation without LiveMotion.JS call
Hi!
Sorry for the barrage of issues, but I was wondering if there is a way to trigger the exit animation of a component via toggling a prop on the LiveMotion.motion component?
The docs outline a way to do trigger exit animations via a LiveMotion.JS call, but I can't seem to find a prop that will allow me trigger the exit animation by toggling the value of a prop.
Looking for something along the lines of:
<LiveMotion.motion
id="foo"
show={@some_state_on_my_socket}
animate={[y: 20, opacity: [0, 1]]}
exit={[y: 0, opacity: [1, 0]]}
transition={[duration: 1.5]}
>
where toggling @some_state_on_my_socket to true or false would trigger the animate and exit animations respectively.
Is this supported? Thanks!
Hey, this should work properly. As soon as the motion component is removed via LiveView, the exit animation will trigger.
<%= if @some_state_on_my_socket do %>
<LiveMotion.motion
id="foo"
initial={[y: 0, opacity: 0]}
animate={[y: 20, opacity: 1]}
exit={[y: 0, opacity: 0]}
transition={[duration: 1.5]}
></LiveMotion>
<% end %>
One sidenote: the master branch contains a new component called LiveMotion.presence which is exactly for use-cases where you would have something like a pagination, where the exiting and entering element could interfere with each other (e.g. when not positioned absolute).
I am still holding back the release due to a possible bug in LiveView (https://github.com/phoenixframework/phoenix_live_view/issues/2114)
Ah I see. I believe I tried this earlier and couldn't get it working, although it should be noted that I'm using SurfaceUI which may or may not be messing with LiveViews mount/unmount cycle?
I'm wondering about whether the exit behavior would still be triggered with removing an element from a list that is being iterated over in the view with a for comprehension, like so:
{#for {topic_id, {topic, current_topic_start_time}} <- @topic_notifications}
<LiveMotion.motion
id={"active-topic-notification-animation-#{id}"}
class="w-full absolute z-10"
animate={[y: 20, opacity: [0, 1]]}
exit={[y: 0, opacity: [1, 0]]}
transition={[duration: 1.5]}
>
<div>{topic.text}</div>
{/for}
If I were to update @topic_notifications to remove an entry, would you expect the exit animation to still occur for that element? It doesn't appear to be as far as I can tell. I attached a gif screencap of whats currently happening--the element animates in perfectly, but is immediately removed from the dom when an entry for that notification is removed (instead of being animated out)

The other thing of concern is that I have no idea how the actual content of the div inside the LiveMotion.motion component would actually be preserved while animating out, it seems unlikely to me?
An alternative to this approach (and why I was asking if there was a prop that LiveMotion.motion takes to trigger the animate in/out animations) was to never remove the DOM element (ie don't use an if or a for comprehension for show/hide logic) and instead toggle some state on each notification to trigger animation in/out.
The bummer with that approach is that it still leaves the responsibility on the consumer of this lib to clean up the component element after it has finished animating, as well as persisting the notification data in socket state until the animation is complete, which I was hoping to avoid having to think about.
EDIT: Also that LiveMotion.presence sounds cool! I couldn't find the LiveMotion.presence code on main though, would you mind pointing me to it? I imagine it might not be possible to import the dep from main without the potential live view bugfix, right?
Oh sorry, it's on the presence branch (https://github.com/benvp/live_motion/blob/presence/lib/live_motion.ex#L268) - undocumented, though. The main logic is on the JS here.
Anyway, It should work within comprehensions, but as you said, it might be surface which is interfering here. I never worked with it so that's out of my knowledge. But a short recap, on how exit currently works.
LiveMotion.motion adds a phx-remove attr, which triggers a LiveMotion.JS.hide action (https://github.com/benvp/live_motion/blob/main/lib/live_motion.ex#L259). This is picked up by the JS and calls into liveSocket.transition. This is the part which defers the actual deletion from the DOM.
I don't know if surfaces handles removal of DOM nodes, but I wouldn't actually think of it, because surface just compiles to normal live view code iirc.
In my last test using presence, I tried it with a comprehension and it worked quite good. (Only have screenshots from the code left, sorry!)


Wow that's cool! Do you not have to do anything other than wrap your list of animated things in LiveMotion.presence to get it to work?
Also, we totally got things working yesterday. It turns out that for some reason when animating the div that was animating got shifted below its sibling div. Wrapping the LiveMotion.motion component in a div prevented this behavior from happening and everything started working again! Also we saw some funky re-animation behavior if we didn't specify an initial prop. Should we always specify one?
Nothing else, presence keeps track of the components mount/unmounting. Do you know why it has been shifted below a sibling div?
Can you elaborate on the funky re-animation behaviour? In general, the initial prop makes sense when you want an initial state of the element to animate from. Say you want to animate to x: 0 when mounting, then you could set initial to x: -20 so that the final position is x: 0.