データベースに保存するデータが増えてくると画面上での表示件数を制御するためにページネーションが必要になります。vue.jsでアプリケーションでページネーションが必要な場合はVuefityを使ったり専用ライブラリのvue-paginateを使っている人もいるかもしれません。本文書ではバックエンドにLaravelを利用してvue.jsを使ってページネーションを自作する方法について説明を行っています。

作成する自作ページネーションはLaravel環境以外で利用することができます。またLaravelを利用しますがLaravel環境の構築の手順については本文書では説明を行っていません。

作成後のページネーションは下記のように動作します。

ページネーションの動作
ページネーションの動作

ユーザビリティを考えた場合にページネーションではなく無限スクロールのほうが都合がいいという場合下記の記事を参考にしてみてください。

Laravelのページネーションについて

今回はvue.jsのバックエンドにLaravelを利用していますがどのようなバックエンドサーバを利用してもページネーションを作成することができます。しかしページネーションを実現するためには全体で何件のデータが保存されているかや1回のデータ取得で何件のデータが入っているのかわかっておく必要があります。

Laravelではページネーションを利用する際に1ページあたりに何件のデータをいれるかをpaginateメソッドで指定することができます。下記では1ページあたり5件のデータが入るように指定を行っています。


$users = User::paginate(15);

またvue.js側に戻されるデータは取得した5件のユーザデータだけではなく、全体の件数やページの数、取得した現在のページ番号も一緒に戻してくれます。


{
   "total": 50, //ページ数
   "per_page": 15, //1ページあたりの件数
   "current_page": 1, //アクセスした現在のページ
   "last_page": 4, //ページ数
   "first_page_url": "http://laravel.app?page=1",
   "last_page_url": "http://laravel.app?page=4",
   "next_page_url": "http://laravel.app?page=2",
   "prev_page_url": null,
   "path": "http://laravel.app",
   "from": 1,
   "to": 15,
   "data":[
        {
            // Result Object
        },
        {
            // Result Object
        }
   ]
}

本文書ではこの中からデータを取得したcurrent_pageとlast_pageを利用します。

環境の構築

Laravel側の設定

LaravelのUsersテーブルに入った100件のデータを利用します。vue.jsからaxiosを使ってLaravelにアクセスを行いますがアクセスするURLは下記となります。pageの値を変えることでアクセスするページを変更します。


/api/users?page=ページ番号

api.webファイルにルーティングを追加しています。


Route::get('/users', 'UserController@index');

UserController.phpファイルにindexメソッドを追加してUserモデルを使って5件ずつユーザテーブルから情報を取得しています。Laravelでページネーションを利用する場合はpaginateメソッドを利用します。Laravelが自動で処理を行うため、ページ番号に関する処理コードは一切記述していません。


namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\User;

class UserController extends Controller
{

    public function index()
    {
        $users = User::paginate(5);

        return $users;
    }
}

vueファイルの初期設定

vue.jsのコードはUser.vueファイルを作成してその中に記述していきます。

resouces¥js¥componentsの下にUser.vueファイルを作成します。User.vueファイルを利用するためにapp.jsファイルで下記を追加します。


Vue.component('User', require('./components/User.vue').default);

welcome.blade.phpファイルを下記のように書き換え、User.vueファイルの内容を表示できるように設定します。


<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>
        <script src="{{ asset('js/app.js') }}" defer></script>
    </head>
    <body>
    <div id="app">
        <User></User>
    </div>
    </body>
</html>
設定が完了したらnpm run dev(npm run watch)コマンドを実行してJavaScriptファイルのビルドを忘れずに行ってください。Laravelの開発サーバを利用する時はphp artisan serveコマンドを実行します。

Laravelからのデータ取得

動作確認のためLaravelからvue.jsのaxiosを利用して5件のデータが取得できるか確認を行います。


<template>
  <div>
    <ul>
      <li v-for="user in users" :key="user.id">{{ user.id}}: {{ user.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      users: []
    };
  },
  methods: {
    async getUsers() {
      const result = await axios.get(`/api/users?page=${this.current_page}`);
      const users = result.data;
      this.users = users.data;
    }
  },
  created() {
    this.getUsers();
  }
};
</script>

ブラウザで確認するとv-forで展開した5件のユーザデータが表示されれば正常に環境設定は行われています。

ユーザ情報の表示
ユーザ情報の表示

Paginationの考え方

本文書で説明しているページネーションの考え方は1例にすぎません。他にもさまざまな方法でページネーションを実現することができます。

作成したいページネーション

ページネーションには前のページと次のページに移動できるシンプルなものからbootstrapライブラリを利用して作成できるようなページネーションなど各種あります。

本文書ではLaravelでデフォルトで利用できるページネーションの見た目を参考に下記のようなものを作成していきます。

Laravelのページネーション
Laravelのページネーション
外観を似せて作るだけでLaravel本体の中で行われているロジックについては調べていないためLaravelのものと同じものではありません。

表示の3つのパターン

参考にするLaravelのページネーションの表示には以下の3つのパターンがあることを理解しておく必要があります。

  1. 後半部分に…(ドット)が1回入る場合
  2. 前半部分と後半部分に1回ずつ…(ドット)が入る場合
  3. 前半部分に…(ドット)が1回入る場合
…はページ数が多いためにページ間のページを省略するために利用します。

1つ目のパターンでは下記のような表示になります。

後半部分に...が入る
後半部分に…が入る場合

2つ目のパターンでは下記のような表示になります。

前半と後半に...が入る
前半と後半に…が入る場合

3つ目のパターンでは下記のような表示になります。

後半部分に...が入る場合
後半部分に…が入る場合

3つのパターンを見ると共通しているのが数字を持つ四角は必ず9個表示させています。また最初の1,2と最後の19,20の2つは必ず表示されています。最初の1,2はどのサイズのページ数の場合でも同じ数字が入りますが、最後の19,20の数字はページ数によって代わり最後のページ番号とその前のページ番号が入ることになります。最初と最後以外に5個の四角があります。

最初の2個と最後の2個以外の残りの5個をどのように表示させるかがポイントになります。

1つ目のパターンの対応方法

5ページ目までであればどのページにアクセスしても下記の表示になります。

5までは前半にくっつく
5までは先頭と一緒に表示

6ページ目になると先頭の2つと離れることになります。

6になると先頭と離れる
6になると先頭と離れる

箱の中の数字は数字の配列をv-forで展開することで表示させるという前提で説明を行っていきます。

現在のページが5以下の場合は1,2を覗いた3から7までの配列を作成する必要があります。それは下記のコードで実現します。この5は先頭の2つと最後2つを覗いた9-4=5の数でこれをrangeとします。


let current_page = 3
let start = "";
let end = "";
const range = 5
if (current_page <= range) {
  start = 3;
  end = range + 2;
}
const range_array = [];
for (let i = start; i <= end; i++) {
  range_array.push(i);
}
console.log(range_array)

実行すると[ 3, 4, 5, 6, 7 ]と表示されます。currnt_pageの数字を1から5のどの数字を入れても同じ結果になります。

これに先頭の1, 2があれば現在のページが5以下であれば[1,2,3,4,5,6,7]の配列をつくることができます。

3つ目のパターンの対応方法

3のパターンは5個の四角が最後の2つと一緒に表示されるパターンです。

今度は16ページから20ページまでのアクセスであれば最後の2つと一緒に表示されます。

16ページから20ページならくっついて表示
16ページから20ページならくっついて表示

15ページになると最後の2つと離れることになります。

15ページは最後と離れて表示
15ページは最後と離れて表示

これをコードにすると以下のようになります。


let current_page = 18
let start = "";
let end = "";
const range = 5
const last_page = 20

if (current_page > last_page - range) {
    start = last_page - range - 1;
    end = last_page - 2;
}

const range_array = [];
for (let i = start; i <= end; i++) {
  range_array.push(i);
}
console.log(range_array)

current_pageが16-20を設定するば結果は、[ 14, 15, 16, 17, 18 ]となります。これに最後の2つの19,20を加えると[ 14, 15, 16, 17, 18 , 19, 20]の配列になります。

ここまでで1-5, 16-20の場合の配列の作成方法がわかりました。

2つ目のパターンの対応方法

1つ目と3つ目以外のcurrent_pageの場合は下記のコードを利用します。

中身は現在のページと前後2個の数字を配列として表示させるといったシンプルなものです。


let current_page = 7
let start = "";
let end = "";
const range = 5

start = current_page - Math.floor(range / 2);
end = current_page + Math.floor(range / 2);

const range_array = [];
for (let i = start; i <= end; i++) {
  range_array.push(i);
}
console.log(range_array)

currnet_pageの数字を6にすると前後2つの数字と一緒に[ 4, 5, 6, 7, 8 ]と表示されます。10の場合は[ 8, 9, 10, 11, 12 ]と表示されます。1-5, 16-20以外のページはこの処理で対応します。

3つのパターンを利用

先程説明した3つの条件を組み合わせることで先頭の2つと最後の2つ以外のページ番号の配列を作成することができます。


let current_page = 16
let start = "";
let end = "";
const range = 5
const last_page = 20

if (current_page <= range) {
    start = 3;
    end = range + 2;
} else if (current_page > last_page - range) {
    start = last_page - range - 1;
    end = last_page - 2;
} else {
    start = current_page - Math.floor(range / 2);
    end = current_page + Math.floor(range / 2);
}

const range_array = [];
for (let i = start; i <= end; i++) {
  range_array.push(i);
}
console.log(range_array)

これでページネーションに重要な箇所の説明は完了でこのロジックを使ってページネーションを作成していきます。

ページネーションの作成

これまでに説明してきたロジックを元にページネーションを作成していきます。

プロパティの追加

新たにページネーションに利用するプロパティを追加します。range以外はgetUsersメソッドの結果から値を挿入します。current_pageの1はデフォルト値でページにアクセスすると1ページ目のデータが表示されます。rangeは変更可能です。


data() {
  return {
    users: [],
    current_page: 1,
    last_page: "",
    range: 5,
  }
},

axiosを使って取得したデータを挿入しています。


async getUsers() {
  const result = await axios.get(`/api/users?page=${this.current_page}`);
  const users = result.data;
  this.current_page = users.current_page;
  this.last_page = users.last_page;
  this.users = users.data;
}

配列作成メソッドの追加

startとendの値を受け取るとその数字を使って配列を作成するcalRangeメソッドを追加します。


calRange(start, end) {
  const range = [];
  for (let i = start; i <= end; i++) {
    range.push(i);
  }
  return range;
},

computedプロパティの追加

先頭の1,2と真ん中の5個、最後の2個の配列を別々のcomputedプロパティで管理し、それぞれのcomputedプロパティをv-forで展開しページネーションとして表示させます。複雑に見えるmiddlePageRangeプロパティは”Paginationの考え方”で作成したコードを利用しています。


  computed: {
    frontPageRange() {
      return this.calRange(1, 2);
    },
    middlePageRange() {
      let start = "";
      let end = "";
      if (this.current_page <= this.range) {
        start = 3;
        end = this.range + 2;
      } else if (this.current_page > this.last_page - this.range) {
        start = this.last_page - this.range - 1;
        end = this.last_page - 2;
      } else {
        start = this.current_page - Math.floor(this.range / 2);
        end = this.current_page + Math.floor(this.range / 2);
      }
      return this.calRange(start, end);
    },
    endPageRange() {
      return this.calRange(this.last_page - 1, this.last_page);
    }
  },

User.vueのtemplateタグの中で追加した3つのcomputedプロパティを展開します。


<template>
  <div>
    <ul>
      <li v-for="page in frontPageRange" :key="page">{{ page }}</li>
      <li v-for="page in middlePageRange" :key="page">{{ page }}</li>
      <li v-for="page in endPageRange" :key="page">{{ page }}</li>
    </ul>
    <ul>
      <li v-for="user in users" :key="user.id">{{ user.id}}: {{ user.name }}</li>
    </ul>
  </div>
</template>

ブラウザで確認するとCSSを使っていないのでリストとしてページ番号が表示されます。現在のcurrent_pageは1なので期待した表示になっています。

ページ番号が表示
ページ番号が表示

これにCSSを適用してます。ulタグにclassでpaginationを追加し、User.vueファイルのstyleタグに以下を設定します。


.pagination {
  display: flex;
  list-style-type: none;
}
.pagination li {
  border: 1px solid #ddd;
  padding: 6px 12px;
  text-align: center;
}

CSSを適用するとページネーションらしくなってきました。

CSS適用後
CSS適用後

ページの移動

ページ番号をクリックするとそのページの内容が表示されるようにliにclickイベントを追加します。


<ul class="pagination">
  <li v-for="page in frontPageRange" :key="page" @click="changePage(page)">{{ page }}</li>
  <li v-for="page in middlePageRange" :key="page" @click="changePage(page)">{{ page }}</li>
  <li v-for="page in endPageRange" :key="page" @click="changePage(page)">{{ page }}</li>
</ul>

clickイベントに設定しているchangePageメソッドを追加します。引数はページ番号です。


changePage(page) {
  if (page > 0 && page <= this.last_page) {
    this.current_page = page;
    this.getUsers();
  }
},

ページ番号をクリックすると表示される内容とページネーションの形が変わっていることが確認できます。ここでは8ページのクリックしています。

ページ番号をクリックすると表示内容が変わる
ページ番号をクリックすると表示内容が変わる

現在のページの背景色を変える

ページネーションから現在表示されているページがどこなのかわかるように背景色を変えます。

classバインディングを利用して現在のページ番号の場合はactiveクラスを適用しそれ以外にはinactiveを適用します。


<ul class="pagination">
  <li
    v-for="page in frontPageRange"
    :key="page"
    @click="changePage(page)"
    :class="(isCurrent(page)) ? 'active' : 'inactive'"
  >{{ page }}</li>
  <li
    v-for="page in middlePageRange"
    :key="page"
    @click="changePage(page)"
    :class="(isCurrent(page)) ? 'active' : 'inactive'"
  >{{ page }}</li>
  <li
    v-for="page in endPageRange"
    :key="page"
    @click="changePage(page)"
    :class="(isCurrent(page)) ? 'active' : 'inactive'"
  >{{ page }}</li>
</ul>
<ul>

現在のページがどうか判別するためのisCurrentメソッドを追加します。


isCurrent(page) {
  return page === this.current_page;
},

ブラウザで確認すると現在表示されているページ番号のみ背景色が設定されていることがわかります。

背景色設定後
背景色設定後

四角のborderが一部かぶって太く表示されているので以下のCSSもstyleタグの中に追加します。


.pagination li + li {
  border-left: none;
}

…(ドット)を追加する

ページ間に間がある場合に表示する…(ドット)が表示されていないので新たに…(ドット)を表示・非表示を制御するデータプロパティfront_dot, end_dotを追加します。


data() {
  return {
    users: [],
    current_page: 1,
    last_page: "",
    range: 5,
    front_dot: false,
    end_dot: false
  };
},

liタグを下記の2箇所に追加しています。v-showによりfront_dotとend_dotによって表示・非表示を制御しています。


<ul class="pagination">
  <li
    v-for="page in frontPageRange"
    :key="page"
    @click="changePage(page)"
    :class="(isCurrent(page)) ? 'active' : 'inactive'"
  >{{ page }}</li>
  <li v-show="front_dot" class="inactive">...</li>
  <li
    v-for="page in middlePageRange"
    :key="page"
    @click="changePage(page)"
    :class="(isCurrent(page)) ? 'active' : 'inactive'"
  >{{ page }}</li>
  <li v-show="end_dot" class="inactive">...</li>
  <li
    v-for="page in endPageRange"
    :key="page"
    @click="changePage(page)"
    :class="(isCurrent(page)) ? 'active' : 'inactive'"
  >{{ page }}</li>
</ul>

これらの値についてはmiddlePageRange内の条件により表示・非表示が決定されるのでmiddlePageRangeメソッドの中で設定を行います。


middlePageRange() {
  let start = "";
  let end = "";
  if (this.current_page <= this.range) {
    start = 3;
    end = this.range + 2;
    this.front_dot = false;
    this.end_dot = true;
  } else if (this.current_page > this.last_page - this.range) {
    start = this.last_page - this.range - 1;
    end = this.last_page - 2;
    this.front_dot = true;
    this.end_dot = false;
  } else {
    start = this.current_page - Math.floor(this.range / 2);
    end = this.current_page + Math.floor(this.range / 2);
    this.front_dot = true;
    this.end_dot = true;
  }
  return this.calRange(start, end);
},

設定後ブラウザ確認すると…が表示されるようになりました。

...が表示されます。
…が表示されます。

…はクリックできないのでその上にカーソルを載せた時に禁止マークが表示されるようにstyleタグにdisabledクラスを追加し、front_dot, end_dotのliタグにdisabledクラスを設定します。


.disabled {
  cursor: not-allowed;
}

<li v-show="front_dot" class="inactive disabled">...</li>

前後移動処理を追加

これまでの設定でページ番号をクリックするとそのページに移動することができましたが次は前後に移動するliタグを追加します。

先頭につけるliを設定します。ページを1つ戻る処理になるのでタグの中は&laquo;を設定します。先頭に追加するのでcurrent_pageが1の場合は戻る場所がないのでdisabledクラスを設定します。clickイベントでは現在のページから1を引いています。


<li
  class="inactive"
  :class="(current_page == 1) ? 'disabled' : ''"
  @click="changePage(current_page-1)"
>«</li>

一番最後につけるliタグを設定します。前に進む処理なのでタグの中は&raquo;設定します。changePageの引数は現在のページに1をプラスします。


<li
  class="inactive"
  :class="(current_page >= last_page) ? 'disabled' : ''"
  @click="changePage(current_page+1)"
>»</li>

前後の移動(<<, >>)の四角を押してもページが移動することを確認してください。

前後に移動するボタンを追加
前後に移動するボタンを追加

ページが少ない場合

データベースに保存されたデータが少ない場合はページ数が少ないためページネーションに…(ドット)などを利用する必要はありません。決まられたサイズ以下の場合はドットや先頭に2つ最後に2つの四角を作る等のルールは必要はありません。

データプロパティsizeを設定し、ページ数であるlast_pageの値がsizeより小さい場合はページ数の数だけ四角が並びます。

ページ数と表示される四角の数が同じ
ページ数と表示される四角の数が同じ

sizeよりlast_pageが小さいかどうかチェックを行うcomputedプロパティを追加します。


sizeCheck() {
  if (this.last_page < this.size) {
    return false;
  }
  return true;
},

frontPageRange、middlePageRange, endPageRangeの3つでチェックを行いますが、frontPageRangeを使ってページ番号の配列を作成します。


frontPageRange() {
  if (!this.sizeCheck) {
    return this.calRange(1, this.last_page);
  }
  return this.calRange(1, 2);
},

そのほかの2つはチェックを行い条件を満たす場合は空の配列を戻します。


middlePageRange() {
  if (!this.sizeCheck) return [];

Laravel側でページ内に表示する件数を5から20件に変更すると5ページになるため下記のように表示されます。

5ページの場合
5ページの場合

vue.jsを利用してページネーションを自作することができました。