In this lesson, we will add pagination to our table. It won't be an easy one-minute process.
Modifying Controller for Pagination
app/Http/Controllers/TaskController.php
// ... public function index(){ return Inertia::render('Tasks/Index', [ 'tasks' => Task::all() 'tasks' => Task::paginate(5) ]);} // ...
Now, our Task List page will stop working because of a TypeScript structure mismatch. We return paginate()
from Controller, which is no longer the array of the Task
type. Let's fix this.
Creating Paginated Response Type
We return tasks
from the Controller not as a list but as a paginated object.
So, we need to create a specific new type for it, corresponding to the structure returned by Laravel pagination.
resources/js/types/index.d.ts
// ... export interface PaginatedResponse<T = Task | null> { current_page: number; data: T[]; first_page_url: string; from: number; last_page: number; last_page_url: string; links: { url: string | null; label: string; active: boolean; }[]; next_page_url: string | null; path: string; per_page: number; prev_page_url: string | null; to: number; total: number;}
Accepting Paginated Response in Vue
In three places, we need to change the type of data we get.
- We import our new
PaginatedResponse
type - We use it in the
export function
instead of the old list type - We change
tasks.map()
totasks.data.map()
because Laravel returns the data intasks.data
now
resources/js/pages/Tasks/Index.vue
<script>import { type BreadcrumbItem, Task } from '@/types';import { type BreadcrumbItem, PaginatedResponse, Task } from '@/types'; // ... interface Props { tasks: Task[]; tasks: PaginatedResponse<Task>;}// ...</script> <template> <TableRow v-for="task in tasks" :key="task.id"> <TableRow v-for="task in tasks.data" :key="task.id"> // ... </TableRow></template>
Ok, now our page should load successfully again.
But wait... we still don't have pagination?
Pagination Component from Shadcn
Shadcn has a component of Pagination.
So, we install it with a regular npx
command.
npx shadcn-vue@latest add pagination
We could use it right away, but I suggest considering the future when we will use the same pagination on other pages. So, we will build our own reusable pagination component suitable for our design.
Creating a "Simple" Pagination Component
Shadcn Pagination gives us the components like <Pagination>
and others. Here's an example of Usage from the docs:
Let's use those <Pagination>
, <PaginationContent>
, <PaginationItem>
and other components in our own new Vue component we'll call <Pagination>
, custom to our own design.
For now, we will add only "Next" and "Previous" links.
resources/js/components/Pagination.vue
<script setup lang="ts">import { Pagination, PaginationList, PaginationNext, PaginationPrev } from '@/components/ui/pagination';import type { PaginatedResponse } from '@/types';import { router } from '@inertiajs/vue3'; interface Props { resource: PaginatedResponse;} const props = withDefaults(defineProps<Props>(), { resource: null,});</script> <template> <Pagination :items-per-page="10" :total="100" :sibling-count="1" show-edges :default-page="2" class="mx-auto"> <PaginationList class="flex items-center gap-1"> <div v-if="props.resource.last_page === 1"> <div class="mt-4 text-center text-gray-500">No more items to show.</div> </div> <div v-if="props.resource.last_page !== 1"> <PaginationPrev v-on:click="() => router.visit(props.resource?.prev_page_url)" v-if="props.resource.prev_page_url" /> <PaginationNext v-on:click="() => router.visit(props.resource?.next_page_url)" v-if="props.resource.next_page_url" /> </div> </PaginationList> </Pagination></template>
Here's the visual result:
If we click "Next", the URL changes, and we see the "Previous" button instead.
Now, if we want to build pagination with numbers, it's more complicated.
Pagination with Numbers
Shadcn is a CSS component library. It's for the presentation layer. It doesn't contain the logic for calculating and showing the page numbers. We need to handle this logic ourselves in custom JavaScript.
It's a pretty complex logic. Luckily, Vue version comes with some helping functions like items-per-page
, total
, sibling-count
, show-edges
, and default-page
.
From here, we mixed their logic with our own custom buttons and links:
- Current page to mark the active page
- Total pages to determine the logic
- Path to generate the links for (e.g.
/tasks
) - Page query to append to the path (e.g.,
?page=
) - Switching pages with
router.visit()
resources/js/components/Pagination.vue
<script setup lang="ts">import { Button } from '@/components/ui/button';import { Pagination, PaginationEllipsis, PaginationFirst, PaginationLast, PaginationList, PaginationListItem, PaginationNext, PaginationPrev,} from '@/components/ui/pagination';import type { PaginatedResponse } from '@/types';import { router } from '@inertiajs/vue3'; interface Props { resource: PaginatedResponse;} const props = withDefaults(defineProps<Props>(), { resource: null,});</script> <template> <Pagination :items-per-page="props.resource.per_page" :total="props.resource.total" :sibling-count="1" show-edges :default-page="props.resource.current_page" class="mx-auto" > <PaginationList v-slot="{ items }" class="flex items-center gap-1"> <div v-if="props.resource.last_page === 1"> <div class="mt-4 text-center text-gray-500">No more items to show.</div> </div> <div v-if="props.resource.last_page !== 1"> <PaginationFirst v-on:click="() => router.visit(props.resource.first_page_url)" /> <PaginationPrev v-on:click="() => router.visit(props.resource.prev_page_url)" v-if="props.resource.prev_page_url" /> <template v-for="(item, index) in items"> <PaginationListItem v-if="item.type === 'page'" :key="index" :value="item.value" as-child> <Button class="h-10 w-10 p-0" :variant="item.value === props.resource.current_page ? 'default' : 'outline'" v-on:click="() => router.visit(props.resource.links[index+1].url)" > {{ item.value }} </Button> </PaginationListItem> <PaginationEllipsis v-else :key="item.type" :index="index" /> </template> <PaginationNext v-on:click="() => router.visit(props.resource.next_page_url)" v-if="props.resource.next_page_url" /> <PaginationLast v-on:click="() => router.visit(props.resource.last_page_url)" /> </div> </PaginationList> </Pagination></template>
Now we have a proper numbered pagination:
Also, if we change the pagination to 10 records per page, we show the text on the bottom that there are no more records:
Here's the GitHub commit for this lesson.
MIssing call "<Pagination :resource="tasks" />" in pages/Tasks/Index.vue.
Thank you for reporting this! I've fixed the tutorial