ページネーションは通常のサイトでもWebアプリでもほとんどの場合に必要となる定番のパーツです。
頻出するからこそ、できるだけ簡単に設置できてカスタマイズもしやすいように、ということでページネーションの実装について考えてみました。
実際に動作するデモ
※何かキーワードを入力して検索してみてください。
ADs
データを全件取得して指定件数ごとに分割して多ページに分割するという方法もありますが、APIからのデータ取得件数に上限があったり総データ数が数千~数万になる場合は厳しいのではないかと思います。
そこでページ遷移ごとに1ページ分ずつデータを取得する方法を取りました。
#1のときは1ページ目、#2のときは2ページ目とハッシュによってページ数を指定する方法は指定ページへの直リンクができるのでいい方法ですが、同一ページに複数のページネーションがある場合に使えませんので今回は見送りました。
設置や管理のことを考えるとコンポーネントとし、親コンポーネントに埋め込むように扱えるほうがいいでしょう。
コンポーネント設置部分は以下のようにpropsで必要データを渡します。
1 2 3 4 5 6 7 8 |
<pagenation :showPages="showPages" //ページネーションを何ページ表示するか(奇数にしないと現ページが中央に来ない) :currentPage="currentPage" //現在のページ :totalCount="totalCount" //総記事数(APIからの返り値を使う) :perPage="perPage" //1ページの表示件数 :totalPages="totalPages" //総ページ数(APIからの返り値を使う) @currentPage="getCurrentPage" //ページネーションコンポーネントから受け取る関数 ></pagenation> |
APIからのデータ取得はこの親コンポーネントで行います。
そこで得られた件数などの必要なデータをページネーションコンポーネントに渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
methods: { //currentPageがページネーションコンポーネントから送られる現在のページ getCurrentPage(currentPage) { var vm = this; vm.$set(vm, "currentPage", currentPage); //APIを呼び直す vm.search(vm.keyword, vm.currentPage); }, //APIからのデータ取得(今回は楽天商品検索APIを使用) //APIによってデータのとり方やキー名は変わるので都度考えないといけない search(keyword, page) { if (!keyword) { return; } var vm = this; var url = "api.php"; var param = { keyword: keyword, page: page, hits: vm.perPage, }; vm.$axios .get(url, { params: param, }) .then(function (response) { console.log(response); if (response.data) { vm.$set(vm, "list", response.data.Items); vm.$set(vm, "totalCount", response.data.count); vm.$set(vm, "currentPage", response.data.page); vm.$set(vm, "totalPages", response.data.pageCount); } }) .catch(function (error) { //エラーのとき console.log(error); }); }, } |
ページネーション本体は以下のように
・1ページ目、最終ページへのリンク
・前後のページへのリンク
・カレントページが中央に来る
という見た目です。
※ Bootstrap4での使用を想定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<div class="row py-3 justify-content-center" v-if="totalPages"> <div class="col-auto"> <nav aria-label="Page navigation"> <ul class="pagination"> <!-- 1ページ目に戻るリンク --> <li class="page-item" :class="{'disabled': currentPageEdited == 1}"> <a class="page-link" href="#" v-on:click.prevent="setPage(1);"><<</a> </li> <!-- 1ページ前に戻るリンク --> <li class="page-item" :class="{'disabled': currentPageEdited == 1}"> <a class="page-link" href="#" v-on:click.prevent="setPage(currentPageEdited -1);" :class="{'disable':currentPageEdited == 1}" ><</a> </li> <!-- ここからページ数分のリンクを生成 --> <li class="page-item" v-for="num in showPagesFix" :key="num" :class="{'active' : numFix(num) == currentPageEdited}" > <template v-if="numFix(num) == currentPageEdited"> <span class="page-link">{{ numFix(num) }}</span> </template> <a class="page-link" href="#" v-on:click.prevent="setPage(numFix(num))" v-else >{{ numFix(num) }}</a> </li> <!-- 1ページ次に進むリンク --> <li class="page-item" :class="{'disabled': currentPageEdited == totalPages}"> <a class="page-link" href="#" v-on:click.prevent="setPage(currentPageEdited + 1);">></a> </li> <!-- 最後のページに進むリンク --> <li class="page-item" :class="{'disabled': currentPageEdited == totalPages}"> <a class="page-link" href="#" v-on:click.prevent="setPage(totalPages);">>></a> </li> </ul> </nav> </div> </div> |
Propsは変数名を定義するだけです。
1 2 3 4 5 6 7 |
props: { showPages: Number, //ページネーションを何件表示するか currentPage: Number, //現在のページ totalCount: Number, //総件数 totalPages: Number, //総ページ数 perPage: Number, //1ページあたりの表示件数 }, |
Computedでは
・ページ番号の計算
・ページ数の計算
を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
computed: { //ページ番号を計算する numFix() { var vm = this; return function (num) { var ajust = 1 + (vm.showPages - 1) / 2; var result = num; //前ページがマイナスになる場合は1からはじめる if (vm.currentPageEdited > vm.showPages / 2) { var result = num + vm.currentPageEdited - ajust; } //後ページが最大ページを超える場合は最大ページを超えないようにする if (vm.currentPageEdited + vm.showPages / 2 > vm.totalPages) { var result = vm.totalPages - vm.showPages + num; } //総ページ数が表示ページ数に満たない場合、連番そのまま if (vm.totalPages <= vm.showPages) { var result = num; } return result; }; }, //総記事数が表示ページ数以下の場合に調整する showPagesFix() { var vm = this; if (vm.totalPages < vm.showPages) { return vm.totalPages; } else { return vm.showPages; } }, }, |
methodsではページネーションをクリックしたときのsetPage(ページ番号)という関数を定義するだけです。
現在のページ数をページネーションコンポーネントに保存しつつ、親コンポーネントに$emitで送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
setPage(page) { var vm = this; //マイナスにならないようにする if (page <= 0) { vm.$set(vm, "currentPageEdited", 1); } //最大ページを超えないようにする else if (page > vm.totalPages) { vm.$set(vm, "currentPageEdited", vm.totalPages); } else { vm.$set(vm, "currentPageEdited", page); } //親コンポーネントに現在のページを送る vm.$emit("currentPage", vm.currentPageEdited); }, |
これでページネーションが動作するようになりました。
APIからのデータ取得部分だけは扱うAPIによって書き換える必要がありますが、それ以外の部分は汎用的に使えるのではないかと思います。
デモで使っているコードそのままです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
<template> <div class="row py-3 justify-content-center" v-if="totalPages"> <div class="col-auto"> <nav aria-label="Page navigation"> <ul class="pagination"> <li class="page-item" :class="{'disabled': currentPageEdited == 1}"> <a class="page-link" href="#" v-on:click.prevent="setPage(1);"><<</a> </li> <li class="page-item" :class="{'disabled': currentPageEdited == 1}"> <a class="page-link" href="#" v-on:click.prevent="setPage(currentPageEdited -1);" :class="{'disable':currentPageEdited == 1}" ><</a> </li> <li class="page-item" v-for="num in showPagesFix" :key="num" :class="{'active' : numFix(num) == currentPageEdited}" > <template v-if="numFix(num) == currentPageEdited"> <span class="page-link">{{ numFix(num) }}</span> </template> <a class="page-link" href="#" v-on:click.prevent="setPage(numFix(num))" v-else >{{ numFix(num) }}</a> </li> <li class="page-item" :class="{'disabled': currentPageEdited == totalPages}"> <a class="page-link" href="#" v-on:click.prevent="setPage(currentPageEdited + 1);">></a> </li> <li class="page-item" :class="{'disabled': currentPageEdited == totalPages}"> <a class="page-link" href="#" v-on:click.prevent="setPage(totalPages);">>></a> </li> </ul> </nav> </div> </div> </template> <script> export default { props: { showPages: Number, //ページネーションを何件表示するか currentPage: Number, //現在のページ totalCount: Number, //総件数 totalPages: Number, //総ページ数 perPage: Number, //1ページあたりの表示件数 }, watch: { //ページネーションを複数設置したときの対応 currentPage(val) { var vm = this; vm.$set(vm, "currentPageEdited", vm.currentPage); }, }, data() { return { currentPageEdited: Number, //現在のページ }; }, computed: { //ページ番号を計算する numFix() { var vm = this; return function (num) { var ajust = 1 + (vm.showPages - 1) / 2; var result = num; //前ページがマイナスになる場合は1からはじめる if (vm.currentPageEdited > vm.showPages / 2) { var result = num + vm.currentPageEdited - ajust; } //後ページが最大ページを超える場合は最大ページを超えないようにする if (vm.currentPageEdited + vm.showPages / 2 > vm.totalPages) { var result = vm.totalPages - vm.showPages + num; } //総ページ数が表示ページ数に満たない場合、連番そのまま if (vm.totalPages <= vm.showPages) { var result = num; } return result; }; }, //総記事数が表示ページ数以下の場合に調整する showPagesFix() { var vm = this; if (vm.totalPages < vm.showPages) { return vm.totalPages; } else { return vm.showPages; } }, }, mounted() { var vm = this; vm.$set(vm, "currentPageEdited", vm.currentPage); }, methods: { //何ページ目を表示するか setPage(page) { var vm = this; //マイナスにならないようにする if (page <= 0) { vm.$set(vm, "currentPageEdited", 1); } //最大ページを超えないようにする else if (page > vm.totalPages) { vm.$set(vm, "currentPageEdited", vm.totalPages); } else { vm.$set(vm, "currentPageEdited", page); } //親コンポーネントに現在のページを送る vm.$emit("currentPage", vm.currentPageEdited); }, }, }; </script> |
「api.php」は楽天商品検索APIの結果をecho file_get_contents()しているだけです。
(API KEYを隠すためだけの対応)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
<template> <div class="container"> <div class="row py-3 align-items-center"> <div class="col-auto"> <h1>楽天検索</h1> </div> <div class="col"> <input type="text" class="form-control" v-model="keyword" v-on:keydown.enter="search(keyword,1)" /> </div> <div class="col-auto"> <button class="btn btn-primary" v-on:click="search(keyword,1)">検索</button> </div> </div> <div class="row" v-if="totalCount"> <div class="col"> <strong>{{totalCount}}</strong> 件の結果が見つかりました。 </div> </div> <pagenation :showPages="showPages" :currentPage="currentPage" :totalCount="totalCount" :perPage="perPage" :totalPages="totalPages" @currentPage="getCurrentPage" ></pagenation> <div class="row flex-column" v-if="list"> <div class="col col-item" v-for="(item,index) in list" :key="index"> <a :href="item.Item.affiliateUrl" target="_blank" class="text-dark border p-2 d-block mb-3" rel="noopener noreferrer"> <div class="row"> <div class="col-auto"> <img :src="item.Item.mediumImageUrls[0].imageUrl" class="img-fluid" v-if="item.Item.mediumImageUrls[0]" /> </div> <div class="col"> <h3 class="font-weight-bold text-primary">{{ item.Item.itemName }}</h3> <div> <p>{{ item.Item.catchcopy }}</p> </div> </div> </div> </a> </div> </div> <pagenation :showPages="showPages" :currentPage="currentPage" :totalCount="totalCount" :perPage="perPage" :totalPages="totalPages" @currentPage="getCurrentPage" ></pagenation> </div> </template> <script> import pagenation from "./components/pagenation.vue"; export default { data() { return { //ページネーションの設定 currentPage: 1, //現在のページ(初期は1) showPages: 5, //ページネーションを何ページ表示するか(奇数でないとずれる) perPage: 5, //1ページの表示件数 //API用の設定 keyword: "", totalCount: 0, //取得したアイテムの総件数 totalPages: 0, //総ページ数 list: [], }; }, components: { pagenation, }, methods: { //currentPageがページネーションコンポーネントから送られる現在のページ getCurrentPage(currentPage) { var vm = this; vm.$set(vm, "currentPage", currentPage); //APIを呼び直す vm.search(vm.keyword, vm.currentPage); }, //APIからのデータ取得 search(keyword, page) { if (!keyword) { return; } var vm = this; var url = "api.php"; var param = { keyword: keyword, page: page, hits: vm.perPage, }; vm.$axios .get(url, { params: param, }) .then(function (response) { console.log(response); if (response.data) { vm.$set(vm, "list", response.data.Items); vm.$set(vm, "totalCount", response.data.count); vm.$set(vm, "currentPage", response.data.page); vm.$set(vm, "totalPages", response.data.pageCount); } }) .catch(function (error) { //エラーのとき console.log(error); }); }, }, }; </script> <style lang="scss" scoped> /* CSSは見栄えを整えるだけ */ h1 { font-size: 1.4rem; margin-bottom: 0; } h3 { font-size: 1.2rem; } .col-item a { text-decoration: none; } </style> |
ADs
コメントはまだありません。