'Vuetify Data Table Slide Transition

I'm trying to animate the data on a Vuetify data table. My goal is to have the current data slide out to the right when Next is clicked, and have the new data slide in from the left.

Here is my current result: data table slide

As you can see, the current data is sliding out, but the new data is appearing at the bottom and sliding up when the old data is out.

Here's my code:

<v-data-table 
    :headers="headers"
    :items="scoreResults"
    disable-sort
    hide-default-footer
    :mobile-breakpoint="0"
  >
  <template v-slot:body="{ items }">
    <tbody name="list" is="transition-group">
      <tr v-for="item in items" :key="item.id" class="item-row">
        <td>{{item.player}}</td>
        <td>{{item.total_score}}</td>
        <td><v-chip
          :dark="item.points > 5 ? true : false"
          :color="getColor(item.points)"
          >+{{ item.points }}</v-chip>
        </td>
        <td><em>{{item.guess}}</em></td>
      </tr>
    </tbody>
  </template>
</v-data-table>

and CSS

.list-enter-active,
.list-leave-active {
    transition: all 0.8s;
}

.list-enter {
    opacity: 0;
    transform: translateX(10%);
}

.list-leave-to {
    opacity: 0;
    transform: translateX(100%);
}

.list-move {
    transition: transform 0.5s;
}

.item-row {
    display: table-row;
}

How do I get the new data to appear from the left, and slide in as the old data leaves?



Solution 1:[1]

The problem is the current table rows get added to the table when old rows are leaving. To solve that I used translateY to move the rows. There are still some issues with some transitions ending, while others start.

const root = document.documentElement;
const TRANSITION_DURATION = 800;

// Copied From https://www.joshwcomeau.com/snippets/javascript/debounce/
const debounce = (callback, wait) => {
  let timeoutId = null;
  return (...args) => {
    window.clearTimeout(timeoutId);
    timeoutId = window.setTimeout(() => {
      callback.apply(null, args);
    }, wait);
  };
}

new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  created() {
    this.onAfterLeave = debounce(this.onAfterLeave, TRANSITION_DURATION);
  },
  computed : {
    totalNumItems() {
      return this.desserts.length;
    }
  },
  mounted() {
    this.tableEl = this.$refs.dataTable.$el.querySelector('table');
  },
  methods: {
    onPagination() {
      this.lastTransitionTime = Date.now();
      if(!this.tableEl 
         || this.isUpdatingItems 
         ||this.isTransitioning) {
        this.isUpdatingItems = false;
        return;
      }
      this.isTransitioning = true;
      this.tableHeight = this.tableEl.offsetHeight;
    },
    onUpdateItems(numItems) {
       let cssNumItems = numItems;
       this.isUpdatingItems = true;
      
       if (numItems === this.totalNumItems) {
         cssNumItems = 0;
       }
      
       root.style.setProperty('--num-items', cssNumItems);
    },
    onAfterLeave(el) {
       if (Date.now() - this.lastTransitionTime < TRANSITION_DURATION) {
          window.setTimeout(this.onAfterLeave.bind(this, el), TRANSITION_DURATION);
       } else {
          this.isTransitioning = false;
          this.tableHeight = 'unset';
       }
    }
  },
  data () {
    return {
      search: '',
      msg: 'No Results Here!',
      tableHeight: 'unset',
      lastTransitionTime: 0,
      numItems: 5,
      headers: [
        {
          text: 'Dessert (100g serving)',
          align: 'start',
          value: 'name',
        },
        { text: 'Calories', value: 'calories' },
        { text: 'Fat (g)', value: 'fat' },
        { text: 'Carbs (g)', value: 'carbs' },
        { text: 'Protein (g)', value: 'protein' },
        { text: 'Iron (%)', value: 'iron' },
      ],
      desserts: [
        {
          name: 'Frozen Yogurt',
          calories: 159,
          fat: 6.0,
          carbs: 24,
          protein: 4.0,
          iron: '1%',
        },
        {
          name: 'Ice cream sandwich',
          calories: 237,
          fat: 9.0,
          carbs: 37,
          protein: 4.3,
          iron: '1%',
        },
        {
          name: 'Eclair',
          calories: 262,
          fat: 16.0,
          carbs: 23,
          protein: 6.0,
          iron: '7%',
        },
        {
          name: 'Cupcake',
          calories: 305,
          fat: 3.7,
          carbs: 67,
          protein: 4.3,
          iron: '8%',
        },
        {
          name: 'Gingerbread',
          calories: 356,
          fat: 16.0,
          carbs: 49,
          protein: 3.9,
          iron: '16%',
        },
        {
          name: 'Jelly bean',
          calories: 375,
          fat: 0.0,
          carbs: 94,
          protein: 0.0,
          iron: '0%',
        },
        {
          name: 'Lollipop',
          calories: 392,
          fat: 0.2,
          carbs: 98,
          protein: 0,
          iron: '2%',
        },
        {
          name: 'Honeycomb',
          calories: 408,
          fat: 3.2,
          carbs: 87,
          protein: 6.5,
          iron: '45%',
        },
        {
          name: 'Donut',
          calories: 452,
          fat: 25.0,
          carbs: 51,
          protein: 4.9,
          iron: '22%',
        },
        {
          name: 'KitKat',
          calories: 518,
          fat: 26.0,
          carbs: 65,
          protein: 7,
          iron: '6%',
        },
      ],
    }
  },
})
:root {
  --num-items: 5; 
  --row-y: calc(var(--num-items) * -100%);
}

#app {
  padding: 20px;
}

table,
.v-data-table__wrapper {
  overflow: hidden !important;
}

@keyframes slide-in {
  0% {
    opacity: 0;
    transform: translateX(-100%) translateY(var(--row-y));
  }
  100% {
    opacity: 1;
    transform: translateX(0) translateY(var(--row-y));
  }
}

.list-enter-to {
  animation: slide-in .8s;
}

.list-leave-active {
  opacity: 1;
  transform: translateX(0);
  transition: all .7s .1s;
}

.list-leave-to {
  opacity: 0;
  transform: translateX(100%);
}
<div id="app">
   <v-app id="inspire">
      <v-card>
         <v-card-title>
            <v-text-field v-model="search" 
                          append-icon="search" 
                          label="Search" 
                          single-line hide-details>
          </v-text-field>
         </v-card-title>
         <v-data-table :headers="headers" 
                       :height="tableHeight"
                       :items="desserts" 
                       :search="search" 
                       :items-per-page="5"
                       item-key="name"
                       @pagination="onPagination"
                       @update:items-per-page="onUpdateItems"
                       ref="dataTable" >
            <template v-slot:body="{items, headers}">
               <tbody name="list" 
                      is="transition-group"
                      v-on:after-leave="onAfterLeave">
                  <tr v-for="item in items" 
                     :key="item.name" 
                     class="item-row">
                     <td>{{item.name}}</td>
                     <td>{{item.calories}}</td>
                     <td>{{item.fat}}</td>
                     <td>{{item.carbs}}</td>
                     <td>{{item.protein}}</td>
                     <td>{{item.iron}}</td>
                  </tr>
               </tbody>
            </template>
         </v-data-table>
      </v-card>
   </v-app>
</div>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 first last