mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-20 06:43:39 +09:00
14 KiB
14 KiB
Async data fetching (advanced)
Most of the time, the data need to be fetched asynchronously from the server.
Thankfully, the data property of <VFlexTableWrapper /> component can accept
an async function that will be triggered each time one of the sorting,
filtering or pagination properties change.
Check the markup for more details about usage.
<script setup lang="ts">
import type { VFlexTableWrapperDataResolver } from '/@src/components/base/table/VFlexTableWrapper.vue'
// the total data will be set by the fetchData function
const total = ref(0)
// we don't have to set "searchable" parameter
// this will be handled by the fetchData function
const columns = {
name: {
label: 'Username',
media: true,
grow: true,
sortable: true,
},
location: {
label: 'Location',
sortable: true,
},
position: {
label: 'Positions',
sortable: true,
},
actions: {
label: '',
align: 'end',
},
} as const
// this is an example of useXxx function that we can reuse across components.
// it will return writable computeds that works like ref values
// but the values will be sync with the route query params
function useQueryParam() {
const router = useRouter()
const route = useRoute()
// when the params match those value,
// we don't set their value to the query params
const defaultSearch = ''
const defaultSort = ''
const defaultLimit = 10
const defaultPage = 1
const searchTerm = computed({
get: () => {
let searchTermQuery: string
// read "search" from the query params
if (Array.isArray(route?.query?.search)) {
searchTermQuery = route.query.search?.[0] ?? defaultSearch
}
else {
searchTermQuery = route.query.search ?? defaultSearch
}
return searchTermQuery
},
set(value) {
// update the route query params with our new "search" value.
// we can use router.replace instead of router.push
// to not write state to the browser history
router.push({
query: {
search: value === defaultSearch ? undefined : value,
sort: sort.value === defaultSort ? undefined : sort.value,
limit: limit.value === defaultLimit ? undefined : limit.value,
page: page.value === defaultPage ? undefined : page.value,
},
})
},
})
const sort = computed({
get: () => {
let sortQuery: string
// read "sort" from the query params
if (Array.isArray(route?.query?.sort)) {
sortQuery = route.query.sort?.[0] ?? defaultSort
}
else {
sortQuery = route.query.sort ?? defaultSort
}
return sortQuery
},
set(value) {
// update the route query params with our new "sort" value.
// we can use router.replace instead of router.push
// to not write state to the browser history
router.push({
query: {
search: searchTerm.value === defaultSearch ? undefined : searchTerm.value,
sort: value === defaultSort ? undefined : value,
limit: limit.value === defaultLimit ? undefined : limit.value,
page: page.value === defaultPage ? undefined : page.value,
},
})
},
})
const limit = computed({
get: () => {
let limitQuery: number
// read "limit" from the query params
if (Array.isArray(route?.query?.limit)) {
limitQuery = parseInt(route.query.limit[0] ?? `${defaultLimit}`)
}
else {
limitQuery = parseInt(route.query.limit ?? `${defaultLimit}`)
}
if (Object.is(limitQuery, NaN)) {
limitQuery = defaultLimit
}
return limitQuery
},
set(value) {
// update the route query params with our new "limit" value.
// we can use router.replace instead of router.push
// to not write state to the browser history
router.push({
query: {
search: searchTerm.value === defaultSearch ? undefined : searchTerm.value,
sort: sort.value === defaultSort ? undefined : sort.value,
limit: value === defaultLimit ? undefined : value,
page: page.value === defaultPage ? undefined : page.value,
},
})
},
})
const page = computed({
get: () => {
let pageQuery: number
if (Array.isArray(route?.query?.page)) {
pageQuery = parseInt(route.query.page[0] ?? `${defaultPage}`)
}
else {
pageQuery = parseInt(route.query.page ?? `${defaultPage}`)
}
// read "page" from the query params
if (Object.is(pageQuery, NaN)) {
pageQuery = defaultPage
}
return pageQuery
},
set(value) {
// update the route query params with our new "page" value.
// we can use router.replace instead of router.push
// to not write state to the browser history
router.push({
query: {
search: searchTerm.value === defaultSearch ? undefined : searchTerm.value,
sort: sort.value === defaultSort ? undefined : sort.value,
limit: limit.value === defaultLimit ? undefined : limit.value,
page: value === defaultPage ? undefined : value,
},
})
},
})
return reactive({
searchTerm,
sort,
limit,
page,
})
}
const queryParam = useQueryParam()
// the fetchData function will be called each time one of the parameter changes
const $fetch = useApiFetch()
const fetchData: VFlexTableWrapperDataResolver = async ({
searchTerm,
start,
limit,
sort,
controller,
}) => {
// sort will be a string like "name:asc"
let [sortField, sortOrder]
= sort && sort.includes(':') ? sort.split(':') : [undefined, undefined]
// async fetch data to our server
const { _data: users, headers } = await $fetch.raw(`/api/users`, {
query: {
// searchTerm will contains the value of the wrapperState.searchInput
// the update will be debounced to avoid to much requests
q: searchTerm,
_start: start,
_limit: limit,
_sort: sortField,
_order: sortOrder,
},
// controller is an instance of AbortController,
// this allow to abort the request when the state
// is invalidated (before fetchData will be retriggered)
signal: controller?.signal,
})
// wait more time
await sleep(1000)
// our backend send us the count in the headers,
// but we can also get it from another request
if (headers.has('X-Total-Count')) {
total.value = parseInt(headers.get('X-Total-Count') ?? '0')
}
// the return of the function must be an array
return users
}
// those data are for the interaction example
const openedRowId = ref<number>()
function onRowClick(row: any) {
if (openedRowId.value === row.id) {
openedRowId.value = undefined
}
else {
openedRowId.value = row.id
}
}
const incomingCallerId = ref<number>()
function onCallClick(row: any) {
if (incomingCallerId.value === row.id) {
incomingCallerId.value = undefined
}
else {
incomingCallerId.value = row.id
}
}
</script>
<template>
<!--
We use v-model to let VFlexTableWrapper update queryParam
-->
<VFlexTableWrapper
v-model:page="queryParam.page"
v-model:limit="queryParam.limit"
v-model:searchTerm="queryParam.searchTerm"
v-model:sort="queryParam.sort"
:columns="columns"
:data="fetchData"
:total="total"
class="mt-4"
>
<!--
Here we retrieve the internal wrapperState.
Note that we can not destructure it
-->
<template #default="wrapperState">
<!--Table Pagination-->
<VFlexPagination
v-model:current-page="wrapperState.page"
:item-per-page="wrapperState.limit"
:total-items="wrapperState.total"
:max-links-displayed="2"
no-router
>
<!-- The controls can be updated anywhere in the slot -->
<template #before-pagination>
<VFlex class="mr-4">
<VField>
<VControl icon="lucide:search">
<input
v-model="wrapperState.searchInput"
type="text"
class="input is-rounded"
placeholder="Filter..."
>
</VControl>
</VField>
</VFlex>
</template>
<template #before-navigation>
<VFlex class="mr-4">
<VField>
<VControl>
<div class="select is-rounded">
<select v-model="wrapperState.limit">
<option :value="1">
1 results per page
</option>
<option :value="10">
10 results per page
</option>
<option :value="15">
15 results per page
</option>
<option :value="25">
25 results per page
</option>
<option :value="50">
50 results per page
</option>
</select>
</div>
</VControl>
</VField>
</VFlex>
</template>
</VFlexPagination>
<VFlexTable
rounded
clickable
@row-click="onRowClick"
>
<template #body>
<!--
The wrapperState.loading will be update
when the fetchData function is running
-->
<div v-if="wrapperState.loading" class="flex-list-inner">
<div
v-for="key in wrapperState.limit"
:key="key"
class="flex-table-item"
>
<VFlexTableCell :column="{ grow: true, media: true }">
<VPlaceloadAvatar size="medium" />
<VPlaceloadText
:lines="2"
width="60%"
last-line-width="20%"
class="mx-2"
/>
</VFlexTableCell>
<VFlexTableCell>
<VPlaceload width="60%" class="mx-1" />
</VFlexTableCell>
<VFlexTableCell>
<VPlaceload width="60%" class="mx-1" />
</VFlexTableCell>
<VFlexTableCell :column="{ align: 'end' }">
<VPlaceload width="45%" class="mx-1" />
</VFlexTableCell>
</div>
</div>
<!-- This is the empty state -->
<div v-else-if="wrapperState.data.length === 0" class="flex-list-inner">
<VPlaceholderSection
title="No matches"
subtitle="There is no data that match your query."
class="my-6"
>
<template #image>
<img
class="light-image"
src="/images/illustrations/placeholders/search-4.svg"
alt=""
>
<img
class="dark-image"
src="/images/illustrations/placeholders/search-4-dark.svg"
alt=""
>
</template>
</VPlaceholderSection>
</div>
</template>
<!-- We can inject content before any rows -->
<template #body-row-pre="{ row }">
<template v-if="row.id === incomingCallerId">
<VProgress size="tiny" class="m-0 mb-1" />
</template>
</template>
<!-- This is the body cell slot -->
<template #body-cell="{ row, column }">
<template v-if="column.key === 'name'">
<VAvatar
size="medium"
:picture="row.pic"
:badge="row.badge"
:initials="row.initials"
/>
<div>
<span class="dark-text">{{ row?.name }}</span>
<VTextEllipsis width="280px" class="light-text">
{{ row?.bio }}
</VTextEllipsis>
</div>
</template>
<template v-if="column.key === 'actions'">
<VAction>
{{ row?.id === openedRowId ? 'Hide details' : 'View details' }}
</VAction>
</template>
</template>
<!-- We can also inject content after rows -->
<template #body-row-post="{ row }">
<template v-if="row?.id === incomingCallerId">
<VTags class="mt-2 mb-0">
<VTag color="primary" outlined>
<VIcon class="is-inline mr-2" icon="lucide:send" />
Calling...
</VTag>
</VTags>
</template>
<template v-if="row?.id === openedRowId">
<div class="is-block p-4 my-2 is-rounded">
<div class="dark-text mb-4 is-size-4">
{{ row?.name }}'s details
</div>
<VFlex justify-content="space-between">
<VFlexItem>
<VCard>
<pre><code>{{ row }}</code></pre>
</VCard>
</VFlexItem>
<VFlexItem align-self="flex-end">
<VFlex flex-direction="column">
<VButton
v-if="row?.id === incomingCallerId"
class="mb-2"
color="danger"
@click="() => onCallClick(row)"
>
<VIcon class="is-inline mr-2" icon="lucide:phone-off" />
Cancel call
</VButton>
<VButton
color="primary"
outlined
:disabled="row.id === incomingCallerId"
:loading="row.id === incomingCallerId"
@click="() => onCallClick(row)"
>
<VIcon class="is-inline mr-2" icon="lucide:phone" />
Call {{ row?.name }}
</VButton>
</VFlex>
</VFlexItem>
</VFlex>
</div>
</template>
</template>
</VFlexTable>
<!--Table Pagination-->
<VFlexPagination
v-model:current-page="wrapperState.page"
class="mt-5"
:item-per-page="wrapperState.limit"
:total-items="wrapperState.total"
:max-links-displayed="2"
no-router
/>
</template>
</VFlexTableWrapper>
</template>