1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Laravel9 + Vite + Vue3 環境でページネーション機能を実装する

Last updated at Posted at 2023-04-03

はじめに

一覧画面においてページネーションはよくある機能の一つですが、一から実装するとなると地味に面倒な機能でもあります。
ところがLaravelにはデータの取得から表示、スタイルの調整まで簡単に行えるような機能が備わっているので一安心…そう、Bladeであれば!!

でも、BladeではなくVueを利用した場合はどうしよう!?

ペジネータの手作業生成

Bladeであれば、下記のようにするだけでページリンクをレンダリングしてくれますが、もちろんVueでは利用できません。

{{ $users->links() }}

「自前でページネーション用のコンポーネントを用意してやる必要があるのですが、果たしてリンクやラベルはどうやって取得できるのか…?」

特にすることはありませんでした。

LaravelペジネータクラスはIlluminate\Contracts\Support\Jsonableインターフェイスコントラクトを実装し、toJsonメソッドを提供しているため、ペジネーションの結果をJSONに変換するのは非常に簡単です。ルートまたはコントローラアクションから返すことで、ペジネーションインスタンスをJSONに変換することもできます。

上記の通り、ペジネーションの結果をJSONでフロントに渡してやることで、オリジナルのページャーを実装できるようになりました。

ところが、新たな課題が出てきました。
「DBから取得したデータを加工してから返したいんだけど、どうすればいいのか…?」

これは少し面倒でしたが、Laravelには自前でペジネーションインスタンスを生成する方法が用意されておりました。

場合によっては、ペジネーションインスタンスを手作業で作成し、メモリ内にすでにあるアイテムの配列を渡すことができます。必要に応じて、Illuminate\Pagination\PaginatorIlluminate\Pagination\LengthAwarePaginatorIlluminate\Pagination\CursorPaginatorインスタンスを生成することでこれが行えます。

自前でページネーションを生成したサンプルがこちら。

app/Services/StaffService.php
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ファイルに渡してあげれば、バックエンド側は完了です。

app/Http/Controllers/StaffController.php
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>

ページネーションコンポーネントのサンプルがこちら。

resources/ts/Components/Paginate.vue
<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" />

一覧画面のサンプルがこちら。

resources/ts/Pages/Staff/List.vue
<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>

無事、ページャーの表示ができました。

image.png

おわりに

色々なサイトを参考にして実装したのですが、あちこち見すぎて参考サイトが見つけられなくなってしまったので、今回備忘録として残しました。

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?