How to make a simple paginator in Vue.js and Laravel

Vue

How to make a simple paginator in Vue.js and Laravel

In the Laravel framework, all the heavy lifting of pagination is done by Laravel. We often just specify the number of paginated records and call the render in our blade file.

So, what if we decide to use a frontend framework instead of the Laravel blade? Will it be too much trouble?
Luckily not.

There are dozens of paginator plugins to choose from, however, I prefer, at least trying to come up with my own solution. I always learned better and with understanding if I recreate things I use them carelessly all the time.

Let’s begin, shall we?

Here is the idea. We will still leverage Laravel pagination. For the purposes of reusable code, we will actually create a small pagination plugin that you can just copy to any of the projects where you use Vue and Laravel. It will also be customizable so you can add features and style it in any way you want.

In this example, I am using a Laravel blade with Vue components inside. However, this paginator will work in any Vue application setup. I will have a route that returns a blade view and a route that the paginator calls to fetch records.

Let’s start from routes.

Route::get('articles', 'ArticleController@index')->name('articles.index');
Route::get('articles/json', 'ArticleController@indexJson')->name('articles.json');


I am going to quickly generate ArticleController with

php artisan make:controller ArticleController


Our index function is responsible only for returning a blade view file.

public function index(Request $request)
{
    return view('pages.article.index');
}


In the index blade, we will call an article-index vue component so let’s create it.

<div class="card shadow-sm">
    <div class="card-header">
        <h5 class="d-inline"><strong>Articles</strong></h5>
    </div>

    <div class="card-body">
        <article-index></article-index>
    </div>

</div>


Starting  code for ArticleIndex

<template>
    <div>
        <table class="table table-hover">
            <thead>
            <tr>
                <th>Title</th>
                <th>Published</th>
                <th>Created At</th>
                <th>Updated</th>
            </tr>
            </thead>
            <tbody>
                <tr v-for="(article, index) in articles" :key="article.id">

                </tr>
            </tbody>
        </table>
    </div>
</template>

<script>
    export default {
        name: "ArticleIndex",
        data() {
            return {
                articles: []
            }
        }
    }
</script>


And we also need to register ArticleComponent in app.js

Vue.component('article-index', require('./components/Article/ArticleIndex.vue').default);


Before we start working on our paginator plugin we have to create an article model, migration, seeder, factory, and resource.

Our article model

class Article extends Model
{

    protected $fillable = [
        'title', 'body', 'published', 'created_at', 'updated_at'
    ];

}


Our article migration created with:

php artisan make:migration create_articles_table
Schema::create('articles', function (Blueprint $table) {
    $table->bigIncrements('id');

    $table->string('title');
    $table->text('body');
    $table->boolean('published')->default(0);

    $table->timestamps();
});


Our article seeder created with: 

php article make:seeder ArticleTableSeeder
factory(\App\Article::class, 200)->create();


Our article factory

$factory->define(Article::class, function (Faker $faker) {
    return [
        'title'      => $faker->sentence,
        'body'       => $faker->randomHtml(),
        'published'  => $faker->boolean(50),
        'updated_at' => Carbon::today()->firstOfMonth()->addDays(rand(1, 52))->format('Y-m-d H:i:s'),
        'created_at' => Carbon::today()->firstOfMonth()->addDays(rand(1, 52))->format('Y-m-d H:i:s'),
    ];
});


Don’t forget to call ArticleTableSeeder from DatabaseSeeder

$this->call(ArticleTableSeeder::class);


Our ArticleResource created with:

php artisan make:resource ArticleResource
public function toArray($request)
{
    return [
        'title'      => $this->title,
        'body'       => $this->body,
        'published'  => $this->published,
        'created_at' => Carbon::parse($this->created_at)->format('M-d-Y'),
        'updated_at' => Carbon::parse($this->created_at)->diffForHumans(),
    ];
}


Let’s also add indexJson method to our ArticleController. This function will return our paginated article resource.

public function indexJson()
{
    return ArticleResource::collection(Article::paginate(10));
}


Now that we have the base code in place, we can start working on our paginator plugin. I will create a plugins folder inside resources/js. Inside I will create a paginator folder for our paginator.


In our index.js we will just expose the install method to the Vue constructor and register our Paginator component.

import Paginator from './Paginator';
import './css/styles.css';

const PaginatorVue = {
    install(Vue, options){

        Vue.component('paginator', Paginator);
    }
};

export default PaginatorVue;


Although, we could have avoided creating it as a plugin altogether, for future updates I kept it here. We never know where we can go with it.

<template>
    <nav aria-label="Pagination" v-if="pagination" class="mt-5">
        <ul class="pagination">
            <!-- Simple Pagination less than 5 pages -->
            <SimplePagination v-if="simple" :pagination="pagination" v-on:update-url="getData"></SimplePagination>
            <!-- Complex Pagination more than 5 pages -->
            <ComplexPagination v-else :pagination="pagination" v-on:update-url="getData"></ComplexPagination>
        </ul>
    </nav>
</template>
<script>
    import SimplePagination from './components/SimplePagination';
    import ComplexPagination from './components/ComplexPagination';

    export default {
        components: {
            SimplePagination,
            ComplexPagination
        },
        props: {
            url: {
                type: String,
                default: '',
                simple: true,
            }
        },
        data(){
            return {
                data: {},
                pagination: ''
            }
        },
        methods: {
            getData(page_url){
                let url = page_url === undefined ? this.url : this.url + page_url;

                axios.get(url)
                    .then(response => {
                        this.data = response.data.data;
                        if(this.data.length !== 0){
                            this.makePagination(response.data);
                        }
                        this.$emit('update-pagination-data', this.data);
                    })
            },
            makePagination({ meta, links }){
                const pagination = {
                    current_page: meta.current_page,
                    last_page:    meta.last_page,
                    to:           meta.to,
                    from:         meta.from,
                    total:        meta.total,
                    next_page:    links.next,
                    prev_page:    links.prev
                };

                this.pagination = pagination;
                this.simple = pagination.last_page < 6;
            }
        },
        created(){
            this.getData();
        }
    }
</script>


Our pagination will work in two modes. Simple and complex. If we have less than five pages we will use simple pagination otherwise we will hide extra links with three dots so we don’t end up having hundreds of pagination buttons.

SimplePagination Component

<template>

    <span>
        <!-- Left Arrow -->
        <li class="page-item" v-bind:class="{ 'disabled': !pagination.prev_page }">
            <a v-on:click.prevent="linkClick(pagination.current_page - 1)" class="page-link" href="#" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
                <span class="sr-only">Previous</span>
            </a>
        </li>

        <!-- Middle -->
        <li class="page-item" v-for="index in pagination.last_page" v-bind:key="index"
            v-bind:class="{ 'active' : pagination.current_page === index }">
            <a v-on:click.prevent="linkClick(index)" class="page-link" href="#">
                {{ index }}
            </a>
        </li>

        <!-- Right Arrow -->
        <li class="page-item" v-bind:class="{'disabled': !pagination.next_page} ">
            <a v-on:click.prevent="linkClick(pagination.current_page + 1)" class="page-link" href="#" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
                <span class="sr-only">Next</span>
            </a>
        </li>
    </span>

</template>
<script>
    export default {
        props: {
            pagination: {
                type: Object
            },
        },
        methods: {
            linkClick(event){
                this.$emit('update-url', '?page=' + event);
            }
        }
    }
</script>
<style scoped>
    span {
        display: inherit;
    }
</style>


ComplexPagination Component

<template>
    <span>
        <!-- Left Arrow -->
        <li class="page-item" v-bind:class="{ 'disabled': !pagination.prev_page }">
            <a v-on:click.prevent="linkClick(pagination.current_page - 1)" class="page-link" href="#" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
                <span class="sr-only">Previous</span>
            </a>
        </li>

        <li class="page-item" v-bind:key="1" v-bind:class="{ 'active' : pagination.current_page === 1 }">
            <a v-on:click.prevent="linkClick(1)" class="page-link" href="#">1</a>
        </li>

        <li v-if="pagination.current_page > 4" class="page-item disabled" aria-disabled="true"><span class="page-link">...</span></li>

        <li v-if="calculateLowerBound(pagination.current_page, 2)"
            class="page-item">
            <a v-on:click.prevent="linkClick(pagination.current_page - 2)"
               class="page-link" href="#">
                {{ pagination.current_page - 2 }}
            </a>
        </li>

        <li v-if="calculateLowerBound(pagination.current_page, 1)"
            class="page-item">
            <a v-on:click.prevent="linkClick(pagination.current_page - 1)"
               class="page-link" href="#">
                {{ pagination.current_page - 1 }}
            </a>
        </li>

        <li v-if="pagination.current_page !== 1 && pagination.current_page !== pagination.last_page"
            v-bind:class="{ 'active' : pagination.current_page }"
            class="page-item">
            <a v-on:click.prevent="linkClick(pagination.current_page)" class="page-link" href="#">
                {{ pagination.current_page }}
            </a>
        </li>

        <li v-if="calculateUpperBound(pagination.current_page, 1)"
            class="page-item">
            <a v-on:click.prevent="linkClick(pagination.current_page + 1)" class="page-link" href="#">
                {{ pagination.current_page + 1 }}
            </a>
        </li>

        <li v-if="calculateUpperBound(pagination.current_page, 2)"
            class="page-item">
            <a v-on:click.prevent="linkClick(pagination.current_page + 2)" class="page-link" href="#">
                {{ pagination.current_page + 2 }}
            </a>
        </li>

        <li v-if="pagination.current_page < (pagination.last_page - 3)" class="page-item disabled" aria-disabled="true"><span class="page-link">...</span></li>

        <li class="page-item" v-bind:key="pagination.last_page"
            v-bind:class="{ 'active' : pagination.current_page === pagination.last_page }">
            <a v-on:click.prevent="linkClick(pagination.last_page)" class="page-link" href="#">
                {{ pagination.last_page }}
            </a>
        </li>

        <!-- Right Arrow -->
        <li class="page-item" v-bind:class="{'disabled': !pagination.next_page} ">
            <a v-on:click.prevent="linkClick(pagination.current_page + 1)" class="page-link" href="#" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
                <span class="sr-only">Next</span>
            </a>
        </li>
    </span>
</template>
<script>
    export default {
        props: {
            pagination: {
                type: Object
            },

        },
        methods: {
            linkClick(event){
                this.$emit('update-url', '?page=' + event);
            },
            calculateLowerBound(current, value){
                return current - value !== 0 && current - value !== 1 && current - value !== -1;
            },
            calculateUpperBound(current, value){
                return current + value < this.pagination.last_page;
            }
        }
    }
</script>
<style scoped>
    span {
        display: inherit;
    }
</style>


In styles.css we only have

.pagination{
    justify-content: center;
}


This is how it finally looks.

This is by no means a be all end all solution. It is a mere example of how you can create a simple paginator yourself.
One of the things you can add to make it a full working paginator is support for search and filters.

Let it be a task for you to implement it.

Challenge yourself.

Show comments

Laravel

5 min

How to make Laravel authentication

Laravel provides a neat function that quickly generates a scaffold of routes, views, and controllers used for authentication.

Laravel

7 min

How to install Laravel application

This article will cover how to install and create a local development environment for your Laravel application. There are only a couple of steps that needs to be done.

Laravel

3 min

How to set appropriate Laravel permissions on Linux server

After publishing your Laravel application to a real web server, you discover then some of your functionalities are failing.

Codinary