Vue Example: Grouping

vue
<script setup lang="ts">
import {
  FlexRender,
  getCoreRowModel,
  getExpandedRowModel,
  getGroupedRowModel,
  useVueTable,
  type ColumnDef,
  type ExpandedState,
  type GroupingState,
} from '@tanstack/vue-table'
import { ref } from 'vue'

// Define task type
interface Task {
  id: string
  title: string
  description: string
  status: { id: string; name: string }
  priority: { id: string; name: string }
  dueDate: string
}

// Dummy data
const tasks: Task[] = [
  {
    id: '1',
    title: 'Implement Authentication',
    description: 'Set up OAuth2 with JWT tokens',
    status: { id: 's1', name: 'In Progress' },
    priority: { id: 'p1', name: 'High' },
    dueDate: '2023-09-15',
  },
  {
    id: '2',
    title: 'Create Dashboard UI',
    description: 'Design and implement main dashboard',
    status: { id: 's2', name: 'To Do' },
    priority: { id: 'p2', name: 'Medium' },
    dueDate: '2023-09-20',
  },
  {
    id: '3',
    title: 'API Documentation',
    description: 'Document all API endpoints using Swagger',
    status: { id: 's1', name: 'In Progress' },
    priority: { id: 'p1', name: 'High' },
    dueDate: '2023-09-10',
  },
  {
    id: '4',
    title: 'Database Optimization',
    description: 'Improve query performance and add indexes',
    status: { id: 's3', name: 'Done' },
    priority: { id: 'p3', name: 'Low' },
    dueDate: '2023-08-30',
  },
  {
    id: '5',
    title: 'User Testing',
    description: 'Conduct user testing sessions',
    status: { id: 's2', name: 'To Do' },
    priority: { id: 'p2', name: 'Medium' },
    dueDate: '2023-09-25',
  },
]

// Define columns
const columns: ColumnDef<Task>[] = [
  {
    accessorKey: 'title',
    header: 'Title',
    cell: ({ row }) => row.original.title,
  },
  {
    accessorKey: 'status',
    header: 'Status',
    enableGrouping: true,
    accessorFn: row => row.status.name,
    cell: ({ row }) => row.original.status.name,
  },
  {
    accessorKey: 'priority',
    header: 'Priority',
    enableGrouping: true,
    accessorFn: row => row.priority.name,
    cell: ({ row }) => row.original.priority.name,
  },
  {
    accessorKey: 'description',
    header: 'Description',
    cell: ({ row }) => row.original.description,
  },
  {
    accessorKey: 'dueDate',
    header: 'Due Date',
    cell: ({ row }) => new Date(row.original.dueDate).toLocaleDateString(),
  },
]

// State for grouping
const grouping = ref<GroupingState>([])
const expanded = ref<ExpandedState>({})

// Initialize table
const table = useVueTable({
  data: tasks,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getGroupedRowModel: getGroupedRowModel(),
  getExpandedRowModel: getExpandedRowModel(),
  state: {
    get grouping() {
      return grouping.value
    },
    get expanded() {
      return expanded.value
    },
  },
  onGroupingChange: updaterOrValue => {
    grouping.value =
      typeof updaterOrValue === 'function'
        ? updaterOrValue(grouping.value)
        : updaterOrValue
  },
  onExpandedChange: updaterOrValue => {
    expanded.value =
      typeof updaterOrValue === 'function'
        ? updaterOrValue(expanded.value)
        : updaterOrValue
  },
})

// Group by status
const groupByStatus = (): void => {
  grouping.value = ['status']
}

// Group by priority
const groupByPriority = (): void => {
  grouping.value = ['priority']
}

// Clear grouping
const clearGrouping = (): void => {
  grouping.value = []
}
</script>

<template>
  <div class="row-grouping-container">
    <h1 class="title">Vue Tanstack Table Example with Row Grouping</h1>

    <!-- Grouping controls -->
    <div class="grouping-controls">
      <button
        @click="groupByStatus"
        class="group-button"
        :class="{ active: grouping.includes('status') }"
      >
        Group by Status
      </button>

      <button
        @click="groupByPriority"
        class="group-button"
        :class="{ active: grouping.includes('priority') }"
      >
        Group by Priority
      </button>

      <button
        v-if="grouping.length > 0"
        @click="clearGrouping"
        class="clear-button"
      >
        Clear Grouping
      </button>
    </div>

    <!-- Table -->
    <div class="table-container">
      <table class="data-table">
        <thead>
          <tr>
            <th
              v-for="header in table.getHeaderGroups()[0].headers"
              :key="header.id"
            >
              {{ header.column.columnDef.header }}
            </th>
          </tr>
        </thead>
        <tbody>
          <tr
            v-for="row in table.getRowModel().rows"
            :key="row.id"
            :class="{ 'grouped-row': row.getIsGrouped() }"
          >
            <td v-for="cell in row.getVisibleCells()" :key="cell.id">
              <!-- Grouped cell -->
              <div v-if="cell.getIsGrouped()" class="grouped-cell">
                <button
                  class="expand-button"
                  @click="row.getToggleExpandedHandler()()"
                >
                  <span v-if="row.getIsExpanded()" class="icon">👇</span>
                  <span v-else class="icon">👉</span>
                  <span class="group-value">{{ cell.getValue() }}</span>
                  <span class="group-count"> ({{ row.subRows.length }}) </span>
                </button>
              </div>

              <!-- Aggregated cell -->
              <div v-else-if="cell.getIsAggregated()">
                {{ cell.getValue() }}
              </div>

              <!-- Placeholder cell -->
              <div v-else-if="cell.getIsPlaceholder()"></div>

              <!-- Regular cell -->
              <div v-else>
                <FlexRender
                  :render="cell.column.columnDef.cell"
                  :props="cell.getContext()"
                />
              </div>
            </td>
          </tr>

          <!-- Empty state -->
          <tr v-if="table.getRowModel().rows.length === 0">
            <td :colspan="columns.length" class="empty-table">
              No tasks found.
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<style scoped>
.row-grouping-container {
  padding: 20px;
  font-family: sans-serif;
}

.title {
  font-size: 24px;
  font-weight: 600;
  margin-bottom: 16px;
}

.grouping-controls {
  display: flex;
  gap: 10px;
  margin-bottom: 16px;
}

.group-button,
.clear-button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 14px;
}

.group-button {
  background-color: #3b82f6;
  color: white;
}

.group-button:hover {
  background-color: #2563eb;
}

.group-button.active {
  background-color: #1d4ed8;
}

.clear-button {
  background-color: #6b7280;
  color: white;
}

.clear-button:hover {
  background-color: #4b5563;
}

.icon {
  height: 16px;
  width: 16px;
  margin-right: 10px;
}

.table-container {
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  overflow: hidden;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th {
  padding: 12px 16px;
  text-align: left;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background-color: #f9fafb;
  color: #6b7280;
  border-bottom: 1px solid #e5e7eb;
}

.data-table td {
  padding: 12px 16px;
  border-bottom: 1px solid #e5e7eb;
}

.grouped-row {
  background-color: #3b82f6;
}

.grouped-cell {
  font-weight: 500;
}

.expand-button {
  display: flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  font-size: inherit;
}

.group-value {
  font-weight: 500;
}

.group-count {
  margin-left: 8px;
  color: #ffffff;
  font-size: 12px;
}

.empty-table {
  text-align: center;
  color: #6b7280;
  padding: 24px;
}
</style>
<script setup lang="ts">
import {
  FlexRender,
  getCoreRowModel,
  getExpandedRowModel,
  getGroupedRowModel,
  useVueTable,
  type ColumnDef,
  type ExpandedState,
  type GroupingState,
} from '@tanstack/vue-table'
import { ref } from 'vue'

// Define task type
interface Task {
  id: string
  title: string
  description: string
  status: { id: string; name: string }
  priority: { id: string; name: string }
  dueDate: string
}

// Dummy data
const tasks: Task[] = [
  {
    id: '1',
    title: 'Implement Authentication',
    description: 'Set up OAuth2 with JWT tokens',
    status: { id: 's1', name: 'In Progress' },
    priority: { id: 'p1', name: 'High' },
    dueDate: '2023-09-15',
  },
  {
    id: '2',
    title: 'Create Dashboard UI',
    description: 'Design and implement main dashboard',
    status: { id: 's2', name: 'To Do' },
    priority: { id: 'p2', name: 'Medium' },
    dueDate: '2023-09-20',
  },
  {
    id: '3',
    title: 'API Documentation',
    description: 'Document all API endpoints using Swagger',
    status: { id: 's1', name: 'In Progress' },
    priority: { id: 'p1', name: 'High' },
    dueDate: '2023-09-10',
  },
  {
    id: '4',
    title: 'Database Optimization',
    description: 'Improve query performance and add indexes',
    status: { id: 's3', name: 'Done' },
    priority: { id: 'p3', name: 'Low' },
    dueDate: '2023-08-30',
  },
  {
    id: '5',
    title: 'User Testing',
    description: 'Conduct user testing sessions',
    status: { id: 's2', name: 'To Do' },
    priority: { id: 'p2', name: 'Medium' },
    dueDate: '2023-09-25',
  },
]

// Define columns
const columns: ColumnDef<Task>[] = [
  {
    accessorKey: 'title',
    header: 'Title',
    cell: ({ row }) => row.original.title,
  },
  {
    accessorKey: 'status',
    header: 'Status',
    enableGrouping: true,
    accessorFn: row => row.status.name,
    cell: ({ row }) => row.original.status.name,
  },
  {
    accessorKey: 'priority',
    header: 'Priority',
    enableGrouping: true,
    accessorFn: row => row.priority.name,
    cell: ({ row }) => row.original.priority.name,
  },
  {
    accessorKey: 'description',
    header: 'Description',
    cell: ({ row }) => row.original.description,
  },
  {
    accessorKey: 'dueDate',
    header: 'Due Date',
    cell: ({ row }) => new Date(row.original.dueDate).toLocaleDateString(),
  },
]

// State for grouping
const grouping = ref<GroupingState>([])
const expanded = ref<ExpandedState>({})

// Initialize table
const table = useVueTable({
  data: tasks,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getGroupedRowModel: getGroupedRowModel(),
  getExpandedRowModel: getExpandedRowModel(),
  state: {
    get grouping() {
      return grouping.value
    },
    get expanded() {
      return expanded.value
    },
  },
  onGroupingChange: updaterOrValue => {
    grouping.value =
      typeof updaterOrValue === 'function'
        ? updaterOrValue(grouping.value)
        : updaterOrValue
  },
  onExpandedChange: updaterOrValue => {
    expanded.value =
      typeof updaterOrValue === 'function'
        ? updaterOrValue(expanded.value)
        : updaterOrValue
  },
})

// Group by status
const groupByStatus = (): void => {
  grouping.value = ['status']
}

// Group by priority
const groupByPriority = (): void => {
  grouping.value = ['priority']
}

// Clear grouping
const clearGrouping = (): void => {
  grouping.value = []
}
</script>

<template>
  <div class="row-grouping-container">
    <h1 class="title">Vue Tanstack Table Example with Row Grouping</h1>

    <!-- Grouping controls -->
    <div class="grouping-controls">
      <button
        @click="groupByStatus"
        class="group-button"
        :class="{ active: grouping.includes('status') }"
      >
        Group by Status
      </button>

      <button
        @click="groupByPriority"
        class="group-button"
        :class="{ active: grouping.includes('priority') }"
      >
        Group by Priority
      </button>

      <button
        v-if="grouping.length > 0"
        @click="clearGrouping"
        class="clear-button"
      >
        Clear Grouping
      </button>
    </div>

    <!-- Table -->
    <div class="table-container">
      <table class="data-table">
        <thead>
          <tr>
            <th
              v-for="header in table.getHeaderGroups()[0].headers"
              :key="header.id"
            >
              {{ header.column.columnDef.header }}
            </th>
          </tr>
        </thead>
        <tbody>
          <tr
            v-for="row in table.getRowModel().rows"
            :key="row.id"
            :class="{ 'grouped-row': row.getIsGrouped() }"
          >
            <td v-for="cell in row.getVisibleCells()" :key="cell.id">
              <!-- Grouped cell -->
              <div v-if="cell.getIsGrouped()" class="grouped-cell">
                <button
                  class="expand-button"
                  @click="row.getToggleExpandedHandler()()"
                >
                  <span v-if="row.getIsExpanded()" class="icon">👇</span>
                  <span v-else class="icon">👉</span>
                  <span class="group-value">{{ cell.getValue() }}</span>
                  <span class="group-count"> ({{ row.subRows.length }}) </span>
                </button>
              </div>

              <!-- Aggregated cell -->
              <div v-else-if="cell.getIsAggregated()">
                {{ cell.getValue() }}
              </div>

              <!-- Placeholder cell -->
              <div v-else-if="cell.getIsPlaceholder()"></div>

              <!-- Regular cell -->
              <div v-else>
                <FlexRender
                  :render="cell.column.columnDef.cell"
                  :props="cell.getContext()"
                />
              </div>
            </td>
          </tr>

          <!-- Empty state -->
          <tr v-if="table.getRowModel().rows.length === 0">
            <td :colspan="columns.length" class="empty-table">
              No tasks found.
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<style scoped>
.row-grouping-container {
  padding: 20px;
  font-family: sans-serif;
}

.title {
  font-size: 24px;
  font-weight: 600;
  margin-bottom: 16px;
}

.grouping-controls {
  display: flex;
  gap: 10px;
  margin-bottom: 16px;
}

.group-button,
.clear-button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 14px;
}

.group-button {
  background-color: #3b82f6;
  color: white;
}

.group-button:hover {
  background-color: #2563eb;
}

.group-button.active {
  background-color: #1d4ed8;
}

.clear-button {
  background-color: #6b7280;
  color: white;
}

.clear-button:hover {
  background-color: #4b5563;
}

.icon {
  height: 16px;
  width: 16px;
  margin-right: 10px;
}

.table-container {
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  overflow: hidden;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th {
  padding: 12px 16px;
  text-align: left;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background-color: #f9fafb;
  color: #6b7280;
  border-bottom: 1px solid #e5e7eb;
}

.data-table td {
  padding: 12px 16px;
  border-bottom: 1px solid #e5e7eb;
}

.grouped-row {
  background-color: #3b82f6;
}

.grouped-cell {
  font-weight: 500;
}

.expand-button {
  display: flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  font-size: inherit;
}

.group-value {
  font-weight: 500;
}

.group-count {
  margin-left: 8px;
  color: #ffffff;
  font-size: 12px;
}

.empty-table {
  text-align: center;
  color: #6b7280;
  padding: 24px;
}
</style>
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.