v-data-tableを利用して無限スクロールテーブルComponentを作成する

# はじめに
無限スクロール機能を作成する機会があったためメモを残すことにしました。
本当はnuxt-property-decoratorを使って作って見たかったのですが、今回は断念しました。

# Component
今回はdocumentではなくtableにスクロールイベントを持たせる作りにしています。
また、apiからページネートされたデータを取得できる前提で作成しています。

```vuejs: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を利用した作りになっています

```vuejs: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)"
  />
```


```typescript: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
    },
  }

```