Search code examples
javascriptcssvue.js

How to animate the sorting of a list with Vue.js


I’m trying to animate the sorting of a list with Vue.js, but not all items are animated. Do you know why? And how to make it work?

new Vue({
  el: '#app',
  data: {
    reverse: 1,
    items: [
      { name: 'Foo' },
      { name: 'Bar' },
      { name: 'Baz' },
      { name: 'Qux' }
    ]
  }
})
.moving-item {
  transition: all 1s ease;
  -webkit-transition: all 1s ease;
}
ul {
  list-style-type: none;
  padding: 0;
  position: relative;
}
li {
  position: absolute;
  border: 1px solid #42b983;
  height: 20px;
  width: 150px;
  padding: 5px;
  margin-bottom: 5px;
  color: #42b983;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.0-alpha.2/vue.min.js"></script>
<div id="app">
  <button on-click="reverse = Math.abs(reverse-1)">
    <span v-if="reverse == 0">△</span>
    <span v-if="reverse == 1">▽</span> Order
  </button>
  <ul>
    <li class="moving-item" v-for="item in items | orderBy 'name' reverse" bind-style="{ top: ($index * 35) + 'px'}">{{ item.name }}</li>
  </ul>  
</div>


Solution

  • I believe the problem is that only one of the elements is remaining in the DOM during the sort. The other three are being removed and reinserted to satisfy the new ordering – but as a result they are not triggering an animation.

    Typically, animation is done using the Vue transition system (http://vuejs.org/guide/transitions.html). However, the same basic problem of deletion and reinsertion not tracking position state will occur using that technique. Usually, items are animated independent of their previous and new positions (like fade-out in their old position and fade-in in their new one).

    If you really need to animate from the the old position to the new one, I think you would need to write your own Javascript transition that remembers the previous position of each item before it is removed and animates it to the new position when it is inserted.

    There is an example here which should be a good starting point: http://vuejs.org/guide/transitions.html#JavaScript_Only_Transitions

    Another option is to not sort by a filter and do it in javascript instead (so that the v-for only renders once). Then target your bind-style against a new index parameter on your items like this:

    new Vue({
      el: '#app',
      data: {
        reverse: 1,
        items: [
          { name: 'Foo', position: 0 },
          { name: 'Bar', position: 1 },
          { name: 'Baz', position: 2 },
          { name: 'Qux', position: 3 }
        ]
      },
      methods: {
        changeOrder: function (event) {
          var self = this;
          self.reverse = self.reverse * -1
          var newItems = self.items.slice().sort(function (a, b) { 
            var result;
            if (a.name < b.name) {
              result = 1
            }
            else if (a.name > b.name) {
              result = -1
            }
            else {
              result = 0
            }
            return result * self.reverse
          })
          newItems.forEach(function (item, index) {
            item.position = index;
          });
        }
      }
    })
     
    .moving-item {
      transition: all 1s ease;
      -webkit-transition: all 1s ease;
    }
    ul {
      list-style-type: none;
      padding: 0;
      position: relative;
    }
    li {
      position: absolute;
      border: 1px solid #42b983;
      height: 20px;
      width: 150px;
      padding: 5px;
      margin-bottom: 5px;
      color: #42b983;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.0-alpha.2/vue.min.js"></script>
    <div id="app">
      <button on-click="changeOrder">
        <span v-if="reverse == -1">△</span>
        <span v-if="reverse == 1">▽</span> Order
      </button>
      <ul>
        <li class="moving-item" v-for="item in items" bind-style="{ top: (item.position * 35) + 'px'}">{{ item.name }}</li>
      </ul>  
    </div>