Article image

HOW TO MAKE A SIMPLE PAGINATOR IN VUE.JS AND LARAVEL

Javascript -

Sep 30 2019

Dino Numic

In 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 frontend framework instead of 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 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 Laravel blade with Vue components inside. However, this paginator will work in any Vue application setup. I will have a route which returns a blade view and a route which 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 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 base code in place, we can start working on our paginator plugin. I will create a plugins folder inside resourecs/js. Inside I will create paginator folder for our paginator.

In our index.js we will just expose install method to 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;
}

Now we need to modify our article-index component with our new paginator and modified table.

<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"><th>{{ article.title }}</th>
                    <th>{{ article.published }}</th>
                    <th>{{ article.created_at }}</th>
                    <th>{{ article.updated_at }}</th>
                </tr>
            </tbody>
        </table>


    <!-- Pagination -->

        <paginator:url ="'/admin/articles/json'"v-on:update-pagination-data="updatePaginationData"
        ></paginator>
    </div>
</template>

<script>
    export default {
        name: "ArticleIndex",
        data() {
            return {
                articles: []
            }
        },
        methods: {
            updatePaginationData(event) {
                this.articles = event;
            }
       }
    }
</script>

This is how it finally looks when everything is pieced together.


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 are support for search and filters. Let it be a task for you to implement it. Challenge yourself!