Velocity-animate for large staggered lists in Vue

504 Views Asked by At

I have a large list that I'm introducing velocity-animate into. At the moment, my test list has 59 individual items that a user can filter by search query (potentially could grow to hundreds of items). I am animating currently according to the way the Vue docs show it being done, but of course their list only has a few items.

In my app, it takes several seconds for the animation to finish which is definitely not ideal. Is there any way of "batch" animating? I think ideally what would happen is I would debounce the input so that only after a second or two, will the search actually perform. Then in the method, the items are filtered, and all of the items that aren't a match disappear. How can I accomplish this?

<template>
  <v-text-field
    background-color="#fff"
    clearable
    dense
    flat
    hide-details
    label="Search"
    placeholder="Search"
    solo-inverted
    :value="searchQuery"
    @click:append="clearSearch"
    @input="debounceSearchQuery" />
  <v-list>
    <v-list-item-group>
      <draggable
        class="drag-area list-group"
        :group="{ name: 'items', pull: 'clone', put: false}"
        handle=".gear-handle"
        :list="filteredItems"
        :sort="false"
        @change="log">
          <transition-group
            :css="false"
            name="staggered-fade"
            @before-enter="beforeEnter"
            @enter="enter"
            @leave="leave">
            <v-list-item
              v-for="(item, i) in filteredItems"
              :key="item.id"
              :data-index="i">
              <p class="mb-0 white--text">
                {{ item.name }}
              </p>
            </v-list-item>
          </transition-group>
        </draggable>
      </v-list-item-group>
    </v-list>
</template>

export default {
  data: () => ({
    searchQuery: ''
  }),

  computed: {
    filteredItems () {
      const filteredItems = [];
      this.categories.forEach(category => {
        if (this.searchQuery) {
          const text = this.searchQuery.toLowerCase();
          filteredItems.push(category.items.filter(item => item.name && item.name.toLowerCase().includes(text)));
        } else {
          filteredItems.push(category.items);
        }
      });
      return filteredItems.flat();
    },
  },

  methods: {
  // gsap methods //
    beforeEnter (el) {
      el.style.opacity = 0;
      el.style.height = 0;
    },
    enter (el, done) {
      const delay = el.dataset.index * 150;
      setTimeout(() => {
        this.$velocity(
          el,
          { opacity: 1, height: '1.6em', duration: 50 },
          [0.57, 0.06, 0, 1.06],
          { complete: done }
        );
      }, delay);
    },
    leave (el, done) {
      const delay = el.dataset.index * 150;
      setTimeout(() => {
        this.$velocity(
          el,
          { opacity: 0, height: 0 },
          { complete: done }
        );
      }, delay);
    },
    // end gsap methods
    debounceSearchQuery: debounce(function (e) {
      this.searchQuery = e;
    }, 1000)
  }
}
1

There are 1 best solutions below

2
Yom T. On BEST ANSWER

I think "staggering" animation and "batch" running transitions are more like two different things—it's almost like you can't have your cake and eat it too.

Nonetheless, if you are simply after smooth, sliding transitions that kind of have staggering effect, you could actually do that with <transition-group> + CSS only (no transition hooks necessary). Here's a quick example (click "Run" and then "Full-page" for better view):

new Vue({
  vuetify: new Vuetify({
    theme: {
      dark: true
    }
  }),

  data: () => ({
    searchQuery: '',
    categories: [
      { items: [{ name: 'vue.js', id: 1 }] },
      { items: [{ name: 'react.js', id: 2 }] },
      { items: [{ name: 'angular.js', id: 3 }] },
      { items: [{ name: 'velocity-animate', id: 4 }] },
      { items: [{ name: 'lodash', id: 5 }] },
      { items: [{ name: 'debounce', id: 6 }] },
    ]
  }),

  computed: {
    filteredItems() {
      const filteredItems = [];
      const text = this.searchQuery.toLowerCase();

      this.categories.forEach(category => {
        if (this.searchQuery) {
          filteredItems.push(category.items.filter(item =>
            item.name && item.name && item.name.toLowerCase().includes(text)));
        } 
        else {
          filteredItems.push(category.items);
        }
      });
      return filteredItems.flat();
    },
  },

  methods: {
    debounceSearchQuery: _.debounce(function(e) {
      this.searchQuery = e;
    }, 200),
    
    clearSearch() {
      this.searchQuery = '';
    }
  }
}).$mount('#app');
.staggered-fade-item {
  transition-timing-function: cubic-bezier(0.57, 0.06, 0, 1.06);
  transition-duration: 500ms;
  transition-property: opacity, transform;
}

.staggered-fade-enter,
.staggered-fade-leave-to {
  opacity: 0;
  transform: translateY(-30px);
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.css" rel="stylesheet">

<div id="app">
  <v-app>
    <v-main>
      <v-container>
        <v-text-field
          @click:append="clearSearch"
          @input="debounceSearchQuery"
          :value="searchQuery"
          label="Search"
          placeholder="Search"
          autocomplete="off"
          solo-inverted clearable dense flat hide-details autofocus dark>
        </v-text-field>

        <v-list>
          <transition-group name="staggered-fade" tag="v-list-item-group">
            <v-list-item 
              v-for="item in filteredItems" :key="item.id"
              class="staggered-fade-item">
              <p class="mb-0 white--text">
                {{ item.name }}
              </p>
            </v-list-item>
          </transition-group>
        </v-list>
      </v-container>
    </v-main>
  </v-app>
</div>

Another recommendation would be Velocity UI pack. This add-on allows you to do sequence running and it comes with some pre-registered effects, which are great for staggering effects. (Check out the docs for more transition effects).

new Vue({
  vuetify: new Vuetify({
    theme: {
      dark: true
    }
  }),

  data: () => ({
    searchQuery: '',
    categories: [
      { items: [{ name: 'vue.js', id: 1 }] },
      { items: [{ name: 'react.js', id: 2 }] },
      { items: [{ name: 'angular.js', id: 3 }] },
      { items: [{ name: 'velocity-animate', id: 4 }] },
      { items: [{ name: 'lodash', id: 5 }] },
      { items: [{ name: 'debounce', id: 6 }] },
    ]
  }),

  computed: {
    filteredItems() {
      const filteredItems = [];
      const text = this.searchQuery.toLowerCase();

      this.categories.forEach(category => {
        if (this.searchQuery) {
          filteredItems.push(category.items.filter(item =>
            item.name && item.name && item.name.toLowerCase().includes(text)));
        } 
        else {
          filteredItems.push(category.items);
        }
      });
      return filteredItems.flat();
    },
  },

  methods: {
    debounceSearchQuery: _.debounce(function(e) {
      this.searchQuery = e;
    }, 200),
    
    clearSearch() {
      this.searchQuery = '';
    }
  },
  
  watch: {
    filteredItems() {
      Velocity(
        this.$refs.items.map(vnode => vnode.$el),
        'transition.flipYIn', {
          stagger: 100,
          drag: true
        }
      );
    }
  }
}).$mount('#app');
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/velocity.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/velocity.ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuetify.min.css" rel="stylesheet">

<div id="app">
  <v-app>
    <v-main>
      <v-container>
        <v-text-field
          @click:append="clearSearch"
          @input="debounceSearchQuery"
          :value="searchQuery"
          label="Search"
          placeholder="Search"
          autocomplete="off"
          solo-inverted clearable dense flat hide-details autofocus dark>
        </v-text-field>

        <v-list>
          <v-list-item-group>
            <v-list-item 
              v-for="item in filteredItems" :key="item.id"
              ref="items">
              <p class="mb-0 white--text">
                {{ item.name }}
              </p>
            </v-list-item>
          </v-list-item-group>
        </v-list>
      </v-container>
    </v-main>
  </v-app>
</div>