Vue Example: Grouping

<script setup lang="ts">
import {
  FlexRender,
  aggregationFns,
  columnFilteringFeature,
  columnGroupingFeature,
  createColumnHelper,
  createExpandedRowModel,
  createFilteredRowModel,
  createGroupedRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFns,
  rowExpandingFeature,
  rowPaginationFeature,
  rowSortingFeature,
  sortFns,
  tableFeatures,
  useTable,
} from '@tanstack/vue-table'
import { ref } from 'vue'
import { makeData } from './makeData'
import type { Person } from './makeData'

const features = tableFeatures({
  columnFilteringFeature,
  columnGroupingFeature,
  rowExpandingFeature,
  rowPaginationFeature,
  rowSortingFeature,
  filteredRowModel: createFilteredRowModel(),
  groupedRowModel: createGroupedRowModel(),
  expandedRowModel: createExpandedRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  sortedRowModel: createSortedRowModel(),
  filterFns,
  sortFns,
  aggregationFns,
})

const columnHelper = createColumnHelper<typeof features, Person>()

const pageSizes = [10, 20, 30, 40, 50]
const data = ref(makeData(10_000))

const columns = ref(
  columnHelper.columns([
    columnHelper.accessor('firstName', {
      header: 'First Name',
      cell: (info) => info.getValue(),
      /**
       * override the value used for row grouping
       * (otherwise, defaults to the value derived from accessorKey / accessorFn)
       */
      getGroupingValue: (row) => `${row.firstName} ${row.lastName}`,
    }),
    columnHelper.accessor((row) => row.lastName, {
      id: 'lastName',
      header: () => 'Last Name',
      cell: (info) => info.getValue(),
    }),
    columnHelper.accessor('age', {
      header: () => 'Age',
      aggregationFn: 'median',
      aggregatedCell: ({ getValue }) =>
        Math.round(getValue<number>() * 100) / 100,
    }),
    columnHelper.accessor('visits', {
      header: () => 'Visits',
      aggregationFn: 'sum',
      aggregatedCell: ({ getValue }) => getValue<number>().toLocaleString(),
    }),
    columnHelper.accessor('status', {
      header: 'Status',
    }),
    columnHelper.accessor('progress', {
      header: 'Profile Progress',
      cell: ({ getValue }) => Math.round(getValue<number>() * 100) / 100 + '%',
      aggregationFn: 'mean',
      aggregatedCell: ({ getValue }) =>
        Math.round(getValue<number>() * 100) / 100 + '%',
    }),
  ]),
)

const table = useTable({
  features,
  data,
  get columns() {
    return columns.value
  },
  debugTable: true,
})

const refreshData = () => {
  data.value = makeData(10_000)
}

const stressTest = () => {
  data.value = makeData(200_000)
}

function handlePageSizeChange(e: Event) {
  const target = e.target as HTMLSelectElement
  table.setPageSize(Number(target.value))
}

function handleGoToPage(e: Event) {
  const target = e.target as HTMLInputElement
  const page = target.value ? Number(target.value) - 1 : 0
  table.setPageIndex(page)
}
</script>

<template>
  <div class="demo-root">
    <div class="button-row">
      <button @click="refreshData" class="demo-button">Regenerate Data</button>
      <button @click="stressTest" class="demo-button">
        Stress Test (200k rows)
      </button>
    </div>
    <div class="spacer-md" />
    <table>
      <thead>
        <tr
          v-for="headerGroup in table.getHeaderGroups()"
          :key="headerGroup.id"
        >
          <th
            v-for="header in headerGroup.headers"
            :key="header.id"
            :colSpan="header.colSpan"
          >
            <div v-if="!header.isPlaceholder">
              <button
                v-if="header.column.getCanGroup()"
                @click="header.column.getToggleGroupingHandler()()"
                style="cursor: pointer"
              >
                {{
                  header.column.getIsGrouped()
                    ? `๐Ÿ›‘ (${header.column.getGroupedIndex()}) `
                    : `๐Ÿ‘Š `
                }}
              </button>
              <FlexRender :header="header" />
            </div>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="row in table.getRowModel().rows" :key="row.id">
          <td
            v-for="cell in row.getAllCells()"
            :key="cell.id"
            :style="{
              background: cell.getIsGrouped()
                ? '#0aff0082'
                : cell.getIsAggregated()
                  ? '#ffa50078'
                  : cell.getIsPlaceholder()
                    ? '#ff000042'
                    : 'white',
            }"
          >
            <button
              v-if="cell.getIsGrouped()"
              @click="row.getToggleExpandedHandler()()"
              :style="{ cursor: row.getCanExpand() ? 'pointer' : 'normal' }"
            >
              {{ row.getIsExpanded() ? '๐Ÿ‘‡' : '๐Ÿ‘‰' }}
              <FlexRender :cell="cell" />
              ({{ row.subRows.length.toLocaleString() }})
            </button>
            <FlexRender v-else-if="cell.getIsAggregated()" :cell="cell" />
            <template v-else-if="cell.getIsPlaceholder()" />
            <FlexRender v-else :cell="cell" />
          </td>
        </tr>
      </tbody>
    </table>
    <div class="spacer-sm" />
    <div>
      <div class="controls">
        <button
          class="demo-button demo-button-sm"
          @click="() => table.setPageIndex(0)"
          :disabled="!table.getCanPreviousPage()"
        >
          ยซ
        </button>
        <button
          class="demo-button demo-button-sm"
          @click="() => table.previousPage()"
          :disabled="!table.getCanPreviousPage()"
        >
          โ€น
        </button>
        <button
          class="demo-button demo-button-sm"
          @click="() => table.nextPage()"
          :disabled="!table.getCanNextPage()"
        >
          โ€บ
        </button>
        <button
          class="demo-button demo-button-sm"
          @click="() => table.setPageIndex(table.getPageCount() - 1)"
          :disabled="!table.getCanNextPage()"
        >
          ยป
        </button>
        <span class="inline-controls">
          <div>Page</div>
          <strong>
            {{ (table.atoms.pagination.get().pageIndex + 1).toLocaleString() }}
            of
            {{ table.getPageCount().toLocaleString() }}
          </strong>
        </span>
        <span class="inline-controls">
          | Go to page:
          <input
            type="number"
            min="1"
            :max="table.getPageCount()"
            :value="table.atoms.pagination.get().pageIndex + 1"
            @change="handleGoToPage"
            class="page-size-input"
          />
        </span>
        <select
          :value="table.atoms.pagination.get().pageSize"
          @change="handlePageSizeChange"
        >
          <option
            :key="pageSize"
            :value="pageSize"
            v-for="pageSize in pageSizes"
          >
            Show {{ pageSize }}
          </option>
        </select>
      </div>
      <div>{{ table.getRowModel().rows.length.toLocaleString() }} Rows</div>
      <pre>{{ JSON.stringify(table.store.get(), null, 2) }}</pre>
    </div>
    <div class="spacer-sm" />
  </div>
</template>

<style>
html {
  font-family: sans-serif;
  font-size: 14px;
}

table {
  border-spacing: 0;
  border-collapse: collapse;
  border: 1px solid lightgray;
}

tbody {
  border-bottom: 1px solid lightgray;
}

th {
  border-bottom: 1px solid lightgray;
  border-right: 1px solid lightgray;
  padding: 2px 4px;
}

tfoot {
  color: gray;
}

tfoot th {
  font-weight: normal;
}
</style>