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

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

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

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

ユーザビリティを考えた場合にページネーションではなく無限スクロールのほうが都合がいいという場合下記の記事を参考にしてみてください。無限スクロールはスクロールを行うとつぎつぎ新しい情報が最終行に追加されていく機能です。

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

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

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


$users = User::paginate(15);

paginateメソッドを利用することでLaravelからvue.js側に戻されるデータは取得した15件のデータだけではなく、全体の件数やページの数、取得した現在のページ番号も一緒に戻してくれます。


{
   "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の値を利用します。上記の例ではlast_pageの値から全体で4ページあり、1ページにアクセスしていることがわかります。

環境の構築

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コマンドを実行します。
fukidashi

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: [],
      current_page:1,
    };
  },
  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例にすぎません。他にもさまざまな方法でページネーションを実現することができます。

ここではテーブルに100件のデータが入っており5件ずつ取得することができるため20ページが存在することを前提に進めていきます。

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

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

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

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

表示の3つのパターン

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

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

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

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

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

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

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

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

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

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

…(ドット)を利用するほどページ数が多くない場合については最後に説明を行っています。
fukidashi

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 ]と表示されます。current_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つ以外のページ番号の配列を作成することができます。current_pageの値を変更するとことで異なる値を持つ配列が表示されます。(rangeは5, ページ数は20の場合)


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>

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


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

.active {
  background-color: #337ab;
  color:white;
}
.inactive{
  color: #337ab;
}

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

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

四角の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クラスを追加します。


.disabled {
  cursor: not-allowed;
}

front_dot, end_dotのliタグにdisabledクラスを設定します。


<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つの四角を作る等のルールは必要はありません。

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

rangeに前後の2を足した値よりlast_pageが小さいかどうかチェックを行うcomputedプロパティsizeCheckを追加します。


sizeCheck() {
  if (this.last_page <= this.range + 4) {
    return false;
  }
  return true;
},

frontPageRange、middlePageRange, endPageRangeの3つで sizeCheckのチェックを行いますが、frontPageRangeのみを使ってページ番号の配列を作成します。dotは必要ないのでfalseに設定します。


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

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


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

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

Laravel側でページ内に表示する件数を5から20件に変更すると5ページになるため下記のように表示されれば正常に動作していることになります。

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

バックエンドにLaravel、フロントエンドにvue.jsを利用してページネーションを自作することができました。Laravel以外でもページネーションを作成したい場合はバックエンドからデータを取得する度に現在のページ、取得件数、ページ数、全体の件数がわかればこの方法が利用できることになります。ページ数は全体の件数を取得件数から割ればいいので必須ではありません。

綺麗なコートではありませんがページネーションがこういった方法で作成できるのだということがわかられば役に立つ場面もあるかと思います。