Image

touring-vue-router

9 min read
Last update: December 19, 2021

Building Pagination

Pagination is a very common piece of functionality, found in many applications. In this lesson, let’s implement basic pagination with Vue 3. We’ll be starting from the code that Adam wrote up in our Real World Vue 3 course, which does an API call to fetch a list of events. By the end of the lesson our page will look and function like this:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.gif?alt=media&token=56e5a390-c599-48d4-8936-a474410859a4

Coding with the Course

Over the remainder of the lessons, we’ll be building features into our application and I encourage you to code along. I’ll sometimes even give you ideas on things to try to build on top of the app. You can either check out the repository, or better yet, just download the L3-start branch code (without version control) and check it into your own git repo.

As we build out functionality on each level, you can look at the starting code for each level by looking at the L#-start branch and the ending code in the L#-end branch (# being the lesson number). So for this lesson, you’ll find the starting code in the L3-start branch and ending code in the L3-end branch.

The Starting Event App

Our app currently lists events from a remote API, and looks like this:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.1604963228579.jpeg?alt=media&token=96a5ea2d-693b-45c0-ab82-dac55f975342

Just listing out six events. We’ll use the following steps to add pagination to this example:

  1. Modify the EventService API call to take “perPage” and “page”
  2. Parse & set the current page from the router using Function Mode
  3. Modify how the EventService is called from the EventList.vue
  4. Add Pagination links to the EventList Template
  5. Only show the Next page link when there is a next page
  6. Improve the Pagination Styling

1. Modifying the EventService API call

Lucky for us, the JSON Server service we’re using has this functionality built in to be able to paginate. There are two query parameters we can send into the service:

  • _limit - How many items to return per page.
  • _page - What page we are on.

Our getEvents() call currently looks like this:

📜 /src/services/EventService.js

...
getEvents() {
return apiClient.get('/events')
},
...

We’ll add two parameters to the function call, and send those into the URL:

getEvents(perPage, page) {
return apiClient.get('/events?_limit=' + perPage + '&_page=' + page)
},

That’s it.

2. Parse & Set Current page from Router

Our URL when we are on page two will look like http://localhost:8080/?page=2. Thus, we will parse out this data (if it exists) and send it in as a prop from the router, like we learned in the last lesson:

{
path: '/',
name: 'EventList',
component: EventList,
props: route => ({ page: parseInt(route.query.page) || 1 })
},

As you can see, if the page query parameter exists we parse it from a string into an integer, otherwise || 1 we set it to page one. We’ll need to accept page as a prop in our EventList.vue, which we’ll edit next:

2. Modify the EventList.vue

The component where we’re paginating is EventList.vue, which currently looks like this:

📜 /src/views/EventList.vue

...
name: 'EventList',
components: {
EventCard
},
data() {
return {
events: null,
}
},
created() {
EventService.getEvents()
.then(response => {
this.events = response.data
})
.catch(error => {
console.log(error)
})
}
...

Let’s modify this to receive page as a prop for now, and send it into the getEvents call. For now we’ll hardcode 2 as the number of events to return per page.

📜 /src/views/EventList.vue

...
name: 'EventList',
props: ['page'], // <---- receive the param as a prop, the current page components: { EventCard }, data() { return { events: null, } }, created() { EventService.getEvents(2, this.page) // <---- 2 events per page, and current page .then(response=> {
    this.events = response.data
    })
    .catch(error => {
    console.log(error)
    })
    },
    ...
    ```

    If you’re following along, it’s at this point you’ll want to run `npm install` and then `npm run dev` to get the server up and running. Here’s what we see:

    ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.gif?alt=media&token=d8e5840e-520e-489a-896e-12bd484e6f53](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.gif?alt=media&token=d8e5840e-520e-489a-896e-12bd484e6f53)

    Looks great, different pages load up different events!

    ## 3\. Adding Pagination Links

    Next we need to add pagination to the bottom of our list.

    ```html
    <template>
        <div class="events">
            <EventCard v-for="event in events" :key="event.id" :event="event" />

            <router-link :to="{ name: 'EventList', query: { page: page - 1 } }" rel="prev" v-if="page != 1">Prev Page</router-link>

            <router-link :to="{ name: 'EventList', query: { page: page + 1 } }" rel="next">Next Page</router-link>
            ...
            ```

            A few things to notice here:

            * The new `router-link` directives, in which I’m using `query:` to specify the previous and the next page by subtracting and removing 1.
            * `rel="prev"` and `rel="next"` don’t have anything to do with Vue, they’re just good SEO and standards best practice for webpages.
            * The Prev Page I’m only displaying if I’m not on the first page using a `v-if`.

            Let’s take a look at this in our browser:

            ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.gif?alt=media&token=5ce5e34a-e71a-4f8b-8760-2d3aafec1261](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.gif?alt=media&token=5ce5e34a-e71a-4f8b-8760-2d3aafec1261)

            As you can see I on my first page the data is properly paginated. However, when I click to go to the next page, nothing is happening.

            ## 🛑 Problem: The Event List Isn’t Updated

            What’s going on here, is that our router sees that we’re loading the same EventList named route, so it doesn’t need to reload the component (or rerun lifecycle hooks where our API call is stored). This is like clicking a navigation link twice. When someone clicks on a navigation link twice and they’re already on that page, do we want it to reload the component? No. That’s what’s going on. `created()` is not getting called again when we go to the second page, because it’s not reloading the component.

            Inevitably, you’ll run into this as a Vue developer: where you want to reload a component with a change of query parameters.

            ## ✅ **2 Solutions**

            There are two ways to fix this:

            1. Tell our router to reload components in our `router-view` when the full URL changes, including the query parameters. We can do this by telling our `router-view` in our App.vue to use `$route.fullPath` as it’s `key`.

            ```html
            <router-view :key="$route.fullPath" />
            ```

            We won’t do this in our application though, we’ll use the next solution which is:

            2. Watch the reactive properties for changes (which includes query parameters). We can do this by simply wrapping the our API call inside a `watchEffect` method. This is a new method in Vue 3 which just watches for reactive property changes and reruns the appropriate code if anything changes. See the Vue 3 Reactivity course if you want to learn more how it works.

            ```javascript
            import { watchEffect } from 'vue' // <--- Have to import it ... export default { ... created() { watchEffect(()=> {
                this.events = null
                EventService.getEvents(2, this.page)
                .then(response => {
                this.events = response.data
                this.totalEvents = response.headers['x-total-count']
                })
                .catch(error => {
                console.log(error)
                })
                })
                },
                ...
                ```

                You’ll notice right under `watchEffect()` I’m resetting the `this.events = null`. This is so when we load another page the current list of events is removed so the user knows that it’s loading. We could also have an animated spinner if we wanted.

                ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F5.gif?alt=media&token=a24c63dd-6f6c-4a85-9978-1a41e19e9829](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F5.gif?alt=media&token=a24c63dd-6f6c-4a85-9978-1a41e19e9829)

                Now everything is working, except on the last page you’ll see it still has `Next Page` and we can click it to get to a last page.

                ## 4\. Checking for the Last Page

                One way to figure out if we’re on the last page is to know how many total events there are, so we can calculate the total number of pages. Luckily JSON Server accounts for this, and on the headers they send back from the API call, there is a `x-total-count` header which is already sending us the total events. Sweet! We just need to pull that out of our API call, and create a new computed property that calculates if it `hasNextPage`. If true then we’ll display “Next Page”.

                ```html
                <template>
                    <div class="events">
                        ...

                        <router-link :to="{ name: 'EventList', query: { page: page + 1 } }" rel="next" v-if="hasNextPage">Next Page</router-link>
                    </div>
                </template>

                <script>
                    ...
                    data() {
                            return {
                                events: null,
                                totalEvents: 0, // <--- Added this to store totalEvents
                            }
                        },
                        created() {
                            watchEffect(() => {
                                        EventService.getEvents(2, this.page)
                                            .then(response => {
                                                this.events = response.data
                                                this.totalEvents = response.headers['x-total-count'] // <--- Store it
                                            })
                                            .catch(error => {
                                                console.log(error)
                                            })
                                    }
                                },
                                computed: {
                                    hasNextPage() {
                                        // First, calculate total pages
                                        var totalPages = Math.ceil(this.totalEvents / 2) // 2 is events per page

                                        // Then check to see if the current page is less than the total pages.
                                        return this.page < totalPages
                                    }
                                }
                        }
                </script>
                ```

                Be sure to check out the comments I left in the code above. Now it works:

                ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F6.1604597653191.gif?alt=media&token=81738348-a921-42d4-abf9-991ec0b61656](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F6.1604597653191.gif?alt=media&token=81738348-a921-42d4-abf9-991ec0b61656)

                ## 6\. Improve the Pagination Styling

                We’re done building out our Vue code, but it’d be nice if our links looked a little prettier. Let’s go ahead and pretty them up a bit using flexbox. If you don’t know flexbox, don’t worry about learning it. Below I’ve added a `div` for our pagination links, gave each an id, changed the link text, and added some styling lower down in this Single File Vue component.

                ```html
                <template>
                    <h1>Events for Good</h1>
                    <div class="events">
                        <EventCard v-for="event in events" :key="event.id" :event="event" />

                        <div class="pagination">
                            <router-link id="page-prev" :to="{ name: 'EventList', query: { page: page - 1 } }" rel="prev" v-if="page != 1">&#60; Previous</router-link>

                            <router-link id="page-next" :to="{ name: 'EventList', query: { page: page + 1 } }" rel="next" v-if="hasNextPage">Next &#62;</router-link>
                        </div>
                    </div>
                </template>

                <style scoped>
                    .events {
                        display: flex;
                        flex-direction: column;
                        align-items: center;
                    }

                    .pagination {
                        display: flex;
                        width: 290px;
                    }

                    .pagination a {
                        flex: 1;
                        text-decoration: none;
                        color: #2c3e50;
                    }

                    #page-prev {
                        text-align: left;
                    }

                    #page-next {
                        text-align: right;
                    }
                </style>
                ```

                Now our links are nicer to look at:

                ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F7.1604597656753.gif?alt=media&token=07f8507f-7a39-426a-badc-58ed8c19a1ce](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F7.1604597656753.gif?alt=media&token=07f8507f-7a39-426a-badc-58ed8c19a1ce)

                In the next lesson we’ll take a look at how to nest our routes when our application grows in complexity.