Nuxt.js

Nuxt3 動的なページネーションの導入方法

公開日:2023-04-02 更新日:2023-06-12

このブログはNuxt.js + microCMSで作っているのですが、動的なページネーションを導入してみました。

表示しているページに応じて、1,2,3,...,10とか1,...,4,5,6,7,8,...,10みたいに表示させるやつです。

意外と実装にてこずったので方法をまとめておきます。

要件

通常のページネーションでは以下のようにページ番号を書き出すと思います。

10ページある場合。

1 2 3 4 5 6 7 8 9 10

ただ、記事数が増えるにつれ、ページ数も増えていくので、スタイルの調整が面倒です。

そのため、以下のような要件で、表示するページ数を動的に変更するようにしました。

  • 1ページ目と最後のページは必ず表示する
  • 現在のページからプラスマイナス2ページまでの範囲は表示する
  • 表示されない範囲は...で表す
  • 次へ、前へボタンの設置

具体例を示します。

例えばユーザーが1ページ目を見ている場合。

1 2 3 ...10 >

5ページ目を見ている場合。

< 1 ... 3 4 5 6 7 ... 10 >

参考

スタイルやロジックについて、microCMSブログのコンポーネントを参考にしました。

https://github.com/microcmsio/microcms-blog/blob/production/components/Pagination.vue

実装

まず<script>部分です。

全ページ数をあらわすnumPagesと現在のページ数をあらわすcurrentを受け取るようにします。

type Props = {
    numPages: number;
    current: number;   
}

const { numPages, current } = defineProps<Props>()

次に合計ページ数と現在のページからpagerの配列を作成します。

要件は現在のページプラスマイナス2のページを表示することでした。

なので、そのような配列をnumPagesとcurrentから作ります。

以下のような素朴なfor loop文で書きました。

const pager: number[] = []
for (let i = 1; i < numPages + 1; i++) {
    if (i < current - 2) continue
    if (i > current + 2) continue
    pager.push(i)
}

あとは番号を受け取りリンクを生成するgetPath関数を書いておきます。

function getPath(p: number) {
    return `/page/${p}`
}

<script>内の 前準備としてはこれで終わりです。

次にtemplate内です。

要件を整理すると、以下のような条件で順番に組み立てていくとよいです。

  1. 現在のページが2ページ目以降だったら前へ戻るボタンを設置。
  2. 現在のページが4ページ目以降だったら1ページ目を表示(配列に入らないため)。
  3. 現在のページが5ページ目以降だったら...を打つ。
  4. 配列の数字を書き出す。
  5. 現在のページが全ページ数の-4以下だったら...を打つ。
  6. 現在のページが全ページ数の-3以下だったら最後のページを表示(配列に入らないため)。
  7. 現在のページが最後のページでなかったら、次へボタンを設置。

nuxtであればv-if構文を使って上に書いた条件に合うように記述していきます。

<template>
    <div class="wrapper">
        <ul class="pager">
            <!--現在のページが2ページ目以降だったら前へ戻るボタンを設置-->
            <li v-if="current > 1" class="page arrow">
                <nuxt-link :to="getPath(current - 1)">
                    <img src="../assets/images/icon_arrow_left.svg" width="24" height="24" alt="前のページへ" />
                </nuxt-link>
            </li>
            <!--現在のページが4ページ目以降だったら1ページ目を表示-->
            <li v-if="current > 3" class="page">
                <nuxt-link :to="getPath(1)">
                    1
                </nuxt-link>
            </li>
            <!--現在のページが5ページ目以降だったら...を打つ-->
            <li v-if="current > 4" class="omission">
                ...
            </li>
            <!--配列の数字を書き出す-->
            <li v-for="p in pager" :key="p" class="page" :class="{ active: current === p }">
                <nuxt-link :to="getPath(p)">
                    {{ p }}
                </nuxt-link>
            </li>
            <!--現在のページが全ページ数の-4以下だったら...を打つ-->
            <li v-if="current < numPages-3" class="omission">
                ...
            </li>
            <!--現在のページが全ページ数の-3以下だったら最後のページを表示-->
            <li v-if="current + 2 < numPages" class="page">
                <nuxt-link :to="getPath(numPages)">
                    {{ numPages }}
                </nuxt-link>
            </li>
            <!--現在のページが最後のページでなかったら、次へボタンを設置-->
            <li v-if="current < numPages" class="page arrow">
                <nuxt-link :to="getPath(current + 1)">
                    <img src="../assets/images/icon_arrow_right.svg" width="24" height="24" alt="次のページへ" />
                </nuxt-link>
            </li>
        </ul>
    </div>
</template>

少し分かりづらいのが、5ページ目以降の時に...を打つ部分でしょうか。

プラスマイナス2までの範囲なので、一見すると4ページ目以降の時に...を打てばいいのでは、と思います。

しかし、そうしてしまうと、4ページ目を表示した際に以下のような並びになってしまいます。

1 ... 2 3 4 5 6 ... 10

書いてみるとよくわかりますね。1の次が2なのに...があるのは明らかに不自然です。

これを避けるために5ページ目以降という条件を加えています。

後ろの...も同じ理屈です。

あとは書き出す最中に現在のページにあたったら、activeクラスを付与して他のページと見分けがつくようにします。

<!--配列の数字を書き出す-->
<li v-for="p in pager" :key="p" class="page" :class="{ active: current === p }">
  <nuxt-link :to="getPath(p)">
   {{ p }}
   </nuxt-link>
</li>

あとは、適当なスタイルを振れば以下のように表示されるでしょう。

1ページ目を表示。pager配列には[1,2,3]が入っている。

5ページ目を表示。pager配列には[3,4,5,6,7]が入っている。

7ページ目を表示。pager配列には[5,6,7,8,9]が入っている。

ソースコード全文

ブログ本文では端折りましたが、オプショナルtagIdとkeywordを受け取るようにしています。

<script setup lang="ts">
	type Props = {
	    numPages: number;
	    current: number;
	    tagId?: string;
	    keyword?: string;
	}
	
	const { numPages, current, tagId, keyword } = defineProps<Props>()
	
	function getPath(p: number) {
	    if (tagId) return `/tags/${tagId}/page/${p}`
	    if (keyword) return `/search?page=${p}&q=${keyword}`
	    return `/page/${p}`
	}
	
	const pager: number[] = []
	for (let i = 1; i < numPages + 1; i++) {
	    if (i < current - 2) continue
	    if (i > current + 2) continue
	    pager.push(i)
	}
	
	</script>
	    
	<template>
	    <div class="wrapper">
	        <ul class="pager">
	            <li v-if="current > 1" class="page arrow">
	                <nuxt-link :to="getPath(current - 1)">
	                    <img src="../assets/images/icon_arrow_left.svg" width="24" height="24" alt="前のページへ" />
	                </nuxt-link>
	            </li>
	            <li v-if="current > 3" class="page">
	                <nuxt-link :to="getPath(1)">
	                    1
	                </nuxt-link>
	            </li>
	            <li v-if="current > 4" class="omission">
	                ...
	            </li>
	            <li v-for="p in pager" :key="p" class="page" :class="{ active: current === p }">
	                <nuxt-link :to="getPath(p)">
	                    {{ p }}
	                </nuxt-link>
	            </li>
	            <li v-if="current < numPages-3" class="omission">
	                ...
	            </li>
	            <li v-if="current + 2 < numPages" class="page">
	                <nuxt-link :to="getPath(numPages)">
	                    {{ numPages }}
	                </nuxt-link>
	            </li>
	            <li v-if="current < numPages" class="page arrow">
	                <nuxt-link :to="getPath(current + 1)">
	                    <img src="../assets/images/icon_arrow_right.svg" width="24" height="24" alt="次のページへ" />
	                </nuxt-link>
	            </li>
	        </ul>
	    </div>
	</template>
	    
	<style scoped lang="scss">
	li {
	    list-style: none;
	}
	
	.wrapper {
	    padding: 16px 0;
	}
	
	.pager {
	    display: flex;
	    flex-wrap: wrap;
	    justify-content: center;
	    align-items: center;
	    padding: 40px 0 0;
	    font-size: var(--font-size-xl);
	    font-weight: 500;
	}
	
	.omission {
	    color: var(--qlitre-colors-gray-400);
	    margin: 4px 12px;
	}
	
	.page {
	    width: 40px;
	    height: 40px;
	    border-radius: 5px;
	    margin: 4px;
	
	    &.arrow {
	        margin: 4px 12px;
	    }
	
	    &.active {
	        background-color: var(--qlitre-colors-gray-400);
	
	        a,
	        a:hover {
	            color: var(--qlitre-colors-white);
	        }
	    }
	
	    a {
	        display: flex;
	        justify-content: center;
	        align-items: center;
	        height: 100%;
	        color: var(--qlitre-colors-gray-600);
	
	        &:hover {
	            color: var(--qlitre-colors-gray-400);
	        }
	    }
	}
	</style>

おわりに

よく見かけるコンポーネントですが、想像していたよりも条件が複雑でした。

ではでは。

Twitter Share