はじめに
無限スクロール機能を作成する機会があったためメモを残すことにしました。
本当はnuxt-property-decoratorを使って作って見たかったのですが、今回は断念しました。
Component
今回はdocumentではなくtableにスクロールイベントを持たせる作りにしています。
また、apiからページネートされたデータを取得できる前提で作成しています。
MyInfinityScrollDataTable.vue
<template>
<v-data-table
v-bind="$attrs"
ref="MyInfinityScrollDataTable"
:loading="loading"
:loading-text="loadingText"
fixed-header
disable-sort
disable-pagination
hide-default-footer
>
<!-- v-data-table slot and scopedSlots -->
<slot
v-for="(_, name) in $slots"
:slot="name"
:name="name"
/>
<template
v-for="(_, name) in $scopedSlots"
:slot="name"
slot-scope="slotData"
>
<slot :name="name" v-bind="slotData" />
</template>
<!-- loading sign -->
<template #foot>
<tr
v-if="!isLastPage"
class="text-center"
>
<td
class="--loading-text text-subtitle-2 my-2"
colspan="7"
>
<v-divider />
<span
v-for="(text, i) of loadingTextArray"
:key="i"
:style="{ animationDelay: `${i * 0.1}s` }"
class="py-3"
:class="{ 'ml-1': text.startsWith(' ') }"
>
{{ text }}
</span>
</td>
</tr>
</template>
</v-data-table>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'MyInfinityScrollDataTable',
props: {
loading: {
type: Boolean,
default: false,
},
loadingText: {
type: String,
default: 'Loading... Please wait',
},
isLastPage: {
type: Boolean,
default: true,
},
},
computed: {
loadingTextArray(): string[] {
return this.loadingText?.match(/.{1,4}/g) || []
},
tableInstance(): Element {
const table = this.$refs.MyInfinityScrollDataTable as Vue
return (table?.$el?.childNodes || [])[0] as Element
},
},
mounted() {
this.tableInstance.addEventListener('scroll', this.onScroll)
},
methods: {
onScroll() {
if (this.loading) {
return
}
const { scrollTop, clientHeight, scrollHeight } = this.tableInstance
if (scrollTop + clientHeight < scrollHeight) {
return
}
if (this.isLastPage) {
return
}
setTimeout(() => {
this.$emit('next')
}, 1000)
},
},
})
</script>
<style lang="scss" scoped>
.--loading-text {
background: var(--v-secondary-lighten3);
color: var(--v-secondary-base);
span {
display: inline-block;
animation: loading 1.4s infinite alternate;
}
}
@keyframes loading {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
使用側
このソースでは、無限スクロールを実現するに当たって、pageではなくper_pageを制御する方針を取ります。
これは、更新用のUIとして使用されることを想定した場合に、更新後データのrefetchをし易くするためです。
データ量が多くなる場合は、この制御は見直す必要があると思われます。
以下は製造時の都合でvuexを利用した作りになっています
template
<infinity-scroll-data-table
:height="items.length > 4 ? 300 : undefined"
:headers="headers"
:items="items"
:server-items-length="items.length"
:loading="loading"
:is-last-page="isLastPage"
@next="fetchList(perPage + 20)"
/>
script
data(): Data {
loading: false,
},
computed: {
isLastPage(): boolean {
// store getterでlast_pageを取得する
return lastPage === 1
},
perPage(): number {
// store getterでper_pageを取得する
},
items(): Item[] {
// store getterでデータリストを取得する
},
},
methods: {
async fetchList (perPage: number = 20): void {
this.loading = true
// (await) store actionで新しいper_pageでデータリストをサーバから取得してstateに詰める
this.loading = false
},
}