Creating a sortable, paginated table in Vue 2

Last week, I described how to create a searchable table in Vue 2. In this article, I will extend that code to and explain how to sort, and paginate that data using Vue. To get you started, I’ve provided some starter code for this article, which is predominantly the end code for the previous article, but with a lot more dummy data that will help us as we proceed here.

Let’s make the columns sortable

Having a sortable table is extremely useful, especially when you have a lot of records. We’re going to start by adding two new variables to our Vue data array…

sortBy: 'id',
sortDir: 'ASC'

…and then some conditional classes to each of the table headers

<thead>
    <tr>
        <th :class=" { 'sorted' : 'id' === sortBy, 'sorted--desc' : 'id' === sortBy && 'DESC' === sortDir }" @click="sort('id')">ID</th>
        <th :class=" { 'sorted' : 'first_name' === sortBy, 'sorted--desc' : 'first_name' === sortBy && 'DESC' === sortDir }" @click="sort('first_name')">Name</th>
        <th :class=" { 'sorted' : 'city' === sortBy, 'sorted--desc' : 'city' === sortBy && 'DESC' === sortDir }" @click="sort('city')">City</th>
        <th :class=" { 'sorted' : 'country' === sortBy, 'sorted--desc' : 'country' === sortBy && 'DESC' === sortDir }" @click="sort('country')">Country</th>
    </tr>
</thead>

To break this down, what we’re doing is adding to variables to our app, sortBy – to define which column we’re sorting by, and sortDir – to define what direction we want to sort the data in – ascending, or descending – and to toggle between the two when the active sorted column header is clicked again.

We can change these hardcoded variables to alter which column and direction we sort the table by on first load.

Secondly, a conditional class has been added to the th elements. We tell view to set the class name to sorted if the sortBy variable matches the array key of the same name, we then tell Vue to add a sorted–desc class if we are sorting that column descendingly.

Finally, we’ve added an onclick event, so that Vue knows to run the sort() function when we click a table header, and to pass through the id as the function parameter.

That was easy, right? But if we reload the page in our browser, nothing actually happens because we haven’t coded a sort() function yet, so let’s make it function!

The functioning sort

We’re actually going to add two functions to get this working. The first function will be what’s called on the header click, and will work out which column we want to sort by, whether we’re already sorting by this column, and if so, which direction to sort the data in.

That will then call a second function, called sortList, which will take that information, compare and sort the values of the array, and then return the new list. All of this will happen in the blink of an eye, meaning your table will sort as soon as you click the header.

Add the following code below the computed methods.

computed: {
    customerList: function() {
        return this.customers.filter(item => {
            if (!this.searchQuery) {
                // If there's no search term, return the item immediately
                return item;
            } else {
                // Otherwise, only return the item if we have a match
                return (
                    item.id.toString().indexOf(this.searchQuery) > -1 ||
                    item.first_name.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                    item.last_name.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                    item.city.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                    item.country.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1
                );
            }
        })
    }
},
methods: {
    sort: function(sortCol) {
        if (sortCol === this.sortBy) {
            this.sortDir = (this.sortDir !== 'ASC') ? 'ASC' : 'DESC';
        } else {
            this.sortDir = 'ASC';
        }

        this.sortBy = sortCol;

        this.sortList(this.customers, sortCol);
    },
    sortList: function(list, sortCol) {
        list.sort(function(a, b) {
            if (a[sortCol] == null || b[sortCol] == null) {
                return a;
            } else {
                if (!a[sortCol] || isNaN(a[sortCol])) {
                    return a[sortCol].localeCompare(b[sortCol]);
                } else {
                    return a[sortCol] - b[sortCol];
                }
            }
        });

        if (this.sortDir !== 'ASC') {
            list.reverse();
        }

        return list;
    }
}

If you reload your browser, you should now have a fully working, sortable table!

Limiting the amount of records on screen

The first stage of moving toward a paginated table, is to limit the amount of records (rows) on screen at any one time. With the future pagination in mind, we’re going to name our variables and functions accordingly. First we’re going to add the two variables to limit the records on screen, and which page we’re on.

currentPage: 0,
perPage: 25

We set the currentPage as zero, as that will actually be the first page when it comes to the maths of functions. As a result, page 2 will be 1 in the variable, page 3 will be 2, and so on. I’ve also initially set the perPage variable to 25. As you might suspect, the perPage variable determines how many rows are on screen at any one time.

We can now use these in our new method to calculate what records, and how many, to show

methods: {
    paginateList: function(theArray) {
        const index = this.currentPage * this.perPage;
        return theArray.slice(index, Number(index) + Number(this.perPage));
    },
    sort: function(sortCol) {

This paginateList function is supplied an array, with which it will work out the index (the starting point for the records to show), and will then return a sliced array with the amount we set perPage, beginning with the index – which is determined by the current page multiplied by the perPage limit.

However, if you refresh the page now, you’ll still get all rows, this is because we need to slightly alter our computed customerList to serve the returned by our new paginatedList method. We’re going to achieve this by changing two things.

Instead of returning the filtered customers array, we’re going to set that to a new array. We’re then going to feed that array into the paginateList function, and return the results. This will ensure that it works with our already present search filter, and sort function.

customerList: function() {
    let customersList = this.customers.filter(item => {
        if (!this.searchQuery) {
            // If there's no search term, return the item immediately
            return item;
        } else {
            // Otherwise, only return the item if we have a match
            return (
                item.id.toString().indexOf(this.searchQuery) > -1 ||
                item.first_name.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                item.last_name.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                item.city.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                item.country.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1
            );
        }
    });

    return this.paginateList(customersList);
}

We can also easily add an element to the page to control, and easily switch, how many records we show…

<div>
    Show
    <select v-model="perPage">
        <option value="10">10</option>
        <option value="25">25</option>
        <option value="50">50</option>
        <option value="100">100</option>
    </select>
    Entries
</div>

With this simple bit of code, we can easily set the value using a select dropdown which sets the perPage value with a v-model attribute. This will automatically update the variable, and the display.

Developing the pagination

Now that we have a table that limits the amount of rows, and can be customised by the user, we need a way for that user to see the records beyond the first page. The first stage of this, is to determine the total amount of records and pages, what page we’re on, and which page links to show.

computed: {
    customerList: function() {
        let customersList = this.customers.filter(item => {
            if (!this.searchQuery) {
                // If there's no search term, return the item immediately
                return item;
            } else {
                // Otherwise, only return the item if we have a match
                return (
                    item.id.toString().indexOf(this.searchQuery) > -1 ||
                    item.first_name.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                    item.last_name.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                    item.city.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                    item.country.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1
                );
            }
        });

        return this.paginateList(customersList);
    },
    endPage: function() {
        return this.currentPage + 6;
    },
    startPage: function() {
        return this.currentPage - 6;
    },
    pages: function() {
        return Math.ceil(this.results / this.perPage);
    },
    results: function() {
        return this.customers.length;
    }
}

The highlighted lines of code above show be added into your computed variables. They are fairly self explanatory, the endPage and startPage determine which pages to start and end our pagination links with. The pages and results variables determine the total amount of pages and results respectively.

Now that we have our computed variables, we can use those to display the pagination, and make it function.

<div v-if="pages > 1">
    <span v-if="currentPage > 0" @click="currentPage = currentPage - 1">&laquo; Prev</span>

    <span v-if="index == 0 || (index > startPage && index < endPage) || index == pages" v-for="(index, i) in pages">
        <span v-if="startPage > 1 && i === startPage" @click="currentPage = 0">1</span>
        <span v-if="index > endPage && endPage || startPage > 1 && i === startPage">...</span>
        <strong v-if="currentPage == index - 1" @click="currentPage = index - 1">{{ index }}</strong>
        <span v-else @click="currentPage = index - 1">{{ index }}</span>
    </span>

    <span v-if="currentPage < pages - 1" @click="currentPage++">Next &raquo;</span>
</div>

Everything you need to get the pagination displayed and functioning is in the above 12 lines, no additional functions are required; we do this by changing the currentPage variable when a page number is clicked. We can also see here a few ways that we improve the UX for the user.

  • In the first line, we conditionally hide the whole pagination if there’s only one page of records available.
  • We only show the previous page link if we’re on a page number higher than 1, but less than the total pages.
  • We then cycle through the amount of pages, to display our specific page links. We limit this in the computed startPage and endPage variables, so that we don’t display every single number if there are dozens of pages. You can change the amount (6), that we limit it by, if you’d like less/more shown.
  • We always show the number 1, so that we can quickly skip back to the first page
  • We will show an ellipsis in between the endPage (limit), and the last page number.
  • We will always show the last page number, so that we can quickly skip to the end, and so that the user can see how many pages there are in total.
  • We only show the next page link if the currently active page is below the total amount of pages.

That’s a wrap

That wraps up the tutorial, and it’s hopefully given you some insight into the basic workings of a Vue, data display, sorting, searching/filtering, pagination and various features of Vue including how to define computed variables and use them to our advantage in the DOM.

This can definitely be expanded to work more efficiently, but being the basic premise it gives you a start with which to customise to your hearts content, and put into your own projects.

Find the code on GitHub at https://github.com/mattgibbo/vue2-table/.

Leave a Reply