2

In my Vue app I have a simple toggle which renders a list when active. The list should not appear/disappear instantly. I want to have a smooth slide down transition on render and a smooth slide up transition on hide.

The following code shows what I have so far, does someone know how to make it work?

new Vue({
  el: '#app',
  data: () => ({
    isOpen: true,
  }),
});
.expand-enter-active,
.expand-leave-active {
  overflow: hidden;
  transition: height .5s linear;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <button @click="isOpen = !isOpen">
    is open: {{ isOpen }}
  </button>
  <transition name="expand">
    <div v-if="isOpen">
      <div>1</div>
      <div>2</div>
      <div>3</div>
    </div>
  </transition>
</div>
Question3r
  • 2,166
  • 19
  • 100
  • 200

3 Answers3

4

Please take a look this solution.

Vue.component('transition-collapse-height', {
  template: `<transition
    enter-active-class="enter-active"
    leave-active-class="leave-active"
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @before-leave="beforeLeave"
    @leave="leave"
    @after-leave="afterLeave"
  >
    <slot />
  </transition>`,
  methods: {
    /**
     * @param {HTMLElement} element
     */
    beforeEnter(element) {
      requestAnimationFrame(() => {
        if (!element.style.height) {
          element.style.height = '0px';
        }

        element.style.display = null;
      });
    },
    /**
     * @param {HTMLElement} element
     */
    enter(element) {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          element.style.height = `${element.scrollHeight}px`;
        });
      });
    },
    /**
     * @param {HTMLElement} element
     */
    afterEnter(element) {
      element.style.height = null;
    },
    /**
     * @param {HTMLElement} element
     */
    beforeLeave(element) {
      requestAnimationFrame(() => {
        if (!element.style.height) {
          element.style.height = `${element.offsetHeight}px`;
        }
      });
    },
    /**
     * @param {HTMLElement} element
     */
    leave(element) {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          element.style.height = '0px';
        });
      });
    },
    /**
     * @param {HTMLElement} element
     */
    afterLeave(element) {
      element.style.height = null;
    },
  },
});

new Vue({
  el: '#app',
  data: () => ({
    isOpen: true,
  }),
});
.enter-active,
.leave-active {
  overflow: hidden;
  transition: height .5s linear;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <button @click="isOpen = !isOpen">
    is open: {{ isOpen }}
  </button>
  <transition-collapse-height>
    <div v-show="isOpen">
      <div>1</div>
      <div>2</div>
      <div>3</div>
    </div>
  </transition-collapse-height>
</div>
  • 2
    hey, thanks for your reply. Isn't this a little bit overkill? :) Was hoping to find a little CSS solution. I updated my question – Question3r Oct 15 '20 at 20:59
  • 1
    If you define transition-collapse-height component, you can use this one in other code. I think it's good solution. How do you think of it. –  Oct 15 '20 at 21:18
  • I'm choosing this as the answer because it's the most dynamic one :) – Question3r Oct 16 '20 at 05:43
3

use max-height for animation because we couldn't animate height prop according to this answer

new Vue({
  el: '#app',
  data: () => ({
    isOpen: false,
  }),
});
.expand-enter-active,
.expand-leave-active {
  transition: max-height .5s ease;
  max-height: 400px;
}

.expand-enter,
.expand-leave-to {
  max-height: 0;
  overflow: hidden;
}

.list {
  box-shadow: 1px 0 10px #aaa;
  padding: 8px;
  width: 64px;
  overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <button @click="isOpen = !isOpen">
    Open
  </button>
  <transition name="expand">
    <div v-if="isOpen" class="list">
      <div>1</div>
      <div>2</div>
      <div>3</div>
      <div>4</div>
      <div>5</div>
    </div>
  </transition>
</div>
Boussadjra Brahim
  • 82,684
  • 19
  • 144
  • 164
  • hey, thanks for your reply! Sorry, I'm not using Vue3 yet. This is a Vue2 app. I updated my code snippet in the question – Question3r Oct 15 '20 at 21:00
  • @BoussadjraBrahim Can you please check your code again? I don't think it's working well. –  Oct 15 '20 at 21:19
  • Yeah, i deleted `to` from `.expand-leave-to` accidentally, please check it again – Boussadjra Brahim Oct 15 '20 at 21:37
  • thanks for your solution. Unfortunately there are some hardcoded values in the css. I think this wasn't dynamic enough (when dealing with a loop generating a list of elements) – Question3r Oct 16 '20 at 05:44
1

You can use keyframe for the enter/mount animation and then set height to 0 while unmount. If the height is not known, you can use max-height instead of height.

Vue.createApp({
  data() {
    return {
      isOpen: false
    };
  }
}).mount('#app')
.expand {
    height: 50px;
    animation: slideDown 1s linear;
    overflow: hidden;
}

.expand-leave-active.expand-leave-to {
    transition: height 1s ease;
    height: 0;
}

@keyframes slideDown{
    from {
        height: 0;
    }
    to {
        height: 50px;
    }
}
<script src="https://unpkg.com/vue@next"></script>

<div id="app">
  <button @click="isOpen = !isOpen">
    is open: {{ isOpen }}
  </button>
  <transition name="expand">
    <div v-if="isOpen" class="expand">
      <div>1</div>
      <div>2</div>
      <div>3</div>
    </div>
  </transition>
</div>
Ashish
  • 4,206
  • 16
  • 45