Skip to content

feat(VDataTable): expose prevPage, nextPage, setPage in bottom slot#22681

Merged
J-Sek merged 1 commit intovuetifyjs:masterfrom
juanjcardona13:fix/data-table-bottom-slot-pagination
Mar 8, 2026
Merged

feat(VDataTable): expose prevPage, nextPage, setPage in bottom slot#22681
J-Sek merged 1 commit intovuetifyjs:masterfrom
juanjcardona13:fix/data-table-bottom-slot-pagination

Conversation

@juanjcardona13
Copy link
Copy Markdown
Contributor

@juanjcardona13 juanjcardona13 commented Mar 4, 2026

What

Exposes prevPage, nextPage, and setPage in the bottom slot of v-data-table and v-data-table-server, so custom pagination can be built without v-model:page or manual handlers—aligned with the footer slot of v-data-iterator.

Changes

  • packages/vuetify/src/components/VDataTable/composables/paginate.tsusePaginatedGroups return type and all three branches (item, group, any) now return prevPage, nextPage, setPage from the paginate callback.
  • packages/vuetify/src/components/VDataTable/VDataTable.tsx — Added prevPage, nextPage, setPage to VDataTableSlotProps and to slotProps; paginate callback returns them from providePagination.
  • packages/vuetify/src/components/VDataTable/VDataTableServer.tsx — Destructure and pass prevPage, nextPage, setPage in the bottom slotProps.
  • packages/vuetify/src/components/VDataTable/VDataTableVirtual.tsx — Omit prevPage, nextPage, setPage from VDataTableVirtualSlotProps (virtual table has no pagination).

How to test

Contents of packages/vuetify/dev/Playground.vue used for testing:

<template>
  <v-app>
    <v-container class="py-8">
      <v-row>
        <v-col cols="12">
          <h1 class="text-h4 mb-2">
            VDataTable custom footer — Before & after
          </h1>
          <p class="text-body-1 text-medium-emphasis mb-6">
            The <strong>bottom</strong> slot now exposes <code>prevPage</code>,
            <code>nextPage</code>, and <code>setPage</code>, so you can build
            custom pagination without <code>v-model:page</code> or manual handlers.
          </p>
        </v-col>
      </v-row>

      <v-row align="stretch">
        <!-- Before: manual state and handlers -->
        <v-col cols="12" md="6">
          <v-card class="h-100" variant="outlined">
            <v-card-title class="bg-grey-darken-2 text-white py-3">
              <v-icon class="me-2" start>mdi-code-braces</v-icon>
              Before
            </v-card-title>
            <v-card-text class="pa-4">
              <p class="text-caption text-medium-emphasis mb-3">
                Requires <code>v-model:page</code>, a local ref, and manual
                <code>handlePrevPage</code> / <code>handleNextPage</code> with
                manual <code>pageCount</code> calculation.
              </p>

              <v-data-table
                v-model:page="tablePage"
                :headers="headers"
                :items="items"
                :items-per-page="5"
                hide-default-footer
              >
                <template #bottom="{ page, pageCount }">
                  <div class="d-flex align-center justify-space-between pa-4">
                    <v-btn
                      :disabled="page === 1"
                      size="small"
                      variant="outlined"
                      @click="handlePrevPage"
                    >
                      <v-icon start>mdi-chevron-left</v-icon>
                      Previous
                    </v-btn>
                    <span class="text-body-2">Page {{ page }} of {{ pageCount }}</span>
                    <v-btn
                      :disabled="page === pageCount"
                      size="small"
                      variant="outlined"
                      @click="handleNextPage"
                    >
                      Next
                      <v-icon end>mdi-chevron-right</v-icon>
                    </v-btn>
                  </div>
                </template>
              </v-data-table>

              <v-divider class="my-4" />

              <div class="text-caption">
                <strong>Template:</strong>
                <pre class="mt-2 pa-2 bg-grey-lighten-4 rounded overflow-x-auto"><code>{{ beforeTemplate }}</code></pre>
                <strong class="mt-2 d-block">Script:</strong>
                <pre class="mt-1 pa-2 bg-grey-lighten-4 rounded overflow-x-auto"><code>{{ beforeScript }}</code></pre>
              </div>
            </v-card-text>
          </v-card>
        </v-col>

        <!-- After: slot props only -->
        <v-col cols="12" md="6">
          <v-card class="h-100" variant="outlined">
            <v-card-title class="bg-success py-3">
              <v-icon class="me-2" start>mdi-check-circle</v-icon>
              After
            </v-card-title>
            <v-card-text class="pa-4">
              <p class="text-caption text-medium-emphasis mb-3">
                Use <code>prevPage</code>, <code>nextPage</code>, and
                <code>setPage</code> from the slot — no refs or handlers needed.
              </p>

              <v-data-table
                :headers="headers"
                :items="items"
                :items-per-page="5"
                hide-default-footer
              >
                <template #bottom="{ prevPage, nextPage, page, pageCount }">
                  <div class="d-flex align-center justify-space-between pa-4">
                    <v-btn
                      :disabled="page === 1"
                      size="small"
                      variant="outlined"
                      @click="prevPage"
                    >
                      <v-icon start>mdi-chevron-left</v-icon>
                      Previous
                    </v-btn>
                    <span class="text-body-2">Page {{ page }} of {{ pageCount }}</span>
                    <v-btn
                      :disabled="page === pageCount"
                      size="small"
                      variant="outlined"
                      @click="nextPage"
                    >
                      Next
                      <v-icon end>mdi-chevron-right</v-icon>
                    </v-btn>
                  </div>
                </template>
              </v-data-table>

              <v-divider class="my-4" />

              <div class="text-caption">
                <strong>Template only (no script logic):</strong>
                <pre class="mt-2 pa-2 bg-grey-lighten-4 rounded overflow-x-auto"><code>{{ afterTemplate }}</code></pre>
              </div>
            </v-card-text>
          </v-card>
        </v-col>
      </v-row>

      <v-row>
        <v-col cols="12">
          <v-card class="mt-4" density="comfortable" variant="tonal">
            <v-card-text class="d-flex align-center py-3">
              <v-icon class="me-2" size="small">mdi-information-outline</v-icon>
              <span>
                This improvement was inspired by the ease of use of
                <strong>v-data-iterator</strong>, whose <strong>footer</strong>
                slot has always exposed <code>prevPage</code>, <code>nextPage</code>,
                and <code>setPage</code> for declarative custom pagination.
              </span>
            </v-card-text>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </v-app>
</template>

<script setup lang="ts">
  import { ref } from 'vue'

  const items = ref([
    { name: 'Frozen Yogurt', calories: 159 },
    { name: 'Ice cream sandwich', calories: 237 },
    { name: 'Eclair', calories: 262 },
    { name: 'Cupcake', calories: 305 },
    { name: 'Gingerbread', calories: 356 },
    { name: 'Jelly bean', calories: 375 },
    { name: 'Lollipop', calories: 392 },
    { name: 'Honeycomb', calories: 408 },
    { name: 'Donut', calories: 452 },
    { name: 'KitKat', calories: 518 },
    { name: 'Brownie', calories: 466 },
    { name: 'Cookie', calories: 501 },
  ])

  const headers = [
    { title: 'Name', key: 'name' },
    { title: 'Calories', key: 'calories' },
  ]

  // Before: manual state and handlers
  const tablePage = ref(1)
  const handlePrevPage = () => {
    if (tablePage.value > 1) tablePage.value--
  }
  const handleNextPage = () => {
    const itemsPerPage = 5
    const pageCount = Math.ceil(items.value.length / itemsPerPage)
    if (tablePage.value < pageCount) tablePage.value++
  }

  const beforeTemplate = `<template #bottom="{ page, pageCount }">
  <v-btn @click="handlePrevPage">Previous</v-btn>
  <v-btn @click="handleNextPage">Next</v-btn>
</template>`

  const beforeScript = `const tablePage = ref(1)
const handlePrevPage = () => { ... }
const handleNextPage = () => {
  const pageCount = Math.ceil(items.length / 5)
  if (tablePage.value < pageCount) tablePage.value++
}`

  const afterTemplate = `<template #bottom="{ prevPage, nextPage, page, pageCount }">
  <v-btn @click="prevPage">Previous</v-btn>
  <v-btn @click="nextPage">Next</v-btn>
</template>`
</script>

<style scoped>
pre {
  font-size: 0.75rem;
}

code {
  font-family: 'Courier New', monospace;
}
</style>

Expose prevPage, nextPage, and setPage in the bottom slot of v-data-table
and v-data-table-server so custom pagination can be built without
v-model:page or manual handlers, aligned with v-data-iterator footer slot.

- paginate.ts: usePaginatedGroups return type and branches now return
  prevPage, nextPage, setPage from the paginate callback.
- VDataTable.tsx: add prevPage, nextPage, setPage to VDataTableSlotProps
  and slotProps; paginate callback returns them from providePagination.
- VDataTableServer.tsx: destructure and pass prevPage, nextPage, setPage
  in bottom slotProps.
- VDataTableVirtual.tsx: omit prevPage, nextPage, setPage from
  VDataTableVirtualSlotProps (no pagination in virtual table).

Made-with: Cursor
@J-Sek J-Sek added T: enhancement Functionality that enhances existing features C: VDataTable labels Mar 4, 2026
@J-Sek J-Sek merged commit 4d1aa79 into vuetifyjs:master Mar 8, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C: VDataTable T: enhancement Functionality that enhances existing features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants