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/.

Creating a searchable table in Vue 2

Vue has become one of my favourite front-end tools to create manageable, data-driven, front-end pages and modules.

One of the most useful modules for data, is representing it in tabular form, and to have that data searchable, sortable, and nicely paginated. This short article will provide a sample that will hopefully give you a good starting point to bring Vue into your project, and expand the component to your needs.

For the purposes of simplicity, I’m going to keep this article to the code, without exploring more advanced features like single-file components, webpack etc.

Getting prepared

First up, you’ll need your tools. Since this is a fairly basic tutorial, all you’ll need is a decent text/code editor, such as Atom, and of course, VueJS.

The basic page code

In case you’re planning to copy-and-paste large parts of this code, I’ve included everything you need in this basic page code, including the latest stable copy of Vue.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Vue2 Table</title>
</head>
<body>

    <div id="myApp" v-cloak>
        <table>
            <thead>
                <tr>
                    <th>Col 1</th>
                    <th>Col 2</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>Data 1</td>
                    <td>Data 2</td>
                </tr>
            </tbody>
        </table>
    </div>

    <script src="https://unpkg.com/vue"></script>
</body>
</html>

Let’s get Vue plugged in!

Now that we have a (very) basic outline of our page, and table, we can begin to plug Vue in so that we can start playing with the data and display. I’m going to assume you already have a basic understanding of Vue, and I’m going to jump straight into setting it up, without a great deal of explanation.

First up, we’re going to add a style. The v-cloak attribute that we’ve added to the main #myApp div will allow us to hide the element until it has been fully rendered by Vue.

<style>
    [v-cloak] {
        display: none;
    }
</style>

We’re now going to add the JavaScript to render the app, and add a quick bit of sample data to ensure it’s working

<script>
    var vm = new Vue({
        el: '#myApp',
        data: {
            message: 'Hello World!'
        }
    });
</script>

You’ll now want to change the content of the first td element in the tbody, to allow us to see that Vue is working, and rendering our data. Change the markup for the first column to this…

<td>{{ message }}</td>

After re-loading the page, you should now have a column that says “Hello World!” Great! We’re up and running.

Adding some real data for the table

Now that we know it’s working and rendering, we can add some sample tabular data to work with. As you probably already know, the best way to provide a lot of data to Vue, especially with things like tabular data (e.g. a list of customers), is with JSON. So replace the javascript with the code below, including our array of JSON objects, to provide a fake list of customers that we can render on the frontend, in our table.

<script>
    var vm = new Vue({
        el: '#myApp',
        data: {
            customers: [{"name":"John Doe","age":"25","nationality":"British"},{"name":"Jane Doe","age":"21","nationality":"Scottish"},{"name":"Donald Frump","age":"70","nationality":"American"}]
        }
    });
</script>

With this data, we can update our markup to display it within the table. Again, I’m going to be presumptuous and assume you already know the basics of if statements and for loops. If not, I will explain a bit about what’s going on, but I’d recommend taking a look at the Vue documentation, which can explain it far better than I.

<div id="myApp" v-cloak>
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Age</th>
                <th>Nationality</th>
            </tr>
        </thead>
        <tbody>
            <tr v-for="customer in customers">
                <td>{{ customer.name }}</td>
                <td>{{ customer.age }}</td>
                <td>{{ customer.nationality }}</td>
            </tr>
        </tbody>
    </table>
</div>

As you can see, I’ve hard-coded the table column headings. We can (and should) make these dynamic, to make it more manageable, but since this is only an example, I’m taking the easy road, as there are more important things to look at, and it works in the same way as the body of the table.

What’s going on in that code!?

You can see I’ve added a v-for loop on the tr element. This means that Vue will add a table row for each row in the customers array. The row will be assigned to the an object called customer, which we can then drill down to the individual columns/data.

I’ve then added a td element for each column of data, and within that added the Vue code to output the data I want.

When you reload this page on the frontend, you should now see three rows in your table, with the data for each of the three people (yes, Frump was intentional).

Let’s make the data searchable!

Okay, we now have a working app with a table that is using our data! We can now easily swap out that array of JSON objects to data we’ve pulled from a database, but as this is a basic tutorial focussing on creating the table component, I’m going to skip that part and move on to making the data searchable.

First, let’s add an input into the page and assign it, via a vue model, to a data variable.

<div id="myApp" v-cloak>
    <input type="text" name="q" v-model="searchQuery" />
    <table>
<script>
    var vm = new Vue({
        el: '#myApp',
        data: {
            customers: [{"name":"John Doe","age":"25","nationality":"British"},{"name":"Jane Doe","age":"21","nationality":"Scottish"},{"name":"Donald Frump","age":"70","nationality":"American"}],
            searchQuery: ''
        }
    });
</script>

Now we’ve set that up, we need to search our data for the query provided. This is actually very straight forward, all we need to do is filter the existing object (customers), and feed back a new object, containing the results, back to our table. We’re going to do this using a computed property.

var vm = new Vue({
    el: '#myApp',
    data: {
        customers: [{"name":"John Doe","age":"25","nationality":"British"},{"name":"Jane Doe","age":"21","nationality":"Scottish"},{"name":"Donald Frump","age":"70","nationality":"American"}],
        searchQuery: ''
    },
    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.name.toLowerCase().indexOf(this.searchQuery.toLowerCase()) > -1 ||
                        item.age.indexOf(this.searchQuery) > -1 ||
                        item.nationality.indexOf(this.searchQuery) > -1
                    );
                }
            })
        }
    }
});

If you now reload the page, and try to use the search input, you’ll notice nothing happens, that’s because we’re still output the unfiltered customers object to the table, so let’s change that to use the new filtered customerList object.

<tbody>
    <tr v-for="customer in customerList">
        <td>{{ customer.name }}</td>
        <td>{{ customer.age }}</td>
        <td>{{ customer.nationality }}</td>
    </tr>
</tbody>

Now, reload the webpage in your browser, and type something into the input field. You should notice it’s now filtering the table results, and only showing you items matching your search term. You can easily customise this to search any and all fields in the base object (i.e. you could remove nationality to only search by name and age).

Hopefully you’ve found this easy to follow, and get started with. In the next article, I’ll be explaining how to sort, and paginate these results, to provide your data in smaller, more manageable chunks.

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