😽

【Vue.js】 v-data-table スクロール位置を調整する方法

に公開

概要

Vuetifyのv-data-tableを使って大量のデータを表示する場合、ページネーションとスクロールの併用はよくあるパターンです。
ただしそのままだと、ページを切り替えてもテーブルのスクロール位置が維持されたままになり、UXが悪化することがあります。
本記事では、ページネーションのたびにスクロール位置を先頭にリセットする方法をご紹介します。

解決方法(watch + nextTick + DOM操作)

v-data-tableは内部的にdiv.v-table__wrapperというスクロールコンテナを生成します。
これをJavaScriptで取得し、ページ変更時にscrollToで先頭に戻します。

DOM要素への参照用の変数定義

テンプレートにある要素をJavaScript側から参照するための変数を定義します。

const tableContainerRef = ref(null)

templateでは、v-data-tableをdivタグで囲い、DOM要素を取得できるようにする。

<div ref="tableContainerRef">
  <v-data-table
    :items="paginatedItems"
    :headers="headers"
    :items-per-page="itemsPerPage"
    :page="currentPage"
    hide-default-footer
    fixed-header="true"
    height="500"
  />
</div>

watchの中で、DOM操作し、スクロール位置を調整

watch(currentPage)によってページ番号の変更を検知し、次にnextTick()を使ってDOMの更新が完了するのを待ちます。
その後、Vuetifyが内部的に生成する.v-table__wrapper要素をdocument.querySelector で取得し、scrollTo({ top: 0 })を使ってスクロール位置をテーブルの先頭に戻します。

watch(currentPage, async () => {
  await nextTick()
  const tableWrapper = document.querySelector('.v-table__wrapper')
  tableWrapper?.scrollTo({
    top: 0,
    behavior: 'auto',
  })
})

サンプルコード

<script setup>
import { ref, computed, watch, nextTick } from 'vue'

const itemsPerPage = ref(100)
const currentPage = ref(1)

const items = Array.from({ length: 1000 }, (_, i) => ({
  id: i + 1,
  name: `ユーザー${i + 1}`,
  email: `user${i + 1}@example.com`,
}))

const headers = [
  { title: 'ID', key: 'id' },
  { title: '名前', key: 'name' },
  { title: 'メール', key: 'email' },
]

const pageCount = computed(() => Math.ceil(items.length / itemsPerPage.value))

const paginatedItems = computed(() => {
  const start = (currentPage.value - 1) * itemsPerPage.value
  return items.slice(start, start + itemsPerPage.value)
})

// スクロール用のrefを追加
const tableContainerRef = ref(null)

watch(currentPage, async () => {
  await nextTick()

  // Vuetifyが自動で作るスクロールエリアを取得してスクロール
  const tableWrapper = document.querySelector('.v-table__wrapper')
  tableWrapper?.scrollTo({
    top: 0,
    behavior: 'auto',
  })
})
</script>

<template>
  <v-app>
    <v-main>
      <v-card>
        <v-card-item>
          <v-row>
            <v-col>
              <!-- ref をつける -->
              <div ref="tableContainerRef">
                <v-data-table
                  :items="paginatedItems"
                  :headers="headers"
                  :items-per-page="itemsPerPage"
                  :page="currentPage"
                  hide-default-footer
                  fixed-header="true"
                  height="500"
                />
              </div>

              <div class="d-flex justify-center mt-4">
                <v-pagination v-model="currentPage" :length="pageCount" :total-visible="5" />
              </div>
            </v-col>
          </v-row>
        </v-card-item>
      </v-card>
    </v-main>
  </v-app>
</template>

<style>
.custom-data-table th,
span.custom-header {
  white-space: nowrap;
  width: auto;
  max-width: max-content;
  overflow: hidden;
  text-overflow: ellipsis;
}

.custom-data-table {
  max-height: 600px;
  overflow-x: auto;
}

.custom-data-table thread {
  position: sticky;
  top: 0;
  z-index: 10;
  background-color: rgb(216, 94, 94);
}
</style>

Discussion