CSS animation when adding new elements to the DOM

When LV change-detection results in new elements rendered, what is the recommended way to do that?

Is this OK? (works, but not sure if right way to do it)

defmodule TestlvtransitionsWeb.MyLive do
  use TestlvtransitionsWeb, :live_view

  def render(assigns) do
    ~H"""
    <style>
      @keyframes fadein {
        from { opacity: 0.2; }
        to { opacity: 1; }
      }
      .new-box { animation: fadein 1000ms linear; }
    </style>
    <div phx-click="show1">SHOW 1</div>
    <div phx-click="show2">SHOW 2</div>
    <div :if={@show1} class="bg-red-500 fadein">HELLO</div>
    <div :if={@show2} class="bg-red-500 fadein">HELLO</div>
    """
  end

  def mount(_, _, socket), do: {:ok, assign(socket, show1: false, show2: false)}

  def handle_event("show1", _, socket), do: {:noreply, assign(socket, :show1, true)}
  def handle_event("show2", _, socket), do: {:noreply, assign(socket, :show2, true)}
end
3 Likes

I would give phx-mounted a try :slight_smile:

1 Like

I’m having some trouble with that.
I think I have to match the transition-duration property value to the time option. (the docs don’t do that, but I do not see how else this should work). OK I did that. Also in class I set the same opacity as in the start-slot of the 3-tuple, otherwise the element appears-vanishes-appears. But still I do not get a smooth transition, it just pops up.

<div
  :if={@show1}
  phx-mounted={
    JS.transition({"ease-out duration-1000", "opacity-20", "opacity-100"}, time: 1000)
  }
  class="bg-red-500 opacity-20"
>

There is a solution with JS.show here: Achieving tailwind enter/leave transitions with LiveView.JS - #7 by paulstatezny

<div
  :if={@show1}
  style="display: none"
  phx-mounted={
    JS.show(
      transition: {"ease-linear duration-1000", "opacity-20", "opacity-100"},
      time: 1000
    )
  }
  class="opacity-20 bg-red-500"
>

:face_with_monocle:

How about Live Motion?

I’d rather not pull in another dependency for this trivial task. The CSS-animation works fine, but I still hope there is a way with LV (expect the hacky solution with JS.show).

1 Like

Take a look at animation-iteration-count. You don’t need JS or LV to limit an css animation to play only once.

I cant wrap my head around how to reliable use JS hooks to animate. There are some unintuitive things happening when animating stuffs and mixing JS hooks with server rendering :neutral_face: problem is probably on my end, but still

2 Likes

You are answering the questions I did not ask yet, thats next level :grin:
Actually trying to figure out one edge case now.

The version in the OP (animate) works fine (and also once as animation-iteration-count is set to 1 (implicitly as its the default). But consider this:

 <style>
      @keyframes fadein { from { opacity: 0.2; } to { opacity: 1; } }
      .new-box { animation: fadein 1000ms linear; }
 </style>
 <div phx-click="show1">SHOW red</div>
 <div phx-click="show2">SHOW blue</div>
 <div :if={@show1} class="bg-red-500 fadein">HELLO</div>
 <div :if={@show2} class="bg-blue-500 fadein">HELLO</div>

When one clicks “SHOW blue” first and then “SHOW red” the animation is blue both times. I have an idea whats happening and maybe one would have to set update-ignore or sth, but its not nice.

I think its just not really fully working right now.

Yep. I came to the same conclusion

I see that your element doesn’t have a an id. I think it needs a unique id assigned to work, otherwise LV can’t distinguish newly mounted elements from those already existing. phx-mounted works for me, but not if I remove the id. Maybe give it a try

Indeed, that solves the messed up animations. But it does not heal the JS.transition solution.
Here are all three approaches I tried: animation, JS.show (@paulstatezny solution) and JS.transition.
The first two look the same, the third does not work.

<div phx-click="show1">SHOW red</div>
<div phx-click="show2">SHOW blue</div>

<div class="flex flex-col gap-2">
  <div>
    <style>
      @keyframes fadein {
        from { opacity: 0.2; }
        to { opacity: 1.0; }
      }
      .fadein { animation: fadein 1 1000ms linear; }
    </style>
    <div :if={@show1} id="11" class="bg-red-500 fadein">HELLO ANIM 1</div>
    <div :if={@show2} id="21" class="bg-blue-500 fadein">HELLO ANIM 2</div>
  </div>

  <div>
    <div :if={@show1} id="21" style="display: none" class="opacity-20 bg-red-500"
      phx-mounted={JS.show(
          transition: {"ease-linear duration-1000", "opacity-20", "opacity-100"},
          time: 1000)}>HELLO LV.JS/A 1</div>
    <div :if={@show2} id="22" style="display: none" class="opacity-20 bg-blue-500"
      phx-mounted={JS.show(
          transition: {"ease-linear duration-1000", "opacity-20", "opacity-100"},
          time: 1000)}>HELLO LV.JS/A 2</div>
  </div>

  <div>
    <div :if={@show1} id="31" class="bg-red-500 opacity-20"
      phx-mounted={
        JS.transition({"ease-linear duration-1000", "opacity-20", "opacity-100"}, time: 1000)
      }>HELLO LV.JS/B 1</div>
    <div :if={@show2} id="32" class="bg-blue-500 opacity-20"
      phx-mounted={
        JS.transition({"ease-linear duration-1000", "opacity-20", "opacity-100"}, time: 1000)
      }>HELLO LV.JS/B 2</div>
  </div>
</div>

yep forget what I said, I just looked and I’m actually using phx-mounted + JS.show as well… I’m having a strong déjà vu feeling of having gone through all this already :laughing:

Here’s what works for me:

 <div
            id={elem.id}
            class="hidden"
            phx-mounted={
              JS.show(transition: {"ease-in duration-500", "opacity-0", "opacity-100"}, time: 500)
            }
          >
Hello
</div>

Basically, exactly like your solution. I guess thre’s something broken with phx-mounted={JS.transistion(...)} after all. :broken_heart:

3 Likes

Unless I’m mistaken, transitions with JS functions are altogether broken except when using JS.show and maybe JS.hide.

By “broken”, I mean to say that the start and end classes are not applied at the correct times.

I could be wrong, but I’ve spent hours trying many different things to make it work without success.

2 Likes

Yes, seems plausible from what I’ve seen.
I can’t find an issue on Github, maybe I should open one unless someone else comes around and explains it all.

Question why do you all prefer JS.show and all the tweaks and hacks over a simple CSS-animation? Am I missing sth?

1 Like

Not sure if it’s related, but the changelog shows a bugfix for js.transition: phoenix_live_view/CHANGELOG.md at master · phoenixframework/phoenix_live_view · GitHub

2 Likes

I’ve also come to the same conclusion. My mental model of how it should work is not overlapping with reality :sweat_smile:

This does not do what (I think) it should with LV 0.18.15

<div :if={@show1} id="21" class="bg-red-500 opacity-20"
        phx-mounted={JS.transition(
           {"ease-linear duration-1000", "opacity-20", "opacity-100"},
           time: 1000)}>
phoenix              1.7.0-rc.3  1.7.0-rc.3  Up-to-date  
phoenix_html         3.3.0       3.3.0       Up-to-date  
phoenix_live_view    0.18.15     0.18.15     Up-to-date  
tailwind             0.1.10      0.1.10      Up-to-date  

Hey @Sebb,

That library uses JS.dispatch.

Is that deprecated already?

this library has its own client-side handler. While it looks very nice, its overkill. After all its working with five lines of CSS.

1 Like

Yup, I went through all of the code and it seems too much.

CSS for the win.

Hopefully it will become as easy as copying an animation from animate.style and pasting it in Live View.

You’re asking why I prefer this:

<div
            id={elem.id}
            class="hidden"
            phx-mounted={
              JS.show(transition: {"ease-in duration-500", "opacity-0", "opacity-100"}, time: 500)
            }
          >
Hello
</div>

to this:

 <style>
      @keyframes fadein {
        from { opacity: 0.2; }
        to { opacity: 1.0; }
      }
      .fadein { animation: fadein 1 1000ms linear; }
 </style>
 <div :if={@show1} id="11" class="bg-red-500 fadein">HELLO ANIM 1</div>

?

I personally find the first solution simpler and less hacky to be honest. And in general, if I can do something without having to write any CSS, I’m happier :slight_smile:

2 Likes