Pagination is a solution for breaking up large results into separate pages and the user is provided with navigation tools to display these pages.
Paginating resources in web applications can be very helpful not only performance-wise but also from a user experience perspective.
In this article, you will learn how to create a dynamic and reusable Vue.js pagination component.
To complete this tutorial, you will need:
This tutorial was verified with Node v16.4.0, npm
v7.19.0, and Vue v2.6.11.
A pagination component should allow the user to go to the first and last pages, move forward and backward, and change directly to a page in a close range.
We want to render a button to go to the first page, the previous one, range number of pages, next page, and last one:
[first] [next] [1] [2] [3] [previous] [last]
Most applications make an API request every time the user changes the page. We need to make sure our component allows us to do so, but we don’t want to make the request within the component. This way we’ll make sure the component is reusable throughout the whole application and the request is all made in the actions/service layer. We can accomplish this by triggering an event with the number of the page the user clicked.
There are several possible ways to implement pagination on an API endpoint.
If the API only informs about the total number of records, we can calculate the total number of pages by dividing the number of results by the number of results per page: totalResults / perPage
.
For this example, let’s assume our API informs us about the number of results per page (perPage
), the total number of pages (totalPages
), and the current page (currentPage
). These will be our dynamic props.
Although we want to render a range of pages, we do not want to render all available pages. Let’s also allow to configure the maximum number of visible buttons as a prop in our component (maxVisibleButtons
).
Now that we know what we want our component to do and which data we’ll need, we can set the HTML structure and the needed props.
You can use @vue/cli
to create a new Vue project:
- npx @vue/cli create --default vue-pagination-example
Then navigate to the newly created project directory;
- cd vue-pagination-example
Create a Pagination.vue
file and open it with your code editor:
<template>
<ul>
<li>
<button
type="button"
>
First
</button>
</li>
<li>
<button
type="button"
>
Previous
</button>
</li>
<!-- Visible Buttons Start -->
<!-- ... -->
<!-- Visible Buttons End -->
<li>
<button
type="button"
>
Next
</button>
</li>
<li>
<button
type="button"
>
Last
</button>
</li>
</ul>
</template>
<script>
export default {
props: {
maxVisibleButtons: {
type: Number,
required: false,
default: 3
},
totalPages: {
type: Number,
required: true
},
perPage: {
type: Number,
required: true
},
currentPage: {
type: Number,
required: true
}
}
};
</script>
At this point, your component will render out four buttons in a list. In order to get the range of visible buttons, we’ll use a for
loop.
We need to make sure the number is never bigger than the prop that sets the maximum number of visible buttons and also not bigger than the number of available pages.
The start number of our cycle depends on the current page:
The end number of our cycle also needs some calculations. We need to get the smallest number between the total number of pages and the position where we want to stop. To calculate the position where we want to stop, we need the sum of the position where we want to start plus the maximum number of visible buttons. Because we always want to show one button to the left hand of the current page, we need to subtract 1 from this number.
Let’s use a computed property that returns an array of objects with a range of visible pages. Each object will have a prop for the page number and another that will tell us whether the button should be disabled or not. After all, we don’t want the user to click for the page they’re already on.
In order to render this array of pages, we’ll use the v-for
directive. For more complex data structures, it’s recommended to provide a key
with each v-for
. Vue uses the key
value to find which element needs to be updated, when this value is not provided, Vue uses a “in-place patch” strategy. Although the data we are using is simple enough, let’s provide the key
value - if you use eslint-vue-plugin
with the vue3-essential
rules, you will always need to provide the key
value.
Revisit the Pagination.vue
file and add startPage()
and pages()
:
<template>
<ul>
...
<!-- Visible Buttons Start -->
<li
v-for="page in pages"
:key="page.name"
>
<button
type="button"
:disabled="page.isDisabled"
>
{{ page.name }}
</button>
</li>
<!-- Visible Buttons End -->
...
</ul>
</template>
<script>
export default {
...
computed: {
startPage() {
// When on the first page
if (this.currentPage === 1) {
return 1;
}
// When on the last page
if (this.currentPage === this.totalPages) {
return this.totalPages - this.maxVisibleButtons;
}
// When inbetween
return this.currentPage - 1;
},
pages() {
const range = [];
for (
let i = this.startPage;
i <= Math.min(this.startPage + this.maxVisibleButtons - 1, this.totalPages);
i++
) {
range.push({
name: i,
isDisabled: i === this.currentPage
});
}
return range;
},
}
};
</script>
Now the logic for the maximum visible buttons is finished.
Now we need to inform the parent component when the user clicks in a button and which button the user has clicked.
We need to add an event listener to each of our buttons. The v-on
directive allows to listen for DOM events. In this example, we’ll use the v-on
shorthand to listen for the click event.
In order to inform the parent, we’ll use the $emit
method to emit an event with the page clicked.
Let’s also make sure the pagination buttons are only active if the page is available. In order to do so, we’ll make use of v-bind
to bind the value of the disabled
attribute with the current page. We’ll also use the :
shorthand for v-bind.
In order to keep our template cleaner, we’ll use the computed properties to check if the button should be disabled. Using computed properties will also cache values, which means that as long as currentPage
won’t change, other requests for the same computed property will return the previously computed result without having to run the function again.
Revisit the Pagination.vue
file and add checks for the current page and click event methods:
<template>
<ul>
<li>
<button
type="button"
@click="onClickFirstPage"
:disabled="isInFirstPage"
>
First
</button>
</li>
<li>
<button
type="button"
@click="onClickPreviousPage"
:disabled="isInFirstPage"
>
Previous
</button>
</li>
<!-- Visible Buttons Start -->
<li
v-for="page in pages"
:key="page.name"
>
<button
type="button"
@click="onClickPage(page.name)"
:disabled="page.isDisabled"
>
{{ page.name }}
</button>
</li>
<!-- Visible Buttons End -->
<li>
<button
type="button"
@click="onClickNextPage"
:disabled="isInLastPage"
>
Next
</button>
</li>
<li>
<button
type="button"
@click="onClickLastPage"
:disabled="isInLastPage"
>
Last
</button>
</li>
</ul>
</template>
<script>
export default {
...
computed: {
...
isInFirstPage() {
return this.currentPage === 1;
},
isInLastPage() {
return this.currentPage === this.totalPages;
},
},
methods: {
onClickFirstPage() {
this.$emit('pagechanged', 1);
},
onClickPreviousPage() {
this.$emit('pagechanged', this.currentPage - 1);
},
onClickPage(page) {
this.$emit('pagechanged', page);
},
onClickNextPage() {
this.$emit('pagechanged', this.currentPage + 1);
},
onClickLastPage() {
this.$emit('pagechanged', this.totalPages);
}
}
}
</script>
Now the logic for clicking buttons is finished.
Now that our component checks all functionalities we initially wanted, we need to add some CSS to make it look more like a pagination component and less like a list.
We also want our users to be able to clearly identify which page they are on. Let’s change the color of the button representing the current page.
In order to so we can bind an HTML class
to our active page button using the object syntax. When using the object syntax to bind class names, Vue will automatically toggle the class when the value changes.
Although each block inside a v-for
has access to the parent scope properties, we’ll use a method
to check if the page is active in order to keep our template cleaner.
Revisit the pagination.vue
file and add checks for the active page and CSS styles and classes:
<template>
<ul class="pagination">
<li class="pagination-item">
<button
type="button"
@click="onClickFirstPage"
:disabled="isInFirstPage"
>
First
</button>
</li>
<li class="pagination-item">
<button
type="button"
@click="onClickPreviousPage"
:disabled="isInFirstPage"
>
Previous
</button>
</li>
<!-- Visible Buttons Start -->
<li
v-for="page in pages"
class="pagination-item"
>
<button
type="button"
@click="onClickPage(page.name)"
:disabled="page.isDisabled"
:class="{ active: isPageActive(page.name) }"
>
{{ page.name }}
</button>
</li>
<!-- Visible Buttons End -->
<li class="pagination-item">
<button
type="button"
@click="onClickNextPage"
:disabled="isInLastPage"
>
Next
</button>
</li>
<li class="pagination-item">
<button
type="button"
@click="onClickLastPage"
:disabled="isInLastPage"
>
Last
</button>
</li>
</ul>
</template>
<script>
export default {
...
methods: {
...
isPageActive(page) {
return this.currentPage === page;
}
}
}
</script>
<style>
.pagination {
list-style-type: none;
}
.pagination-item {
display: inline-block;
}
.active {
background-color: #4AAE9B;
color: #ffffff;
}
</style>
Now the logic for styling active buttons is finished.
At this point, you can use the component in your app. You will need to simulate an API call by providing values for totalPages
and perPage
.
For this example, currentPage
should be set to 1
and onPageChange
will log out the active page.
<template>
<div id="app">
<pagination
:totalPages="10"
:perPage="10"
:currentPage="currentPage"
@pagechanged="onPageChange"
/>
</div>
</template>
<script>
import Pagination from './components/Pagination.vue'
export default {
name: 'App',
components: {
Pagination
},
data () {
return {
currentPage: 1,
};
},
methods: {
onPageChange(page) {
console.log(page)
this.currentPage = page;
}
}
}
</script>
At this point, you can run the application:
- npm run serve
Open the application in a browser and observe the pagination component. There will be a First, Previous, Next, and Last button. Three page buttons out of the total 10 pages will also be displayed. Since the first page is the current page, the 1 button is indicated with an active class. Also since the first page is the current page, the First and Previous buttons are disabled.
A live code example is available on CodePen.
In this article, you learned how to create a dynamic and reusable Vue.js pagination component.
If you’d like to learn more about Vue.js, check out our Vue.js topic page for exercises and programming projects.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
If you have only 2 pages and you go to page number 2, the start page is 0, you can even go to negative numbers.
Great Article Filipa. I noticed a small bug in the
startPage
computed property. There may be a need to subtract 1 fromthis.maxVisibleButtons
when the current page is equal to the total pages. That way iftotalPages
is 12 andmaxVisibleButtons
is 3 thestartPage
will be12 - (3 - 1) = 10
and pages will be (10, 11, 12). This should look something like: