はじめに
一覧画面においてページネーションはよくある機能の一つですが、一から実装するとなると地味に面倒な機能でもあります。
ところがLaravelにはデータの取得から表示、スタイルの調整まで簡単に行えるような機能が備わっているので一安心…そう、Bladeであれば!!
でも、BladeではなくVueを利用した場合はどうしよう!?
ペジネータの手作業生成
Bladeであれば、下記のようにするだけでページリンクをレンダリングしてくれますが、もちろんVueでは利用できません。
{{ $users->links() }}
「自前でページネーション用のコンポーネントを用意してやる必要があるのですが、果たしてリンクやラベルはどうやって取得できるのか…?」
特にすることはありませんでした。
Laravelペジネータクラスは
Illuminate\Contracts\Support\Jsonable
インターフェイスコントラクトを実装し、toJson
メソッドを提供しているため、ペジネーションの結果をJSONに変換するのは非常に簡単です。ルートまたはコントローラアクションから返すことで、ペジネーションインスタンスをJSONに変換することもできます。
上記の通り、ペジネーションの結果をJSONでフロントに渡してやることで、オリジナルのページャーを実装できるようになりました。
ところが、新たな課題が出てきました。
「DBから取得したデータを加工してから返したいんだけど、どうすればいいのか…?」
これは少し面倒でしたが、Laravelには自前でペジネーションインスタンスを生成する方法が用意されておりました。
場合によっては、ペジネーションインスタンスを手作業で作成し、メモリ内にすでにあるアイテムの配列を渡すことができます。必要に応じて、
Illuminate\Pagination\Paginator
、Illuminate\Pagination\LengthAwarePaginator
、Illuminate\Pagination\CursorPaginator
インスタンスを生成することでこれが行えます。
自前でページネーションを生成したサンプルがこちら。
class StaffService
{
public function list(Request $request)
{
list(
$limit,
$page,
$query,
) = [
isset($request->limit) ? intval($request->limit) : 10,
isset($request->page) ? intval($request->page) : 1,
isset($request->query) ? $query : null
];
try
{
// 全ページのデータ取得
$staffsQuery = Staff::whereNotNull('enabled_at');
// 対象ページのデータ取得
$total = $staffsQuery->count();
$staffs = $staffsQuery
->offset(($page - 1) * $limit)
->limit($limit)
->get();
$items = array();
// 取得したデータの加工
foreach ($staffs as $staff) {
$created_at = new Carbon($staff->created_at);
$item = array(
'id' => $staff->id,
'name' => $staff->name,
'email' => $staff->email,
'created_at' => $created_at->timezone('Asia/Tokyo')->format('Y年m月d日 H:i'),
);
array_push($items, $item);
}
// ページのパスを指定
$options = array(
'path' => LengthAwarePaginator::resolveCurrentPath()
);
// ページネーションの作成
$result = new LengthAwarePaginator(
$items,
$total,
$limit,
$page,
$options
);
$response = array(
'items' => $result,
'query' => $query
);
return $response;
}
catch (Exception $exception)
{
Log::error($exception->getMessage());
}
}
}
あとは結果をVueファイルに渡してあげれば、バックエンド側は完了です。
class StaffController extends Controller
{
public function list(Request $request)
{
$response = $this->staffService->list($request);
return Inertia::render('Staff/List', [
'items' => $response['items'],
'query' => $response['query'],
]);
}
ページネーションコンポーネントの作成
各一覧画面で利用できるようにコンポーネント化します。
バックエンド側で生成したページネーションのリンク一覧をlinks
として、検索条件となるURLクエリパラメータを文字列でquery
として、画面側のソースから受け取れるようにします。
const props = defineProps({
links: {
type: Array,
},
query: {
type: String,
default: ''
}
});
ページネーションのベースはtailwindcss
のソースを利用しました。
https://flowbite.com/docs/components/pagination/
ページネーションのリンクの一覧をループさせて表示し、インデックスによって「前へ」「ページ番号」「次へ」のリンクを出し分けます。
URLクエリパラメータがあれば、ページネーションリンクに付け足しています。
<template v-if='key === 0'>
<!-- Previous -->
<Link
class="block p-2 mr-2 leading-tight bg-neutral-500 text-white dark:bg-neutral-500 dark:text-white rounded-full hover:bg-neutral-400 dark:hover:bg-neutral-400"
:class="{ 'bg-blue-700 text-white': link.active }"
:href="link.url ? link.url + (query ? '&' + query : '') : '#'"
>
<span class="sr-only">Previous</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="4" stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
</svg>
</Link>
</template>
ページネーションコンポーネントのサンプルがこちら。
<script setup>
import { Link } from '@inertiajs/vue3';
const props = defineProps({
links: {
type: Array,
},
query: {
type: String,
default: ''
}
});
</script>
<template>
<div v-if="links.length > 3">
<ul class="inline-flex items-center -space-x-px">
<li v-for="(link, key) in links" :key="key">
<template v-if='key === 0'>
<!-- Previous -->
<Link
class="block p-2 mr-2 leading-tight bg-neutral-500 text-white dark:bg-neutral-500 dark:text-white rounded-full hover:bg-neutral-400 dark:hover:bg-neutral-400"
:class="{ 'bg-blue-700 text-white': link.active }"
:href="link.url ? link.url + (query ? '&' + query : '') : '#'"
>
<span class="sr-only">Previous</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="4" stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
</svg>
</Link>
</template>
<template v-else-if='key !== 0 && key !== links.length - 1'>
<!-- Number -->
<span
v-if="link.url === null"
class="px-4 py-2 leading-tight text-neutral-300 dark:text-neutral-300"
v-html="link.label"
/>
<Link
v-else
class="px-4 py-2 leading-tight"
:class="link.active ? 'text-neutral-500 dark:text-neutral-500 hover:text-neutral-400 dark:hover:text-neutral-400' : 'text-blue-600 underline dark:text-blue-500 hover:no-underline'"
:href="link.url + (query ? '&' + query : '')"
v-html="link.label"
/>
</template>
<template v-else-if='key === links.length - 1'>
<!-- Next -->
<Link
class="block p-2 ml-2 leading-tight bg-neutral-500 text-white dark:bg-neutral-500 dark:text-white rounded-full hover:bg-neutral-400 dark:hover:bg-neutral-400"
:href="link.url ? link.url + (query ? '&' + query : '') : '#'"
>
<span class="sr-only">Next</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="4" stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</svg>
</Link>
</template>
</li>
</ul>
</div>
</template>
ページネーションコンポーネントの呼び出し
作成したページネーションコンポーネントは下記のようにして一覧画面で呼び出せます。
<Paginate class="mt-6" :links="items.links" :query="query" />
一覧画面のサンプルがこちら。
<script setup>
const props = defineProps({
items: {
type: Object,
},
query: {
type: Array
}
});
</script>
<template>
<template #header>
<h2 class="font-semibold text-3xl text-gray-800 leading-tight">スタッフ一覧</h2>
</template>
<div class="py-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 text-center">
<div class="relative overflow-x-auto shadow-md">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-white uppercase bg-neutral-500 dark:bg-neutral-500 dark:text-white">
<tr>
<th scope="col" class="px-6 py-3 border-b border-white">
ID
</th>
<th scope="col" class="px-6 py-3 border-b border-white">
氏名
</th>
<th scope="col" class="px-6 py-3 border-b border-white">
メールアドレス
</th>
<th scope="col" class="px-6 py-3 border-b border-white">
作成日
</th>
</tr>
</thead>
<tbody class="text-neutral-500">
<tr v-for="(item, key) in items.data" :key="key" class="border-b border-white" :class="[key%2 === 0 ? 'bg-neutral-100' : 'bg-neutral-200']">
<td class="px-6 py-4 text-center">
{{ item.id }}
</td>
<td class="px-6 py-4 text-center">
{{ item.name }}
</td>
<td class="px-6 py-4 text-center">
{{ item.email }}
</td>
<td class="px-6 py-4 text-center">
{{ item.created_at }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-10">
<Paginate class="mt-6" :links="items.links" :query="query" />
</div>
</div>
</div>
</template>
無事、ページャーの表示ができました。
おわりに
色々なサイトを参考にして実装したのですが、あちこち見すぎて参考サイトが見つけられなくなってしまったので、今回備忘録として残しました。