さて前回はWP REST APIでの投稿の全件取得と指定件数分の投稿表示を行いました。

【投稿全件取得編】WP REST APIで投稿を非同期で全件取得した話
【投稿全件取得編】WPRESTAPIで投稿を非同期で全件取得した話
WPRESTAPIは、全件取得しようと思ってもper_pageが100までしかきいてくれないので、何も考えなければ100件しか取得することができません。 そのため、全件取得しようと思ったら、下記...

今回はその続きで、ページネーションをつけてみましょう。

注意していただきたいのは、infinitLoaderや、ボタンを押して下に新たな記事を読み込むタイプではありません。

あくまで連番をクリックするタイプのページネーションです。

また、子コンポーネントを作成して、前回作成したarchive.vueと連携を取る形で進めていきます。

それではやっていきましょう。

# この記事で解決すること

  • WP REST APIで取得してきた一覧に対してページネーションをつけることができる(前記事の続き)

 

手順

  1. 事前準備
    • フォルダ・ファイル構成の確認
    • 構造とデータの確認
  2. 初期処理
    • archive.vue
    • pagination.vue
    • page-test.php
  3. 最大ページ数取得
    • perPageから換算した最大ページ数取得
    • 最大ページ数取得後にページネーションを表示
  4. 親子間のpagerの送受信
    • 親コンポーネントへの送信
    • 親コンポーネントでの受信
  5. 親コンポーネントでのデータ処理
    • pager受信による表示データの書き換え
    • ページネーションを表示しない場合のデータ取得処理
  6. pagerの設定
    • pagerの初期設定
    • pagerの読み込み
  7. Next, Prevの挙動
    • Nextの挙動
    • Prevの挙動
    • Next・Prevボタンの不要判定(Style調整)
  8. 挙動確認

            事前準備

            フォルダ・ファイル構成の確認

            今回追加するファイルはpagination.vue、
            変更するファイルはarchive.vueとpage-test.phpとなります。

            • theme folder
              • src
                • js
                  • components
                    • archive.vue(edit)
                    • pagination.vue(add)
                  • app.js
              • page-test.php(edit)

            構造とデータの確認

            前回の続きとして作業を行いますので、データなども前回から引き続き使用するものがあります。

            また、今回はpagenationpagerの関係がややこしいので、こちらの図で理解してください。

             

            上記を踏まえた上で構造を確認していきます。
            (上図の色と下図の色は関係ありません。)

             

            はい。XDで作らなくていいし、労力の割にわかりにくい。。。

            ひとまず、やっていることは簡単で「初期表示設定」「Next,Prevの挙動と表示設定」だけです。

            あとはデータの監視と受け渡しですね。

            新しく追加するのも青色のデータだけなので、割りとシンプルです。

            showPagecomputedでも行けそうな感じがしますが、
            途中で変更するデータなので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"
                />

            changePagerchangePageとして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
            }

            ここでは、paginationShowtrueの場合と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のpostQueryper_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から換算した最大ページ数取得」の項で取得した
            maxPagepagerVisibleを比較して、数字から配列を作成しています。

            前回の記事にもありましたが、[...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行目

            showPagefor ofで回しています。

            7行目

            今どこのページにいるのか知りたいので、
            numpagerが同一であればcurrentクラスを付与するよう設定しています。

            9行目

            数字をクリックした際に、
            sendPagerでarchive.vueのpagernumが送られる仕組みです。

             

            ここまでで下記の流れができています。

            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]は配列の最後を取得するので、5showPageMaxに代入されます。

            9行目

            showPageMax=5+pagerScroll=4=9endに代入されます。

            12行目

            endと「最大ページ数」を比較して、endの方が大きければ「最大ページ数」を、
            そうでなければendを返します。

            ですので、end=9>maxPage=15=9endに上書きされます。

            13行目

            startendから「表示するpagerの羅列数」を引いた数字になります。

            つまりend=9pagerVisible=5=4startに代入されます。

            ここまでで、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
            },

            上記でtrueorfalseを返すようにしています。

            あとは、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ライフを!

            あわせて読みたい

            【投稿全件取得編】WP REST APIで投稿を非同期で全件取得した話
            【投稿全件取得編】WPRESTAPIで投稿を非同期で全件取得した話
            WPRESTAPIは、全件取得しようと思ってもper_pageが100までしかきいてくれないので、何も考えなければ100件しか取得することができません。 そのため、全件取得しようと思ったら、下記...
            【タクソノミーフィルター編】WP REST APIで投稿を非同期で全件取得した話
            【タクソノミーフィルター編】WPRESTAPIで投稿を非同期で全件取得した話
            前記事にてWPRESTAPIで取得した投稿全件に対し、チェックの入ったタームのみ表示させ、ページネーションと連携します。