ページネーション機能の解説 (Svelte)
このコードはSvelteを使用して実装されたページネーションコンポーネントです。このコンポーネントは記事一覧などで複数ページに分割された内容をナビゲートするために使用されます。ここでは、コードの各部分を詳細に解説し、その動作原理と実装の工夫について説明します。
プロパティとステート管理
interface Props {
current: number;
slug: string;
total: number;
limit: number;
max: number;
}
let {
current = $bindable(),
slug,
total,
limit,
max = $bindable(),
}: Props = $props();
このコンポーネントは次のプロパティを受け取ります:
current
: 現在表示しているページ番号slug
: 記事のスラッグ(URL識別子)total
: 全アイテム数limit
: 1ページあたりのアイテム数max
: 最大ページ数
特筆すべき点は$bindable()
と$props()
の使用です。これはSvelte 5で導入された新しい「runes」構文の一部で、よりシンプルなリアクティビティを実現します。$bindable()
を使うことで、親コンポーネントからバインド可能なプロパティを定義しています。
リアクティブな効果
$effect(() => {
current = Number(page.params.page);
});
$effect(() => {
max = Math.ceil(total / limit);
});
$effect
はSvelte 5のリアクティブステートメントで、依存するデータが変更されるたびに再実行されます:
- 最初の
$effect
はpage.params.page
が変更されるたびにcurrent
を更新します。これにより、URLのページパラメータが変わると現在のページが自動的に更新されます。 - 2つ目の
$effect
はtotal
またはlimit
が変更されるたびに最大ページ数(max
)を計算します。
派生値の計算
let start = $derived((current - 1) * limit);
let end = $derived(current === max ? total - 1 : start + limit - 1);
$derived
を使用して、既存の状態から新しい値を計算しています:
start
は現在のページの最初のアイテムのインデックスです(0ベース)end
は現在のページの最後のアイテムのインデックスです
通常のページではstart + limit - 1
が使用されますが、最後のページでは残りのアイテム数に合わせてtotal - 1
が使用されます。これにより、最後のページで余分なアイテムが表示されないようになっています。
ナビゲーション関数
const goBack = (e: Event) => {
e.preventDefault();
goto(`/articles/${slug}/${current - 1}`);
};
const goNext = (e: Event) => {
e.preventDefault();
goto(`/articles/${slug}/${current + 1}`);
};
これらの関数はページナビゲーションを処理します:
goBack
は前のページに移動しますgoNext
は次のページに移動します
どちらの関数も、まずe.preventDefault()
を呼び出してリンクのデフォルトの動作を防止し、次にSvelteKitのgoto
関数を使用してクライアントサイドナビゲーションを実行します。これにより、ページ全体をリロードせずにURLと表示コンテンツを更新できます。
テンプレートマークアップ
{#if total && total > limit}
<div class="pagination">
{#if current > 1}
<a
href={`/articles/${slug}/${current - 1}`}
class="button"
onclick={goBack}
aria-label="left arrow icon"
aria-describedby="prev">prev</a
>
{/if}
<p>{start + 1} - {end + 1} of {total}</p>
{#if current < max}
<a
href={`/articles/${slug}/${current + 1}`}
class="button"
onclick={goNext}
aria-label="right arrow icon"
aria-describedby="next">next</a
>
{/if}
</div>
{/if}
テンプレート部分は条件付きレンダリングを多用して、適切なUIを表示します:
total && total > limit
の条件は、ページネーションUI自体が表示されるべきかどうかを判断します。アイテム数が1ページに収まる場合、ページネーションは不要です。current > 1
の条件は「prev」ボタンを表示するかどうかを判断します。最初のページでは「prev」ボタンは不要です。- 中央のテキストは「X - Y of Z」形式でページ情報を表示します。表示はユーザーにわかりやすい1ベースのインデックスを使用しています。
current < max
の条件は「next」ボタンを表示するかどうかを判断します。最後のページでは「next」ボタンは不要です。
アクセシビリティの観点から、aria-label
とaria-describedby
属性を使用してスクリーンリーダーユーザーに情報を提供しています。
SEOとアクセシビリティのための隠しリンク
<div class="dummy">
{#each Array(max) as _, i}
<a href="/articles/{slug}/{i + 1}">{i + 1}</a>
{/each}
</div>
このセクションは視覚的には非表示ですが、すべてのページへの直接リンクを生成しています。これには主に2つの目的があります:
- SEO向上:検索エンジンのクローラーが全ページを発見できるようにする
- アクセシビリティ:JavaScriptが無効な環境でもすべてのページにアクセスできるようにする
display: none
とvisibility: hidden
を両方使用することで、視覚的にもスクリーンリーダーにも非表示にしつつ、検索エンジンがリンクをたどれるようにしています。
スタイリング
.dummy {
display: none;
visibility: hidden;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
pointer-events: all;
}
.pagination p {
margin: 1rem;
}
スタイリングはシンプルですが効果的です:
- ダミーリンクコンテナは完全に非表示
- ページネーションコンテナはフレックスボックスを使用して中央揃え
pointer-events: all
はこの要素がポインタイベントを確実に受け取れるようにします(親要素がpointer-events: none
を持つ可能性がある場合に重要)- テキスト部分には適切なマージンが設定され、ボタンと適切な間隔を確保しています
実装の工夫と特筆点
-
SvelteKitとの統合:
$app/navigation
からのgoto
と$app/state
からのpage
の使用により、SvelteKitアプリケーションとシームレスに統合されています。 -
Svelte 5のrunes: 新しい
$bindable
、$props
、$effect
、$derived
などの構文を使用して、よりクリーンで直感的なリアクティビティを実現しています。 -
SEOとアクセシビリティへの配慮: 検索エンジンとJavaScriptが無効な環境の両方をサポートする隠しリンクを含めています。
-
柔軟性: このコンポーネントは異なる記事コレクションでのページネーションに再利用できるよう、
slug
をパラメータとして受け取ります。 -
ユーザービリティ: 現在のページに不要なナビゲーションボタン(最初のページの「prev」や最後のページの「next」)は表示されません。
-
1ベースと0ベースのインデックスの使い分け: 内部計算では0ベースのインデックスを使用しつつ、ユーザーに表示する際には人間にとって自然な1ベースのインデックスを使用しています。
まとめ
このSvelteページネーションコンポーネントは、最新のSvelte 5の機能を活用しつつ、SEO、アクセシビリティ、ユーザビリティに配慮した設計になっています。シンプルながらも柔軟性が高く、さまざまなコンテンツリストで再利用できるコンポーネントです。また、クライアントサイドナビゲーションを活用することで、スムーズなユーザーエクスペリエンスを提供しています。