[Vuejs]-Vuetify Data Table Slide Transition

0👍

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>

Leave a comment