このブログは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ページ目を見ている場合。
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内です。
要件を整理すると、以下のような条件で順番に組み立てていくとよいです。
...
を打つ。...
を打つ。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>
よく見かけるコンポーネントですが、想像していたよりも条件が複雑でした。
ではでは。