Laravel and Vue.js: Pagination and Infinite Scroll Examples

Laravel and Vue.js: Pagination and Infinite Scroll Examples
Admin
Thursday, September 28, 2023 5 mins to read
Share
Laravel and Vue.js: Pagination and Infinite Scroll Examples

Data pagination is a common feature on the web. This article will cover implementing it in Vue.js components by fetching data from Laravel API. Let's quickly implement one using the laravel-vue-pagination package. Also, we will cover the "infinite scroll" pagination.

We have installed Laravel with the Laravel Breeze Vue starter-kit preset. Post Model has title and content fields, and we seeded 1000 instances.

Vue Pagination


Install Laravel Vue Pagination Package

First, install the laravel-vue-pagination package.

npm install laravel-vue-pagination

Make API Resource for Posts Model.

php artisan make:resource PostResource

Then, create an API controller for the Post Model.

app/Http/Controllers/Api/PostController.php

namespace App\Http\Controllers\Api;
 
use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
 
class PostController extends Controller
{
public function index()
{
return PostResource::collection(Post::paginate());
}
}

We return PostResource as a response, and the paginate() method has been called on the Post Model. This way, the response has all the metadata required for pagination.

Example response

{
"data": [
{
"id": 1,
"title": "Quisquam asperiores cumque aut",
"content": "It did so indeed, and much sooner than she had made the whole party at once crowded round her head. Still she went hunting about, and called out 'The Queen! The Queen!' and the baby--the fire-irons.",
"created_at": "2023-09-28T14:06:43.000000Z",
"updated_at": "2023-09-28T14:06:43.000000Z"
}
// ...
],
"links": {
"first": "http://vue-pagination.test/api/posts?page=1",
"last": "http://vue-pagination.test/api/posts?page=67",
"prev": null,
"next": "http://vue-pagination.test/api/posts?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 67,
"links": [
{
"url": null,
"label": "« Previous",
"active": false
},
{
"url": "http://vue-pagination.test/api/posts?page=1",
"label": "1",
"active": true
}
// ...
],
"path": "http://vue-pagination.test/api/posts",
"per_page": 15,
"to": 15,
"total": 1000
}
}

Now register the /posts route in the api.php file. We can reach it directly by calling the /api/posts endpoint.

routes/api.php

use App\Http\Controllers\Api\PostController;
 
Route::get('/posts', [PostController::class, 'index']);

Create a new Posts.vue page.

resources/js/Pages/Posts.vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '@inertiajs/vue3';
import { ref, onMounted } from 'vue';
import { TailwindPagination } from 'laravel-vue-pagination';
 
const posts = ref({})
 
const getPosts = (page = 1) => {
axios.get('/api/posts', { params: { page } })
.then(response => posts.value = response.data)
}
 
onMounted(getPosts)
</script>
 
<template>
<Head title="Posts" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Posts</h2>
</template>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<table>
<thead>
<tr>
<th>Name</th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts.data" :key="post.id">
<td class="p-2 border">{{ post.title }}</td>
<td class="p-2 border">{{ post.content }}</td>
</tr>
</tbody>
</table>
 
<TailwindPagination
:data="posts"
:limit="2"
@pagination-change-page="getPosts"
class="mt-4"
/>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

Here, we have a single getPosts function, called once when the page is loaded.

const getPosts = (page = 1) => {
axios.get('/api/posts', { params: { page } })
.then(response => posts.value = response.data)
}
 
onMounted(getPosts)

Then, we import the TailwindPagination component from the package.

import { TailwindPagination } from 'laravel-vue-pagination';

And with the given parameters, pagination is handled automatically for us.

<TailwindPagination
:data="posts"
:limit="2"
@pagination-change-page="getPosts"
class="mt-4"
/>

When you press the page button, the getPosts function is called again, and the page number gets passed as an argument.

Now register the route in the web.php file to preview the page by visiting the /posts URL in your browser.

routes/web.php

Route::get('/posts', function () {
return Inertia::render('Posts');
})->middleware(['auth', 'verified'])->name('posts');

Remember to compile everything.

npm run build

Infinite Scroll

Sometimes, pagination is not the option you want to implement, so we provide an alternative using infinite scroll.

Vue Infinite Scroll

To test it in action, update the Posts.vue file as follows.

resources/js/Pages/Posts.vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '@inertiajs/vue3';
import { reactive, onMounted, onBeforeUnmount } from 'vue';
 
const posts = reactive({
data: [],
links: {},
meta: {}
})
 
const getPosts = (page = 1) => {
axios.get('/api/posts', { params: { page } })
.then(response => {
posts.data = posts.data.concat(response.data.data)
posts.meta = response.data.meta
})
.finally(loadMore)
}
 
const debounce = (method, delay) => {
clearTimeout(method._tId);
method._tId = setTimeout(function(){
method();
}, delay);
}
 
const loadMore = (e) => {
let preloadHeightPx = 640;
let needsMoreContent = document.documentElement.scrollTop + window.innerHeight >= document.documentElement.offsetHeight - preloadHeightPx;
let currentPage = posts.meta.current_page
let lastPage = posts.meta.last_page
 
if (needsMoreContent && currentPage < lastPage) {
getPosts(currentPage + 1)
}
}
 
const scrollListener = () => {
debounce(loadMore, 200)
}
 
onMounted(() => {
getPosts()
window.addEventListener('scroll', scrollListener)
})
 
onBeforeUnmount(() => {
window.removeEventListener('scroll', scrollListener)
})
</script>
 
<template>
<Head title="Posts" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Posts</h2>
</template>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<table id="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts.data" :key="post.id">
<td class="p-2 border">{{ post.id }}</td>
<td class="p-2 border">{{ post.title }}</td>
<td class="p-2 border">{{ post.content }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

Here, we listen for a scroll event on the browser, and if you reach the near bottom of the page, we fetch the next page.

const loadMore = (e) => {
let preloadHeightPx = 640;
let needsMoreContent = document.documentElement.scrollTop + window.innerHeight >= document.documentElement.offsetHeight - preloadHeightPx;
let currentPage = posts.meta.current_page
let lastPage = posts.meta.last_page
 
if (needsMoreContent && currentPage < lastPage) {
getPosts(currentPage + 1)
}
}

The function getPosts is updated not to replace data but to append data to the current list of posts we have already loaded. When you load data, it checks if you have enough content and attempts to fetch the next page.

const getPosts = (page = 1) => {
axios.get('/api/posts', { params: { page } })
.then(response => {
posts.data = posts.data.concat(response.data.data)
posts.meta = response.data.meta
})
.finally(loadMore)
}

We have defined the debounce function to only fetch data when you stop scrolling because you want to avoid calling an API endpoint several times while scrolling.

const debounce = (method, delay) => {
clearTimeout(method._tId);
method._tId = setTimeout(function(){
method();
}, delay);
}