Tutorial

How To Build a Reusable Pagination Component with Vue.js

Vue.js

Introduction

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.

Prerequisites

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.

Step 1 —Setting Up the Project

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:

src/components/Pagination.vue
<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:

  1. When the current page is the first one, let’s show the user the current one and the next ones.
  2. When the current page is the last one, let’s show the last page and the previous ones.
  3. For anything in between let’s show the previous page and the next one(s).

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():

src/components/Pagination.vue
<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.

Step 2 —Adding Event Listeners

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:

src/components/Pagination.vue
<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.

Step 3 —Adding Styles

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:

src/components/Pagination.vue
<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.

Step 4 — Using the Component

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.

src/App.vue
<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.

Conclusion

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.

Creative Commons License