Skip to content

Instantly share code, notes, and snippets.

@blackcoper
Last active June 13, 2025 17:00
Show Gist options
  • Select an option

  • Save blackcoper/17c9717e925fc76f3b565c0f7c135cfc to your computer and use it in GitHub Desktop.

Select an option

Save blackcoper/17c9717e925fc76f3b565c0f7c135cfc to your computer and use it in GitHub Desktop.
vue-select feature Remote infinity scroll
<template>
<vSelect
ref="v_select_remote"
:options="config.data"
@open="onOpen"
@close="onClose"
append-to-body
:searchable="true"
@input="onSearch">
<template #list-footer class="vue-select-loader">
<div class="d-flex justify-content-center">
<li v-if="loading" class="vue-select-no-options">
<i class="fas fa-circle-notch fa-spin"></i>
</li>
<li v-show="hasNextPage" ref="load" class="loader">
&nbsp;
</li>
</div>
</template>
<template #no-options>
<!-- check if still get data from remote dont show no data. -->
<li v-if="loading" class="vue-select-no-options"></li>
</template>
</vSelect>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
import vSelect from 'vue-select'
const vSelectRemoteRef: any = useTemplateRef('v_select_remote')
const { remote, excludeIds = [], filterKey, extraFilters } = defineProps(['remote','excludeIds','filterKey', 'extraFilters'])
const observer = ref < any > (null)
const initial = () => ({
currentPage: 0,
perPage: 20,
data: <Array<any>>[],
search: '',
total: 1,
})
const config = ref(initial())
const load = ref(null)
const loading = ref(false)
watch(() => excludeIds, () => {
if(vSelectRemoteRef && vSelectRemoteRef.value.open) {
config.value = initial()
onSearch(undefined);
}
})
watch(() => extraFilters, (id) => {
if(vSelectRemoteRef && vSelectRemoteRef.value.open) {
config.value = initial()
onSearch(undefined);
}
})
const debounceQueryTimer = ref<number | null>(null)
const onSearch = (query: any) => {
loading.value = true
if (debounceQueryTimer.value) {
clearTimeout(debounceQueryTimer.value)
}
debounceQueryTimer.value = setTimeout(async () => {
config.value = initial()
config.value.search = (query?.target as HTMLInputElement)?.value || ''
await getData();
}, 300)
return;
}
const getData = async () => {
const response = await remote({
page: config.value.currentPage + 1,
count: config.value.perPage,
excludeIds: excludeIds,
filters: config.value.search && filterKey
? {
[filterKey]: {
op: 'like',
value: config.value.search
},
...(extraFilters ? extraFilters : {})
}
: {
...(extraFilters ? extraFilters : {})
},
})
const { items, page, total } = response.result
config.value.currentPage = page
config.value.total = total
config.value.data = config.value.data.concat(items);
loading.value = false
}
const onOpen = async () => {
if (hasNextPage) {
await nextTick()
if(load.value) {
observer.value.observe(load.value)
}
}
}
const onClose = async () => {
observer.value.disconnect()
}
const infiniteScroll = async ([{ isIntersecting, target }]: any) => {
if (isIntersecting) {
const ul = target.offsetParent
const scrollTop = target.offsetParent.scrollTop
await nextTick()
if(!loading.value) await getData();
ul.scrollTop = scrollTop + (config.value.currentPage > 1 ? 12 : 0)
}
};
const hasNextPage = computed(() => {
return config.value.data.length < config.value.total
})
onMounted(() => {
observer.value = new IntersectionObserver(infiniteScroll)
})
</script>
@blackcoper
Copy link
Author

Usage in your view:

<v-select-remote v-if="
  column.filterOptions &&
  column.filterOptions.customFilter &&
  column.field === 'roleName'
" v-model="selectedRole" label="roleName" :remote="roleService.getListRoles" placeholder="Filter by Role"
  filterKey="roleName"
  @update:modelValue="() => {
    updateFilters(column, selectedRole?.id || null);
  }" />

in your script:
const selectedRole = ref<any>(null)

in service axios:

async getListRoles(params: Record<string, any>): Promise<any> {
    const _api = this.api || api.create('Role')
    try {
      const response = await _api.get('/role', { params })
      return response.data
    } catch (error: any) {
      console.error('Failed to fetch users:', error)
      if (error.response) {
        console.error('Server responded with:', error.response.data)
      }
      throw new Error(error.message) // Re-throw the error to handle it in the caller
    }
  }

in your return api must like this format:

async findRoleAll(
....
// your query db:
....
return {
      page, // page 1,2,3,4,etc
      count, // count is total found items.length
      items,  // data query filtered and limited per page count 
      total, // total all data without limit.
    };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment