Writing pagination from scratch can be tricky at times.
I was recently tasked with writing a pagination component to organize a variety of items on our website. In the process, I found that using slots can be helpful in organizing multiple lists across a website, depending on what you’re trying to do.
Simply put, if you are just writing pagination for a single list in your application, just letting the pagination component handle all the logic works fine.
If you want to reuse the pagination with slots, you need to have the parent component handle a bit of the logic.
Below, you'll see some examples of when and when not to implement slots for pagination.
Without slots
This example uses a card component, but you can replace that with whatever component or item you want rendered within the pagination. This is the template I used in my Pagination.Vue component
<template>
<div>
<div class="grid">
<Card
v-for="(item, index) in displayedItems"
:key="`${item.id}--${index}`"
:item="item"
/>
</div>
<div>
<button
type="button"
:class="[
'button--link button--large',
{ isActive: page === currentPage }
]"
v-for="page in pages"
:key="page"
@click="changePage(page)"
>
{{ page }}
</button>
</div>
</div>
</template>
Pagination.vue markup
Note the v-for is going through a list called displayedItems – which is a computed property. The script is as follows for Pagination.Vue:
import { ref, computed, watch } from 'vue';
export default {
props: {
items: {
type: Array,
required: true
},
itemsPerPage: {
type: Number,
default: 10
},
},
setup(props) {
const currentPage = ref(1);
const pageCount = computed(() =>
Math.ceil(props.items.length / props.itemsPerPage)
);
const pages = computed(() => {
return Array.from({ length: pageCount.value }, (_, i) => i + 1);
});
const displayedItems = computed(() => {
const startIndex = (currentPage.value - 1) * props.itemsPerPage;
const endIndex = startIndex + props.itemsPerPage;
return props.items.slice(startIndex, endIndex);
});
function changePage(pageNumber) {
currentPage.value = pageNumber;
}
watch(currentPage, () => {
//If you want the window to scroll up on page change
//window.scrollTo({
// top: 0,
// behavior: 'smooth'
// });
});
return {
currentPage,
pageCount,
pages,
displayedItems,
changePage
};
}
};
Pagination.vue script
To call it from the parent, simply send the list and itemsPerPage as props, and it will handle the rest.
<Pagination
:items="items"
itemsPerPage=6
></Pagination>
Add styles in the pagination component as needed!
With slots
To make use of slots and have the component be reusable across the entire application, there needs to be a bit more logic handled by the parent component wherever it is used.
Move the variables of currentPage and itemsPerPage into the parent, as well as the computed property displayedItems.
Somewhere in the setup of your parent component, you should have this code:
setup(props) {
const currentPage = ref(1);
const itemsPerPage = ref(6);
const displayedItems = computed(() => {
const startIndex = (currentPage.value - 1) * itemsPerPage.value;
const endIndex = startIndex + itemsPerPage.value;
return props.courses.slice(startIndex, endIndex);
});
//pass this function to pagination component
function changePage(pageNumber: number) {
currentPage.value = pageNumber;
}
return {
currentPage,
displayedItems,
itemsPerPage,
changePage
};
}
Parent component rendering Pagination.vue
Now, the Pagination component will look like this, with a slot set up to render whatever list you want. Note the difference in props – these will be sent by the parent.
<template>
<div>
<slot></slot>
<div>
<button
type="button"
:class="[
'button--link button--large',
{ isActive: page === currentPage }
]"
v-for="page in pages"
:key="page"
@click="$emit('changePage', page)"
>
{{ page }}
</button>
</div>
</div>
</template>
<script>
import { computed } from 'vue';
export default {
//note this emit back to parent
emits: ['changePage'],
props: {
//changed to just get the length of the list
itemCount: {
type: Number,
required: true
},
itemsPerPage: {
type: Number,
default: 10
},
currentPage: {
type: Number,
required: true
}
},
setup(props) {
const pageCount = computed(() =>
Math.ceil(props.noOfItems / props.itemsPerPage)
);
const pages = computed(() => {
return Array.from({ length: pageCount.value }, (_, i) => i + 1);
});
return {
pageCount,
pages
};
}
};
</script>
Pagination.vue
You'll see that we will now emit the change page event to the parent, which will handle the logic.
The rendering from the parent now looks like this
<Pagination
:itemCount="items.length"
:itemsPerPage="itemsPerPage"
:currentPage="currentPage"
@changePage="changePage"
>
<div class="grid">
<Card
v-for="(item, index) in displayedItems"
:key="`${item.id}--${index}`"
:course="item"
/>
</div>
</Pagination>
Parent component
Slots can be useful depending on what you’re aiming to do with your page or application — but don’t be afraid to get creative with your pages and see what works. Happy coding!