LiveView and enter / leave transitions?

I’m trying to implement some TailwindUI components using LiveView and some of these specify enter / leave transitions to be implemented in JS. As an example, if you go here and view the code you’ll see comments such as:

<!--
  'Solutions' flyout menu, show/hide based on flyout menu state.

  Entering: "transition ease-out duration-200"
    From: "opacity-0 translate-y-1"
    To: "opacity-100 translate-y-0"
  Leaving: "transition ease-in duration-150"
    From: "opacity-100 translate-y-0"
    To: "opacity-0 translate-y-1"
-->

(some background on why this can’t be done with CSS alone can be found here)

React, Vue and other frameworks have built-in support for such transitions, for example: https://vuejs.org/v2/guide/transitions.html#Transition-Classes but it doesn’t appear that LiveView currently does, and I haven’t found an issue about this.

I was wondering if anyone has found a way to implement enter / leave transitions with LiveView. Thanks.

7 Likes

Do you mean transitions onmouseenter/onmouseout?

If so, these are not currently supported in LiveView yet. I hope this will change, c.f. Phoenix LiveView request/proposal: mouse events

1 Like

Thanks, but the issue is also about the ability to change a property once the transition has completed. Here’s some further illustration from Enter & leave transitions | Sebastian De Deyne :

Broadly speaking, there are two ways to implement enter & leave transitions:

  • Just use the transition CSS properties
  • Animations with JavaScript (either by modifying CSS properties, with the web animation API, or with third-party animation libraries)

Unfortunately, transition doesn’t cut it. Here’s what we need for our dropdown list transition:

  • When the dropdown opens, set it’s display property to block , then set transition opacity from 0 to 1
  • When the dropdown closes, transition opacity from 1 to 0 , then set it’s display property to none

We can’t build this with CSS transitions alone, because there’s no way to change the display value before or after the transition happened. I don’t like JavaScript animations for these things in JavaScript either, because it couples your JavaScript to CSS and vice-versa.

This is the scenario supported explicitly by Vue, React and other frameworks.

Actually I just noticed that you asked a similar question here: LiveView: Are there any clever techniques to do 'removal' animations?

3 Likes

I would also love to figure out how to do that without getting into Liveview’s hair :slightly_smiling_face:

My initial hope was to be able to use Alpine.js, as outlined here, but Alpine and Liveview appear to be incompatible as discussed here.

Have bumped into the same article you mentioned also and been studying it over the last couple days to see if this might be the way forward. But I’m not very experienced with javascript, and it’s a bit slow going :slight_smile:

Hoping to get it to work in conjunction with Liveview’s phx-update="ignore"

doesn’t css keyframes work for these multi step animations? - not sure I agree with the article but I’m certainly no css/js expert at all…

you should be able to adapt below code to show/hide that nav thing in tailwind…

if you take the phoenix image example:

add this above the form in the render in the component:
<p class="my-class <%= @bg %>">updated</p><br><br>

then the css:

.my-class.blue
{
    animation: fadeInFromNone 2.5s ease-out;
}
.my-class.black
{
    animation-fill-mode: forwards !important;
    animation: fadeOut 2.5s ease-out;
}

@keyframes fadeInFromNone {
    0% {
        display: none;
        opacity: 0;
    }

    1% {
        display: block;
        opacity: 0;
    }

    100% {
        display: block;
        opacity: 1;
    }
}

@keyframes fadeOut {
    0% {
        opacity: 1;
    }

    99% {
        opacity: 0;
    }

    100% {
        display: none;
        opacity: 0;
    }
}
2 Likes

had some spare time:
add tailwindcss and ui to your app
copy the html from https://tailwindui.com/components/marketing/elements/headers
put a phx-click on the more button, and add my_more_nav <%= @more_status %> to the div class

        <div class="relative">
          <!-- Item active: "text-gray-900", Item inactive: "text-gray-500" -->
          <button  phx-click="toggle_more" type="button" class="text-gray-500 inline-flex items-center space-x-2 text-base leading-6 font-medium hover:text-gray-900 focus:outline-none focus:text-gray-900 transition ease-in-out duration-150">
            <span>More</span>
            <!-- Item active: "text-gray-600", Item inactive: "text-gray-400" -->
            <svg class="text-gray-400 h-5 w-5 group-hover:text-gray-500 group-focus:text-gray-500 transition ease-in-out duration-150" fill="currentColor" viewBox="0 0 20 20">
              <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
            </svg>
          </button>

          <!--
            'More' flyout menu, show/hide based on flyout menu state.

            Entering: "transition ease-out duration-200"
              From: "opacity-0 translate-y-1"
              To: "opacity-100 translate-y-0"
            Leaving: "transition ease-in duration-150"
              From: "opacity-100 translate-y-0"
              To: "opacity-0 translate-y-1"
          -->
          <div class="my_more_nav <%= @more_status %> absolute left-1/2 transform -translate-x-1/2 mt-3 px-2 w-screen max-w-md sm:px-0">

css is: (probably some unneeded !important and what not)

div.my_more_nav:not(.show_more):not(.hide_more) {
  display: none;
}

div.show_more
{
    animation-fill-mode: forwards !important;
    animation: fadeInFromNone 0.2s ease-out;
}



div.hide_more
{ 

    animation-fill-mode: forwards !important;
    animation: fadeOut 0.15s ease-in;
}

@keyframes fadeInFromNone {
    0% {
        display: none;
        opacity: 0;
       transform: translateY(4px) translateX(-50%);
        
    }

    1% {
        display: block;
        opacity: 0;
        transform: translateY(4px) translateX(-50%);
    }

    100% {
        display: block !important;
        opacity: 1;
       transform: translateY(0px) translateX(-50%);
    }
}

@keyframes fadeOut {
    0% {
        display: block;
        opacity: 1;
        transform: translateY(0px) translateX(-50%);
    }

    99% {
        opacity: 0;
        transform: translateY(4px) translateX(-50%);
    }

    100% {
        display: none;
        opacity: 0;
        transform: translateY(4px) translateX(-50%);
    }
}

then in the liveview mount assign more_status: "" on the socket…

and have a toggle_more:

  @impl true
  def handle_event("toggle_more", _query, socket) do

   value = if socket.assigns.more_status == "show_more" do
       "hide_more"
     else
      "show_more"
   end
   {:noreply,
         socket
         |> assign(results: %{}, more_status: value)}
  end

most likely I would handle navbar clicks client side (do a phx-hook and addeventlistener on click) - I would also underlay the entire div - and catch clicks outside the navbar thing…

4 Likes

@outlog thanks so much! I’ll give it a try.

:wave:

I think doing it the same way as it is done in alpine.js would work fine in live view: https://github.com/alpinejs/alpine/blob/master/src/utils.js#L132-L379

@outlog: Thanks a ton, this looks great! Working through it now…

@idi527: Thanks also. I have looked at the way alpine.js does it, but it’s a bit over my head in terms of js :blush:

1 Like

Just to help me understand: Did you mean that you would have a full screen underlay (transparent), so that if somebody clicked outside of the open menu (navbar element), you’d be able to close it again?

Edit: Oops, meant to reply to @outlog, apologies…

exactly - all depends on your UI/UX preferences of course…

Great, thank you @outlog!

I’m currently using alpine.js with phx-update="ignore" - While not ideal, I don’t want to create anything more complex until a more robust solution presents itself.

There is this article about combining alpine.js with Liveview, but I didn’t find the proposed solution to work properly, and it still got wound up in Liveview updates unless you added the phx-update="ignore" property.

For simple things like menu popovers and mobile navigation hamburgers, it feels a bit complex to be wiring state to and from a Liveview. I also didn’t like the idea of having to wire up views to catch taps outside the controls, to dismiss them.

I’ll see how it goes using Liveview with a sprinkle of alpine. Ideally, it would be amazing if Liveview had alpine.js style markup for simple show/hide view transitions.

In fact, there’s been some promising input by the creator of alpine.js on Liveview integration

4 Likes

LiveView added support for AlpineJS in 0.13.3. It works great, with no phx-update="ignore" necessary. I wrote about it here:

9 Likes

Very nice blog post! Really looking forward to the next one showing transitions with LV, AlpineJS, and Tailwind CSS.

I have been following Slack and I think Chris went over a high level near term roadmap for LV in his ElixirConf EU V 2020 video a couple weeks ago. I can’t find the video posted yet on YouTube but does anyone know if there is a plan for native LV transition support without AlpineJS? Alpine integration is great for many usecases but if simple transitions will be natively supported then it probably makes sense for me to wait and not add excess complexity for that yet. Does anyone know if the video is posted?

2 Likes

Fantastic write-up. Very in-depth. TBH, approaching alpine and seeing it on Tailwind website I was expecting a simple system to just manage transitions and visual appearance, but this looks much fuller. Looking forward to write-up covering show / hide and transitions. Excellent work!

1 Like

I just sent an email asking about the video. Not sure if they will be charging for them because it was done virtually online?

1 Like

It is unlikely, in fact, to charge a fee for such a letter should not.

1 Like

Thanks…free is good! :slight_smile: