Tuesday 26 November 2019

VueJS Pagination

I've written about pagination before, and it's something I'm interested in so when the chance came to write a component that dealt with it at work I jumped at the chance. Rather than using any of the other techniques I've used in the past, this component was using VueJS, and I knew I'd get some data via a message bus. The data was in the form of an object with just three numbers: the current page selected, the total number of items and the number of items for display on each page.

The total number of items could range from one to many; the number of pages could also be massive depending upon the page size selected by the user.

As well as wanting to display a sensible number of page numbers to the user, I also wanted to decorate the pagination. The decoration needed to have the ability to jump three pages back and forth as well as a quick way of navigating to the first and last page.

So I did what I usually do and fired up JSFiddle to work up a quick prototype and came up with this. I've not included the message bus, but I'm instead using values within the data of the component. While I know it's not good form to alter the data within the component; I'm doing so here for illustration purposes.

Logic splits between the markup and the JS, but I quite like that for some reason.

HTML
<div id="app">
    <ol v-if="totalPages > 0" 
        aria-label="Pagination navigation">
        <li v-on:click="paginationOptions.page = 1" 
            aria-label="Go to the first page" 
            title="Go to the first page"
            v-bind:class="{
                'disabled': paginationOptions.page === 1
            }">
            <div class="content">&larr;</div>
        </li>
        <li v-if="totalPages > 3 && paginationOptions.page - 2 >= 1" 
            v-on:click="paginationOptions.page = (paginationOptions.page - 3 < 1) ? 1 : paginationOptions.page - 3" 
            aria-label="Jump three pages backward" 
            title="Jump three pages backward"><div class="content"></div></li>
        <li v-for="page in pages" 
            v-bind:key="page" 
            v-on:click="paginationOptions.page = page" 
            v-bind:aria-label="(page === paginationOptions.page) ? 'Current page, page ' + page : 'Go to page ' + page" 
            v-bind:aria-current="page === paginationOptions.page" 
            v-bind:title="(page === paginationOptions.page) ? 'Current page, page ' + page : 'Go to page ' + page"
            v-bind:class="{
                'active': page === paginationOptions.page
            }">
            <div class="content">{{page}}</div>
            <span v-if="page === paginationOptions.page" 
                  style="display: none">(current)</span>
        </li>
        <li v-if="totalPages > 3 && paginationOptions.page + 2 <= totalPages" 
            v-on:click="paginationOptions.page = (paginationOptions.page + 3 <= totalPages) ? paginationOptions.page + 3 : totalPages" 
            aria-label="Jump three pages forward" 
            title="Jump three pages forward"><div class="content"></div></li>
        <li v-on:click="paginationOptions.page = totalPages" 
            aria-label="Go to the last page" 
            title="Go to the last page"
            v-bind:class="{
                'disabled': paginationOptions.page === totalPages
            }">
            <div class="content">&rarr;</div>
        </li>
    </ol>
</div>
JS
new Vue({
    el: "#app",
    data: {
        paginationOptions: {
            page: 2,
            total: 55,
            pageSize: 10
        }
    },
    computed: {
        totalPages() {
            return Math.ceil(this.paginationOptions.total / this.paginationOptions.pageSize);
        },
        pages: function () {
            const returnArray = [];
            if(this.paginationOptions.page === 1){
                for(i = 1, count = 0; i <= this.totalPages && count < 3; i++, count++){
                    returnArray.push(i)
                }
            } else {
                if(this.paginationOptions.page === this.totalPages){
                    for(let i = this.totalPages, count = 0; i >= 1 && count < 3; i--, count++){
                        returnArray.push(i)
                    }
                    returnArray.reverse();
                } else {
                    returnArray.push(this.paginationOptions.page);
                    if(this.paginationOptions.page < this.totalPages){
                        returnArray.push(this.paginationOptions.page + 1)
                    }
                    if(this.paginationOptions.page >= 1){
                        returnArray.unshift(this.paginationOptions.page - 1)
                    }
                }
            }
            return returnArray;
        },
    }
});
SCSS
#app{
    margin: 1em;
}
ol {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    margin: 0;
    padding: 0;
    list-style-type: none;
    width: 100%;
    text-align: right;
    li{
        display: inline-block;
        &.disabled {
            cursor: not-allowed;
        }
        .content {
            text-decoration: none;
            font-weight: bold;
            background-color: #fff;
            color: #7f8c8d;
            display: flex;
            align-items: center;
            justify-content: center;
            width: 40px;
            height: 40px;
            margin: 0 3px;
            border: 1px solid #e5e5e5;
        }
        &.active {
            .content {
                border-color: #2c3e50;
                background-color: #2c3e50;
                color: #fff;
                text-decoration: none;
            }
        }
    }
}