Search code examples
javascriptcssvue.jscss-transitionstransition

VueJS JS-only transition hook requires setTimeout for CSS transition to work


I'm working with pure JS hooks for the <transition-group> element on VueJS, and I am quite puzzled on how the enter hook actually works. Based on the documentation, I understand that I will have to call done() to avoid events being called synchronously:

When using JavaScript-only transitions, the done callbacks are required for the enter and leave hooks. Otherwise, the hooks will be called synchronously and the transition will finish immediately.

However, even when I use it, it seems to stop CSS transitions from happening in the entering transition. The only solution I found is to use window.setTimeout to set the style, which I think is a code smell. Here is a quick visual comparison between the code without the timeout, and the one with (the one with the timeout is the desired effect):

Broken enter transition (no transition of the left padding and opacity):

Broken enter transition

Desired enter transition:

Desired effect

In the example below, I am displaying a list using <transition-group> and wanted to use JS-hooks so that I can create staggered paddings on individual list items. It seems to work with the exception that in the enter transition, the CSS transitions on the padding property does not work.

new Vue({
  el: '#app',
  data: {
    items: [
      'Lorem',
      'Ipsum',
      'Dolor',
      'Sit',
      'Amet'
    ],
    toggle: false
  },
  computed: {
    filteredItems: function() {
      if (!this.toggle)
        return [];

      return this.items;
    }
  },
  methods: {
    toggleItems: function() {
      this.toggle = !this.toggle;
    },
    beforeEnter: function(el) {
      el.style.paddingLeft = '0px';
      el.style.opacity = '0';
    },
    enter: function(el, done) {
      el.style.paddingLeft = `${10 * +el.dataset.index}px`;
      el.style.opacity = '1';
      done();
    },
    beforeLeave: function(el) {
      el.style.paddingLeft = '0px';
      el.style.opacity = '0';
    }
  }
})
ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

ul li {
  transition: all 500ms ease-in-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">

  <button @click="toggleItems">
    Toggle items
  </button>
  
  <transition-group
    tag="ul"
    @before-enter="beforeEnter"
    @enter="enter"
    @before-leave="beforeLeave">
    
    <li
      v-for="(item, i) in filteredItems"
      v-bind:key="i"
      v-bind:data-index="i">
      {{ item }}
    </li>
  </transition-group>
</div>

If you wrap all the logic inside the enter method inside an arbitrary timout, then it works:

enter: function(el, done) {
  window.setTimeout(() => {
    el.style.paddingLeft = `${10 * +el.dataset.index}px`;
    el.style.opacity = '1';
    done();
  }, 100);
},

And this is where I am a little confused: does the enter hook not wait for beforeEnter to complete first? The working snippet is as follow


Solution

  • Changing the @enter hook to @after-enter should fix it

    I have no clue why the @enter hook isn't working for this, as looking at the documentation it should but this should at least get rid of the timeout without being a hack

    new Vue({
      el: '#app',
      data: {
        items: [
          'Lorem',
          'Ipsum',
          'Dolor',
          'Sit',
          'Amet'
        ],
        toggle: false
      },
      computed: {
        filteredItems: function() {
          if (!this.toggle)
            return [];
    
          return this.items;
        }
      },
      methods: {
        toggleItems: function() {
          this.toggle = !this.toggle;
        },
        beforeEnter: function(el) {
          el.style.paddingLeft = '0px';
          el.style.opacity = '0';
        },
        afterEnter: function(el) {
          el.style.paddingLeft = `${10 * +el.dataset.index}px`;
          el.style.opacity = '1';
        },
        beforeLeave: function(el) {
          el.style.paddingLeft = '0px';
          el.style.opacity = '0';
        }
      }
    })
    ul {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    
    ul li {
      transition: all 500ms ease-in-out;
    }
    
    li.v-enter-active {
      transition: none
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
    <div id="app">
    
      <button @click="toggleItems">
        Toggle items
      </button>
    
      <transition-group 
        tag="ul" 
        @before-enter="beforeEnter"
        @after-enter="afterEnter" 
        @before-leave="beforeLeave">
        <li v-for="(item, i) in filteredItems" v-bind:key="i" v-bind:data-index="i">
          {{ item }}
        </li>
      </transition-group>
    </div>

    As a side note, if you're using SCSS or SASS, you can achieve this with that rather than JavaScript