さて前回はWP REST APIでの投稿の全件取得と指定件数分の投稿表示を行いました。
今回はその続きで、ページネーションをつけてみましょう。
注意していただきたいのは、infinitLoaderや、ボタンを押して下に新たな記事を読み込むタイプではありません。
あくまで連番をクリックするタイプのページネーションです。
また、子コンポーネントを作成して、前回作成したarchive.vueと連携を取る形で進めていきます。
それではやっていきましょう。
# この記事で解決すること
- WP REST APIで取得してきた一覧に対してページネーションをつけることができる(前記事の続き)
手順
- 事前準備
- フォルダ・ファイル構成の確認
- 構造とデータの確認
- 初期処理
- archive.vue
- pagination.vue
- page-test.php
- 最大ページ数取得
- perPageから換算した最大ページ数取得
- 最大ページ数取得後にページネーションを表示
- 親子間のpagerの送受信
- 親コンポーネントへの送信
- 親コンポーネントでの受信
- 親コンポーネントでのデータ処理
- pager受信による表示データの書き換え
- ページネーションを表示しない場合のデータ取得処理
- pagerの設定
- pagerの初期設定
- pagerの読み込み
- Next, Prevの挙動
- Nextの挙動
- Prevの挙動
- Next・Prevボタンの不要判定(Style調整)
- 挙動確認
事前準備
フォルダ・ファイル構成の確認
今回追加するファイルはpagination.vue、
変更するファイルはarchive.vueとpage-test.phpとなります。
- theme folder
- src
- js
- components
- archive.vue(edit)
- pagination.vue(add)
- app.js
- components
- js
- page-test.php(edit)
- src
構造とデータの確認
前回の続きとして作業を行いますので、データなども前回から引き続き使用するものがあります。
また、今回はpagenation
とpager
の関係がややこしいので、こちらの図で理解してください。
上記を踏まえた上で構造を確認していきます。
(上図の色と下図の色は関係ありません。)
はい。XDで作らなくていいし、労力の割にわかりにくい。。。
ひとまず、やっていることは簡単で「初期表示設定」「Next,Prevの挙動と表示設定」だけです。
あとはデータの監視と受け渡しですね。
新しく追加するのも青色のデータだけなので、割りとシンプルです。
showPage
はcomputed
でも行けそうな感じがしますが、
途中で変更するデータなのでdata
に入れるのが得策だと思います。
では、前回から引き続き使用するデータと新たに追加するデータの一覧です。
(ページネーションで使用するもののみ記載)
引き続き使用するデータ
name | remark |
---|---|
perPage | 1回(ページ)で表示させる件数 |
totalPosts | 合計投稿数 |
pager | props:perPage を踏まえた現在のページ |
新たに追加するデータ:props
name | type | remark |
---|---|---|
pagerVisible | Number | nextとprevの間に入れる数字の数 default: 5 |
pagerScroll | Number | next, prevを押した際に移動する数字の数 default: 1 |
paginationShow | Boolean | ページネーションを表示するか否か default: true |
新たに追加するデータ:data
name | type | remark |
---|---|---|
showPage | Array | nextとprevの間に入れる数字の数の配列 ex: [1, 2, 3, 4, 5] |
初期処理
前項のデータを入れ込むと各ファイルの初期状態は下記の内容となります。
※説明と関係ない部分は省いています。
archive.vue
<template>
<div class="my-post-archive">
<article class="archive" v-if="datas">...</article>
<pagination
v-if="paginationShow"
:totalPosts="totalPosts"
:perPage="perPage"
:pager="pager"
:pagerVisible="pagerVisible"
:pagerScroll="pagerScroll"
/>
</div>
</template>
<style lang="scss" scoped>...</style>
<script>
import axios from 'axios'
import pagination from './pagination.vue'
export default {
props: {
postFetchUrl: {
type: String,
require: true
},
postQuery: {
type: Object,
default: () => ({
'_embed': '', // アイキャッチ情報の取得
})
},
perPage: {
type: Number,
default: 10,
},
/* pagination ------------------------------------------------- */
pagerVisible: {
type: Number,
default: 5,
},
pagerScroll: {
type: Number,
default: 1,
},
paginationShow: {
type: Boolean,
default: true,
},
},
components: {
pagination,
},
...
5行目
paginationShow
でページネーションを表示させるか否か決めたいので、
v-if
で判定しています。
19行目
pagination.vueの読み込み
36~48行目
propsの追加
51行目
paginationの使用宣言
pagination.vue
<template>
<div class="pagination">
<ul class="pagination__list">
<li class="pagination__item">
<span class="pagination__item--perv">prev</span>
</li>
<li class="pagination__item">
<span class="pagination__item--num">1</span>
<span class="pagination__item--num">2</span>
<span class="pagination__item--num">3</span>
</li>
<li class="pagination__item">
<span class="pagination__item--next">next</span>
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
</style>
<script>
export default {
props: {
totalPosts: {
type: Number,
},
perPage: {
type: Number,
},
pager: {
type: Number,
},
pagerVisible: {
type: Number,
},
pagerScroll: {
type: Number,
},
},
data() {
return {
showPage: [],
}
},
}
</script>
2~16行目
とりあえず静的なhtmlをあてています。
22行目~
propsは親コンポーネントであるarchive.vueからのデータの引き渡しであり、
defaultやrequireは親コンポーネントに設定していますので、type
のみ設定しておきます。
page-test.php
<div id="archive">
<archive
:post-fetch-url="'/wp-json/wp/v2/posts'"
:per-page="10"
:post-query="{
'_fields': 'id,content,title,categories,_links,_embedded',
'_embed': '',
}"
:pager-visible="5"
:pager-scroll="1"
:pagination-show="true"
#post="{ data }"
>
<span v-text="data.id"></span>
</archive>
</div>
9行目~11行目
追加分です。それ以外は前回同様となります。
最大ページ数取得
perPageから換算した最大ページ数取得
次にページネーションでもかなりの頻度で使用する
「props:perPage
から換算した場合の最大ページ数」を取得していきます。
pagination.vue: computed
computed: {
maxPage() {
return Math.ceil(this.totalPosts / this.perPage)
},
}
合計投稿数 / 1ページに表示させる投稿件数 をMath.ceil
(四捨五入で切り上げ)しています。
例えば、合計投稿数が121件で、1ページに表示させる投稿件数が12件だった場合、
121/12=10.083…=11となります。
computed
に入れておくことで、totalPosts
が親テンプレートであるarchive.vueで更新された時にmaxPage
も更新されます。
今回は最初の取得ぐらいしか更新することがないので地味な役割ですが、
次回の「タクソノミーフィルター機能追加」で活躍します。
最大ページ数取得後にpagerを表示
また、この数値が決まらない限りはページネーションを表示してほしくないので、
pagination.vueにv-if
でそのことを記述してあげましょう。
pagination.vue: template
<template>
<div class="pagination" v-if="maxPage">
...
親子間のpagerの送受信
親: archive.vue
子: pagination.vue
親子間でのデータの受け渡しには、双方への記述が必要となります。
親コンポーネントで指定した内容を子コンポーネントで使用しますので、
まずは親コンポーネントでの受信から説明します。
親コンポーネントでの受信
archive.vue: methods
changePager(num) {
this.pager = num
},
num
には子コンポーネントから渡されるpagerなどの数字が入ります。
archive.vue: template
<template>
<div class="my-post-archive">
...
<pagination
...
@changePage="changePager"
/>
changePager
をchangePage
としてemitに定義しています。
親コンポーネントへの送信
pagination.vue: methods
sendPager(num) {
this.$emit('changePage', num)
}
先程emitに定義したchangePage
を呼び出します。
第2引数にchangePage
に定義されたchangePager
の引数を設定します。
親コンポーネントでのデータ処理
pager受信による表示データの書き換え
さて、これで親のpagerに任意の数字を送ることができるようになりました。
あとは、その数字によって投稿一覧を切り替えたいので、
archive.vueのcomputed:datas
を書き換えていきます。
archive.vue: computed
datas() {
// paginationを表示する場合
if(this.paginationShow !== false) {
const start = (this.pager - 1) * this.perPage
const end = start + this.perPage
return this.dataBase.slice(start, end)
// paginationを表示しない場合
} else {
return this.dataBase
}
ここでは、paginationShow
がtrue
の場合とfalse
の場合で処理を分けています。
false
の場合は全件取得するので、次項でちゃんと処理します。
4~6行目
this.pager
を-1しているのは配列が0始まりだからです。
なので、もしperPage=10, pager=1
の場合、start=0, end=10
となります。
同じ要領でpager=4の場合、start=30, end=40となります。
slice
の性質がend
になっている数字の直前までというのがややこしいですね。
perPage=10, pager=4の例でいうと、datasはdataBase[30]~[39]となります。
ページネーションを表示しない場合のデータ取得処理
前項のpaginationShow=false
の場合、
投稿を全件取得して無駄にリソースを割いてしまいますので、ちゃんと処理しておきましょう。
archive.vue: methods: getPostDatas
async getPostDatas() {
// ページネーションを表示する場合
if(this.paginationShow === true) {
await this.getPostInfo()
const numPostAry = [...Array(this.totalPages).keys()].map( i => ++i )
for (let num of numPostAry) {
await this.getPostData(num)
}
// ページネーションを表示しない場合
} else {
await this.getPostData(false)
}
},
ページネーションを表示しない場合は、投稿の合計数もページ数も必要ないので取得しません。
10件以上取得したいけれどページネーションは表示したくないという場合は、
page-test.phpのpostQuery
にper_page: x
を追加する必要があります。
pagerの設定
pagerに羅列する番号の数はpagerVisible
から取得します。
ですが、pagerVisibleが5なのに、最大ページ数が3ページだった場合、
どこかでエラーが起きる未来しか想像できません。
そのため、pagerVisibleとは別にshowPage
というdataを最初に追加しました。
pagerの初期設定
pagination.vue: methods
pagerDefault() {
this.sendPager(1)
if(this.maxPage <= this.pagerVisible) {
this.showPage = [...Array(this.maxPage).keys()].map( i => ++i)
} else {
this.showPage = [...Array(this.pagerVisible).keys()].map( i => ++i)
}
},
2行目
まずは「親コンポーネントでのデータ処理」で作成したsendPager
で、
初期のpager
を1に変更します。
4, 6行目
「perPageから換算した最大ページ数取得」の項で取得した
maxPage
とpagerVisible
を比較して、数字から配列を作成しています。
前回の記事にもありましたが、[...Array(num).keys()].map( i => ++i)
はnum=5の時、[1, 2, 3, 4, 5]という配列にしてくれます。
.map( i => ++i)
がない場合は、[0, 1, 2, 3, 4]ですね。
pagerの読み込み
ここまでくるとそれなりになっていますので、実際にtemplateに読み込んでみましょう。
pagination.vue: template
...
<span class="pagination__item--perv">prev</span>
</li>
<li class="pagination__item" v-for="num of showPage" :key="num">
<span
class="pagination__item--num"
:class="{ 'pagination__item--current' : num == pager}"
v-text="num"
@click="sendPager(num)" />
</li>
<li class="pagination__item">
<span class="pager__item--next">next</span>
...
4行目
showPage
をfor of
で回しています。
7行目
今どこのページにいるのか知りたいので、
num
とpager
が同一であればcurrentクラスを付与するよう設定しています。
9行目
数字をクリックした際に、
sendPager
でarchive.vueのpager
にnum
が送られる仕組みです。
ここまでで下記の流れができています。
flow | file(.vue) | action |
---|---|---|
1 | pagination | pagerをクリック |
2 | archive | pagerが切り替わる |
3 | archive | pagination.vueにpagerを送信 |
4 | archive | computed:data発火 データが切り替わる |
4 | pagination | currentクラス付与 |
Next, Prevの挙動
ここまででpagerの設定が完了したので、次はNext, Prevです。
まずはNextから見ていきましょう。
Nextの挙動
pagination.vue: methods
goNext() {
// 最大ページ数の配列化
const maxPageAry = [...Array(this.maxPage).keys()].map( i => ++i)
// 現状表示されているpagerの最大値取得
const showPageMax = this.showPage.slice(-1)[0]
// next時の最終の値を取得
let end = showPageMax + this.pagerScroll
// 最終値と最大ページ数を比較して最初の値を修正
end = end > this.maxPage ? this.maxPage : end
const start = end - this.pagerVisible
// 表示に反映
this.showPage = maxPageAry.slice(start, end)
this.sendPager(start + 1)
},
例えば下記の状態で、現在1~5のpagerが表示されている場合で考えてみましょう。
name | value | remark |
---|---|---|
pagerVisible | 5 | pagerの羅列数 |
maxPage | 15 | 最大ページ数 |
pagerScroll | 4 | Next, prevを押した際に移動する数字の数 |
3行目
15を配列[1, 2, 3…15]に
6行目
this.showPage
には[1, 2, 3, 4, 5]が入っています。
slice(-1)[0]
は配列の最後を取得するので、5がshowPageMax
に代入されます。
9行目
showPageMax=5
+pagerScroll=4
=9がend
に代入されます。
12行目
end
と「最大ページ数」を比較して、end
の方が大きければ「最大ページ数」を、
そうでなければend
を返します。
ですので、end=9
>maxPage=15
=9がend
に上書きされます。
13行目
start
はend
から「表示するpagerの羅列数」を引いた数字になります。
つまりend=9
–pagerVisible=5
=4がstart
に代入されます。
ここまでで、start=4, end=9
という数字が確定しました。
16行目
3行目で求めたmaxPageAry=[1, 2, 3...15]
の[4]~[8]を切り出します。
slice
はendの手前までを切り出すので9ではなく8となります。
配列自体は0始まりなので、実際に切り出される数字は
maxPageAry[4]=5, maxPageAry[8]=9
となり、showPage
に[5, 6, 7, 8, 9]が代入されます。
17行目
上記まででpagerの表示は切り替わりましたが、肝心の投稿の表示は切り替わっていませんので、
archive.vueのpagerにstartの値を送ってあげましょう。
といっても送るのはstart=4
ではなく、
実際に表示されているpagerの一番左の数字ですので、
start+1=5
となります。
上記でpagerに羅列する数字の取得が完了しましたので、templateに反映していきます。
pagination.vue: template
<span class="pagination__item--next" @click="goNext()">next</span>
前項で作成したgoNext
をclickFunctionとして追加しました。
Prevの挙動
PrevはNextの逆をすればいいだけです。
pagination.vue: methods
goPrev() {
// 最大ページ数の配列化
const maxPageAry = [...Array(this.maxPage).keys()].map( i => ++i)
// 現状表示されているpagerの最小値取得
const showPageMin = this.showPage.slice(0)[0]
// prev時の最初の値を取得
let start = showPageMin - this.pagerScroll - 1
// 最小値とstartの値を比較して最初の値を修正
start = 0 >= start ? 0 : start
const end = start + this.pagerVisible
// 表示に反映
this.showPage = maxPageAry.slice(start, end)
this.sendPager(end)
},
pagination.vue: template
<span class="pagination__item--prev" @click="goPrev()">prev</span>
Next・Prevボタンの不要判定
さて、ここまで来るとpagerが1~5の時にPrevボタンが表示されている違和感に気づくと思います。
また、最終ページで表示されているNextボタンに関しても同じです。
ですので、showPage
を利用して不要な時は省いてあげましょう。
pagination.vue: methods
// nextの不要判定
hideNext() {
return this.showPage.slice(-1)[0] == this.maxPage
},
// prevの不要判定
hidePrev() {
return this.showPage.slice(0)[0] == 1
},
上記でtrue
orfalse
を返すようにしています。
あとは、template側でtrue
の場合にclassを付与してあげます。
pagination.vue: template
<span class="pagination__item--perv" :class="{ 'pagination__item--hidden' : hidePrev()}" @click="goPrev()">prev</span>
<span class="pagination__item--next" :class="{ 'pagination__item--hidden' : hideNext()}" @click="goNext()">next</span>
true
の場合に、それぞれpagination__item--hidden
というclassを付与しました。
v-if
でも良いのですが、prevとnextが消えることでレイアウトがずれるのを避けるためにclass付与で対応しています。
そういえばstyleの設定をしていませんでしたので、ついでにしておきましょう。
pagination.vue: style
.pagination {
border-top: 2px solid #eee;
margin-top: 1rem;
padding-top: 1rem;
&__list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: .5rem 1rem;
list-style-type: none;
margin: 0;
padding: 0;
}
&__item {
span {
display: inline-block;
text-align: center;
cursor: pointer;
}
&--num {
width: 2rem;
}
&--current {
color: #f00;
}
&--hidden {
visibility: hidden;
}
&--perv,
&--next {
width: auto;
}
}
}
すごく適当なのでお好きに改変してください。
挙動確認
では実際に動いているか確認してみましょう。
問題なく動いていますね。
最後に
かなり長くなりましたがお付き合いいただきありがとうございます。
やっていることはすごく難しそうに見えて非常にシンプルですね。
Vueでコンポーネントを作成する際はデータの受け渡しが重要になります。
というより、データの受け渡し以外は、ほぼバグが起きないように書いているだけです。
次回はこの状態からタクソノミーフィルター機能を追加していきましょう。
では、よいWPライフを!