ヘッドレスUI TanStack vue-table を Vue 3 で利用する – ページング

Vue
Vueフロントエンド

この記事では TanStack が提供する @tanstack/vue-table を利用してページングを行う方法を説明します。全件を一括で取得する場合とページ切り替えごとに情報を取得してくるケース、それぞれのケースがあります。

@tanstack/vue-table を利用したプロジェクトのセットアップを始め、シンプルな一覧の表示、ヘッダやフッタのカスタマイズ、ソート、フィルタ、列幅変更など方法については以下の記事で解説しています。


これからはじめるVue.js 3実践入門

ページング(データを最初に全件取得する場合)

useVueTable に以下を追加します。

  // ページングを行ったときの処理モデル
  getPaginationRowModel: getPaginationRowModel(),

後は template 側の変更です。今回行ったのは、公式の React 版のサンプルを Vue 版にしたものです。ページングするために必要なIFが揃っています

項目指定方法
現在のページ番号table.getState().pagination.pageIndex
総ページ数table.getPageCount()
ページあたりに表示件数table.getState().pagination.pageSize
前のページがあるかtable.getCanPreviousPage()
次のページがあるかtable.getNextPreviousPage()
指定ページへ移動table.setPageIndex()
前ページへ移動table.previousPage()
次ページへ移動table.nextPage()
vue-table が提供しているページングで利用するメソッド

実装は以下の様になります。
末尾の button はもともとあったものですが、ページングのためにテストデータを作るメソッドに置き換えています。コードは長いように見えますが、ページングするための「<<」「<」「>」「>> 」だったり、何ページにジャンプするか、の要素を作成しているだけです。

    <div class="pagination">
      <div className="flex items-center gap-2">
        <button
          className="border rounded p-1"
          @click="() => table.setPageIndex(0)"
          :disabled="!table.getCanPreviousPage()"
        >
          {{ '<<' }}
        </button>
        <button
          className="border rounded p-1"
          @click="() => table.previousPage()"
          :disabled="!table.getCanPreviousPage()"
        >
          {{ '<' }}
        </button>
        <button
          className="border rounded p-1"
          @click="() => table.nextPage()"
          :disabled="!table.getCanNextPage()"
        >
          {{ '>' }}
        </button>
        <button
          className="border rounded p-1"
          @click="() => table.setPageIndex(table.getPageCount() - 1)"
          :disabled="!table.getCanNextPage()"
        >
          {{ '>>' }}
        </button>
        <span className="flex items-center gap-1">
          <div>Page</div>
          <strong>
            {{ table.getState().pagination.pageIndex + 1 }} of {{ ' ' }}
            {{ table.getPageCount() }}
          </strong>
        </span>
        <span className="flex items-center gap-1">
          | Go to page:
          <input
            type="number"
            :defaultValue="table.getState().pagination.pageIndex + 1"
            @change="(e: Event) => {
              table.setPageIndex((e.target as HTMLInputElement).value ? Number((e.target as HTMLInputElement).value) - 1 : 0)
            }"
            className="border p-1 rounded w-16"
          />
        </span>
        <select
          value="{table.getState().pagination.pageSize}"
          @change="(e: Event) => {
            table.setPageSize(Number((e.target as HTMLInputElement).value))
          }"
        >
          <option
            v-for="pageSize in [10, 20, 30, 40, 50]"
            :key="pageSize"
            :value="pageSize"
          >
            Show {{ pageSize }}
          </option>
        </select>
      </div>
      <div>{{ table.getRowModel().rows.length }} Rows</div>
      <pre>{{ JSON.stringify(table.getState().pagination, null, 2) }}</pre>
    </div>
    <button @click="() => (userList = makeData(3000))">Rerender</button>

テストデータを作るコードはこちら。ダミーデータが作成できる faker を利用しています(version は 7.5.0)。

import { faker } from '@faker-js/faker'

faker.locale = 'ja'

type User = {
  firstName: string
  lastName: string
  age: number
  visits: number
  status: string
  progress: number
}

const newUser = (): User => {
  return {
    firstName: faker.name.firstName(),
    lastName: faker.name.lastName(),
    age: faker.datatype.number(40),
    visits: faker.datatype.number(1000),
    progress: faker.datatype.number(100),
    status: faker.helpers.shuffle<User['status']>(['無効', '有向'])[0]!,
  }
}

export function makeData(lens: number) {
  const result: User[] = []
  const lists = [...Array(lens)].map((_, i) => i)
  for (let i of lists) {
    result.push(newUser())
  }
  return result
}

結果です。いい感じにページングができています。また、3000件のデータとしましたが表示やページの移動は一瞬です。データがプレーンテキスト数個なので、そもそもデータが軽い点はありますが、それでも本当に一瞬です。

実行結果 - ページング(データを最初に全件取得する場合)
実行結果 – ページング(データを最初に全件取得する場合)

ページング(ページ単位で都度表示対象を取得する)

業務システムでは最初に一括取得するケースも多い印象ですが、一般的な Web アプリだと基本はページ単位の情報取得だと思います。
この場合は、表示する情報と、pagination の状態管理、総ページ数(pageCount) を自前で行うことになります。useVueTable で変わる部分は以下の通りです。

  get data() {
    // 表示対象
    return data.value
  },
  state: {
    get sorting() {
      return sorting.value
    },
    // 現在のページ数(pageIndex), ページごとに表示数(pageSize) をもつオブジェクト 
    get pagination() {
      return paginationState
    },
  },
   ...
  // ページングを行ったときの処理モデル
  // 手動制御する場合は使いません
  // getPaginationRowModel: getPaginationRowModel(),

  // 総ページ数を指定
  get pageCount() {
    return pageCount.value
  },
  // 表示ページのindexが変更された際のイベントハンドラ
  onPaginationChange: setPagination,
  // 手動制御する場合はこちらを true にする(と上のgetPaginationRowMode()が使用されない)
  manualPagination: true,

pagination の宣言やイベントハンドラたちは以下の通りです。
今回は実際には API は実行していませんが、3000 件のダミーデータに対して、該当する表示対象を取得する getData メソッドをサーバからの取得の代替としています。
また、マウント時と paginationState が変わったタイミングでデータを再取得するようにしています。

// ページ数
// ページあたりの表示数
const paginationState = reactive<PaginationState>({
  pageIndex: 0,
  pageSize: 10,
})
// 総ページ数
const pageCount = ref(-1)
// ページ数変更時に更新
const setPagination = (ps: Updater<PaginationState>) => {
  if (typeof ps === 'function') {
    const newState = ps(paginationState)
    console.log(`setPagination is called: `)
    console.log(newState)
    paginationState.pageIndex = newState.pageIndex
    paginationState.pageSize = newState.pageSize
  }
}
// 実際のデータ
const data = ref<User[]>([])
// サーバからのデータ取得のダミー処理
// 
const getData = async () => {
  const { pageIndex, pageSize } = paginationState

  // 次に表示する情報の取得
  const api = () => {
    return {
      items: [...userList.value].slice(
        pageIndex * pageSize,
        pageIndex * pageSize + pageSize
      ),
      pageCount: userList.value.length / pageSize,
    }
  }
  const result = await api()

  data.value = result.items
  pageCount.value = result.pageCount
}
// マウント時
onMounted(() => getData())
// ページ変更に応じて表示データ切り替え
watch(paginationState, () => getData())

実際にページ切り替えをやって上手く動くことが確認できています。Template 部分はイベントハンドラ含め一切変更なしで両方のパターンが実現できました。ただ、ソートは現在保持しているものに限るため、ページ内ソートになっています。これをデータ全体に対して行いたい場合は同じくデフォルトの実装モデルをやめて、独自にデータを取得するようにすればいけそうです。

実行結果 - ページング(ページ単位で都度表示対象を取得する)
実行結果 – ページング(ページ単位で都度表示対象を取得する)

コメント