Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create infinite scroll in Vuetify Autocomplete component?

I have a page with Vuetify Autocomplete component, and REST API backend with '/vendors' method. This method takes limit, page and name parameters and returns JSON with id and name fields.

I made some code with lazy list load on user input event. But now I want to add the ability to load this list on user scroll event.

For example, by default there is a list of 100 vendors. User scrolled this list until the end, then "some event" is called and loads next 100 of vendors. Then user keeps scrolling and the action is repeated.

Is it possible to made this with Vuetify Autocomplete component, or should i use another library?

Example code of current component is shown below:

<template>
  <v-autocomplete
          :items="vendors"
          v-model="selectedVendorId"
          item-text="name"
          item-value="id"
          label="Select a vendor"
          @input.native="getVendorsFromApi"
  ></v-autocomplete>
</template>

<script>
  export default {
    data () {
      return {
        page: 0,
        limit: 100,
        selectedVendorId: null,
        vendors: [],
        loading: true
      }
    },
    created: function (){
      this.getVendorsFromApi();
    },
    methods: {
      getVendorsFromApi (event) {
        return new Promise(() => {
          this.$axios.get(this.$backendLink 
                  + '/vendors?limit=' + this.limit 
                  + '&page=' + this.page 
                  + '&name=' + (event ? event.target.value : ''))
            .then(response => {
              this.vendors = response.data;
            })
        })
      }
    }
  }
</script>
like image 863
Alexander Shkirkov Avatar asked Jun 07 '19 10:06

Alexander Shkirkov


People also ask

How to implement an infinite scroll with Vue JS?

How To Implement an Infinite Scroll with Vue.js 1 Setting Up the Project. For the purpose of this tutorial, you will build from a default Vue project generated with @vue/cli. 2 Getting Initial User Data. There are various npm packages for an infinite scroll that you can use for your Vue app. ... 3 Implementing the Infinite Scroll Logic. ...

How do I create an autocomplete component in Vue?

Now, you can use your code editor to create a new autocomplete component. This will be a single-file Vue component with a template, scripts, and styles. In order to build an autocomplete component, your template will need at least two things: an input and a list.

What is vuetify Vue?

Vuetify — A Material Design Framework for Vue.js Vuetify is a Material Design component framework for Vue.js. It aims to provide all the tools necessary to create beautiful content rich applications.

How to make vuetify list auto-loading?

I was able to get auto-loading going with the Vuetify AutoComplete component. It's a bit of a hack, but basically the solution is to use the v-slot append item, the v-intersect directive to detect if that appended item is visible, and if it is, call your API to fetch more items and append it to your current list.


1 Answers

I was able to get auto-loading going with the Vuetify AutoComplete component. It's a bit of a hack, but basically the solution is to use the v-slot append item, the v-intersect directive to detect if that appended item is visible, and if it is, call your API to fetch more items and append it to your current list.

  <v-autocomplete
          :items="vendors"
          v-model="selectedVendorId"
          item-text="name"
          item-value="id"
          label="Select a vendor"
          @input.native="getVendorsFromApi"
  >
  <template v-slot:append-item>
    <div v-intersect="endIntersect" />
  </template>
</v-autocomplete>


...

export default {
  methods: {
    endIntersect(entries, observer, isIntersecting) {
      if (isIntersecting) {
        let moreVendors = loadMoreFromApi()
        this.vendors = [ ...this.vendors, ...moreVendors]
      }
    }
  }
}

In my use case, I was using API Platform as a backend, using GraphQL pagination using a cursor.

   <component
      v-bind:is="canAdd ? 'v-combobox' : 'v-autocomplete'"
      v-model="user"
      :items="computedUsers"
      :search-input.sync="search"
      item-text="item.node.userProfile.username"
      hide-details
      rounded
      solo
      :filter="
      (item, queryText, itemText) => { 
        return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
        } "
      :loading="loading"
      item-value="username"
      class="text-left pl-1"
      color="blue-grey lighten-2"
      :label="label"
    >
      <template v-slot:selection="{ item }">
        <v-chip v-if="typeof item == 'object'">
          <v-avatar left>
            <v-img v-if="item.node.userProfile.image" :src="item.node.userProfile.image" />
            <v-icon v-else>mdi-account-circle</v-icon>
          </v-avatar>
          {{ item.node.userProfile.firstName }} {{ item.node.userProfile.lastName }}
        </v-chip>
        <v-chip v-else-if="typeof item == 'string'">
          {{ item }}
        </v-chip>
      </template>
      <template v-slot:item="{ item: { node } }">
        <v-list-item-avatar >
          <img v-if="node.userProfile.avatar" :src="node.userProfile.avatar" />
          <v-icon v-else>mdi-account-circle</v-icon>
        </v-list-item-avatar>
        <v-list-item-content class="text-left">
          <v-list-item-title>
            {{ $t('fullName', { firstName: node.userProfile.firstName, lastName: node.userProfile.lastName } )}}
          </v-list-item-title>
          <v-list-item-subtitle v-html="node.userProfile.username"></v-list-item-subtitle>
        </v-list-item-content>
      </template>
      <template v-slot:append-item="">
        <div v-intersect="endIntersect" >
        </div>
      </template>
    </component>
import { VCombobox, VAutocomplete } from "vuetify/lib";
import debounce from "@/helpers/debounce"
import { SEARCH_USER_BY_USERNAME } from "@/graphql/UserQueries";
const RESULTS_TO_SHOW = 5
export default {
  props: {
    canAdd: {
      type: Boolean,
      default: false,
    },
    value: [Object, String],
    label: String,
  },
  components: { VCombobox, VAutocomplete },
  apollo: {
    users: {
      query: SEARCH_USER_BY_USERNAME,
      variables() { 
        return  {
          username: this.search,
          numberToShow: RESULTS_TO_SHOW,
          cursor: null,
        }
      },
      watchLoading(isLoading) {
        this.loading = isLoading
      },
      skip() {
        if (this.search) {
          return !(this.search.length > 1)
        }
        return true
      },
    },
  },
  data() {
    return {
      user: this.value,
      search: "",
      cursor: null,
      loading: false,
    };
  },
  watch: {
    user(newValue) {
      let emit = newValue
      if (newValue) {
        emit = newValue.node
      }
      this.$emit("input", emit);
    },
    value(newValue) {
      if (this.user && this.user.node != newValue) {
        if (newValue == null) {
          this.user = null
        }
        else {
          this.user =  { node: newValue };
        }
      }
    },
    search(newValue) {
      this.debouncedSearch(newValue)
    },
  },
  methods: {
    endIntersect(entries, observer, isIntersecting) {
      if (isIntersecting && this.users && this.users.pageInfo.hasNextPage) {
        let cursor = this.users.pageInfo.endCursor
        
        this.$apollo.queries.users.fetchMore({
          variables: { cursor: cursor},
          updateQuery: (previousResult, { fetchMoreResult }) => {
            let edges = [
              ...previousResult.users.edges,
              ...fetchMoreResult.users.edges,
            ]

            let pageInfo = fetchMoreResult.users.pageInfo;
            return { 
              users: {
                edges: edges,
                pageInfo: pageInfo,
                __typename: previousResult.users.__typename,
              }
            }
          }
        })
      }
    },
    debouncedSearch: debounce(function (search) {
      if (this.users) {
        this.$apollo.queries.users.refetch({
          username: search,
          numberToShow: RESULTS_TO_SHOW, 
          cursor: null,
        });
      }
    }, 500),
    filter(item, queryText) {
      return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
    }
  },
  computed: {
    computedUsers() {
      if (this.users){
        return this.users.edges
      }
      return []
    },
    skip() {
      if (this.search) {
        return this.search.length > 1
      }
      return false
    }
  }
};
</script>

Before scrolling down

After scrolling down

like image 138
Brettins Avatar answered Oct 31 '22 10:10

Brettins