Skip to content

How to animate list items in Vue when one is removed

i have a vertical list of items, each of which can be removed. I put my items inside a transition-group and created simple opacity and transform transitions for them. The transitions on the removed elements work as expected, however if I remove an element which is not placed at the bottom, the ones beneath just jump up and take its place without any transition. I Can’t find a way to target this behaviour. All I want is just that the elements below slide up smoothly.

Is there any way to achieve this effect by using css transitipms and Vue’s animation hooks?

Here is a demo: https://jsfiddle.net/gcp18nq0/

Template:

<div id="app">
 <div class="form">
  <label for="name">Name</label>
  <input type="text" id="name" v-model="name">
  <button @click="addPlayer">Add player</button>
 </div>
 <div class="players">
  <transition-group name="player">
   <div class="panel" v-for="player in players" :key="player.id">
    <h2>
      {{ player.name}}
      <span class="remove" @click="removePlayer(player.id)">Remove</span>
    </h2>
   </div>
  </transition-group>
 </div>
</div>

Script:

  data() {
    return {
     name: "",
     players: [
       {id: 1, name: 'Player1'},
       {id: 2, name: 'Player2'},
       {id: 3, name: 'Player3'},
     ]
    }
  },
  methods: {
            addPlayer: function () {
                  //,,,,
                },
            removePlayer: function (playerId) {
                   //...
            }
         }
  });

CSS

.form {
  margin:0 auto;
  width:400px;
}
.panel {
  width: 400px;
  margin: 10px auto;
  overflow: hidden;
  border: 1px solid;
  text-align: center;
}

.remove {
  float: right;
  cursor: pointer;
  text-decoration: underline;
  font-size: 12px;
  vertical-align: bottom
}

.player-enter,
.player-leave-to
/* .fade-leave-active below version 2.1.8 */

  {
  opacity: 0;
}

.player-enter {
  transform: translateY(30%);
}

.player-leave-to {
  transform: translateX(30%);
}

.player-enter-active,
.player-leave-active {
  transition: all 1.5s;
}

.player-move {
  transition: all 1.5s;
}

The only working way I found was by adding position:absolute on “player-leave-active” state but since the element collapses it changes its vertical position, which is not the desired effect. I also tried changing the height but there the elements below still jump up a bit after the height is set to 0. Im sure that this can be achieved easily with jQuery but i believe that there should be a way to do it without js.

Thank you in advance!

p.s. its my first post here, so i hope that it was explained clearly enough.

Answer

So I made some small tweaks to your fiddle: https://jsfiddle.net/gcp18nq0/1/ and hopefully that is what you looking for.

The most important change was setting display: inline-block on the .panel class, according to the Vue documentation:

One important note is that these FLIP transitions do not work with elements set to display: inline. As an alternative, you can use display: inline-block or place elements in a flex context.

new Vue({
  el: "#app",
  data() {
    return {
      name: "",
      players: [{
          id: 1,
          name: 'Batman'
        },
        {
          id: 2,
          name: 'Robin'
        },
        {
          id: 3,
          name: 'Superman'
        },
        {
          id: 4,
          name: 'Spiderman'
        },
      ]
    }
  },
  methods: {
    addPlayer: function() {
      const newPlayer = {
        id: this.players.length + 1,
        name: this.name,
      };
      this.players.push(newPlayer);
    },
    deletePlayer: function(playerId) {
      let playerToRemove = this.players.find((player) => {
        return player.id === playerId;
      });

      let playerIndex = this.players.indexOf(playerToRemove);
      this.players.splice(playerIndex, 1);
    }
  }
});
.form {
  margin: 0 auto;
  width: 400px;
}

.panel {
  width: 400px;
  margin: 6px auto;
  overflow: hidden;
  border: 1px solid;
  text-align: center;
  transition: all 1s;
  display: inline-block;
}

.players {
  position: relative;
  text-align: center;
}

.remove {
  float: right;
  cursor: pointer;
  text-decoration: underline;
  font-size: 12px;
  vertical-align: bottom
}

.player-enter,
.player-leave-to {
  opacity: 0;
}

.player-enter {
  transform: translateY(30%);
}

.player-leave-to {
  transform: translateX(300%);
}

.player-leave-active {
  position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
  <div class="form">
    <label for="name">Name</label>
    <input type="text" id="name" v-model="name">
    <button @click="addPlayer">Add player</button>
  </div>
  <div class="players">
    <transition-group name="player" tag="div">
      <div class="panel" v-for="player in players" :key="player.id">
        <h2>
          {{ player.name}}
          <span class="remove" @click="deletePlayer(player.id)">Remove</span>
        </h2>
      </div>
    </transition-group>
  </div>
</div>