LoginSignup
17
17

More than 3 years have passed since last update.

Laravel + Vue.js + Vuetifyでページネーションを実現する

Last updated at Posted at 2019-10-04

バージョン

Laravel : 5.8
Vue.js : 2.5
Vuetify : 1.5

作る画面

サーバーサイドにLaravel、フロントエンドにVue.js、デザインにVuetifyを使用して検索つきのページネーションを作ります。
データは何でもいいので、本を検索することにしました。
このような画面が出来上がります。
movie.gif

booksテーブルはtitle(タイトル)とpublishing_year(発行年)だけのシンプルなテーブルです。
データはLaravelのFakerとSeederで500件用意しました。
list.png

Vuetifyとは

簡単に言うとCSSを使わずに、独自のHTMLタグでデザインを完成させてしまえる優れものです。
その他にもVue.jsで使えるマテリアルデザインコンポーネントはQuasarやBootstrapVueもあるみたいですが、何となくVuetifyが良さげな雰囲気を出しているので採用しました。
https://vuetifyjs.com/ja/

インストール

まずはVue.jsとVuetifyを使う上で必要なライブラリをインストールします。
前述の通りVuetifyは1系を使います。

npm install vue-router vuex vuetify@1 css-loader material-design-icons-iconfont vuex-persistedstate

ディレクトリ構成

主なファイルの配置です。
LaravelとVue.jsの一般的な構造なので特に問題ないと思います。

├── app
│   ├── Http
│   │   └── Controller 
│   │       └── BookController.php
│   └── Book.php
├── resources
│   └── js
│       ├── components
│       │   ├── Book.vue
│       │   ├── BookList.vue
│       │   └── SearchArea.vue
│       ├── app.js
│       ├── bootstrap.js
│       ├── router.js
│       ├── store.js
│       ├── util.js
│       └── App.vue
└── routes
    ├── api.php
    └── web.php

余談ですがresources/js/componentsにBook.vueとBookList.vueがあるのが腑に落ちない方はAtomic Designがピッタリです。
https://uxdaystokyo.com/articles/glossary/atomic-design/

ルーティング

ここからはコードをどんどん載せていきます。

routes/web.php
<?php

Route::get('/{any?}', function () {
    return view('index');
})->where('any', '.+');

画面の変化はJavaScriptで行うことになるので、どのURLでもindex.blade.phpを呼びます。

resources/views/index.blade.php
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>vuetify pagination</title>
  <script src="{{ mix('js/app.js') }}" defer></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

Laravel Mixでコンパイルしたapp.jsを読み込みます。
VuetifyがあるのでCSSは使いません。

webpack.mix.js
const mix = require("laravel-mix");

mix.js("resources/js/app.js", "public/js")
    .version();

webpack.mix.jsはこれだけです。
ここでもCSSは不要です。

routes/api.php
<?php

Route::get('/books', 'BookController@index')->name('books');

apiのルーティングは本を検索するためのものです。

JavaScript

resources/js/app.js
import "./bootstrap";
import Vue from "vue";

import Vuetify from "vuetify";
Vue.use(Vuetify);

import "vuetify/dist/vuetify.min.css";
import "material-design-icons-iconfont/dist/material-design-icons.css";

import router from "./router";
import store from "./store";
import App from "./App.vue";

new Vue({
    el: "#app",
    router,
    store,
    components: { App },
    template: "<App />"
});

app.jsにVuetifyを使用するための記載をします。

resources/js/router.js
import Vue from "vue";
import VueRouter from "vue-router";

import BookList from "./components/BookList.vue";

Vue.use(VueRouter);

const routes = [
    {
        path: "/",
        component: BookList
    }
];

const router = new VueRouter({
    mode: "history",
    routes
});

export default router;

router.jsでルートパスにアクセスしたらBookList.vueが呼ばれるように設定します。

resources/js/store.js
import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";

Vue.use(Vuex);

const store = new Vuex.Store({
    plugins: [createPersistedState()]
});

export default store;

store.jsのvuex-persistedstateはブラウザをリロードしてもVuexのストアを保持してくれるもので、ログイン認証の永続化でよく使われるみたいです。
今回は特に使いませんが便利なので載せときましょう。

resources/js/bootstrap.js
import { getCookieValue } from "./util";

window.axios = require("axios");

// Ajaxリクエストであることを示すヘッダーを付与する
window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";

window.axios.interceptors.request.use(config => {
    // クッキーからトークンを取り出してヘッダーに添付する
    config.headers["X-XSRF-TOKEN"] = getCookieValue("XSRF-TOKEN");

    return config;
});

window.axios.interceptors.response.use(
    response => response,
    error => error.response || error
);
resources/js/util.js
/**
 * クッキーの値を取得する
 * @param {String} searchKey 検索するキー
 * @returns {String} キーに対応する値
 */
export function getCookieValue(searchKey) {
    if (typeof searchKey === "undefined") {
        return "";
    }

    let val = "";

    document.cookie.split(";").forEach(cookie => {
        const [key, value] = cookie.split("=");
        if (key === searchKey) {
            return (val = value);
        }
    });

    return val;
}

bootstrap.jsはとutil.jsはCSRF対策のためのトークンをクッキーから取り出して、リクエストに含める処理です。
こちらのサイトを参考にさせてもらっています。
https://www.hypertextcandy.com/vue-laravel-tutorial-authentication-part-3/
素晴らしいサイトでかなりお世話になりました!

Vue

resources/js/App.vue
<template>
  <v-app>
    <v-content tag="div">
      <v-container fluid>
        <RouterView />
      </v-container>
    </v-content>
  </v-app>
</template>

やっとVuetifyのタグが登場しました。
<v-app>で囲む必要があります。
公式を見れば便利なタグがたくさんあるので、こちらを参考に。
https://vuetifyjs.com/ja/components/paginations

resources/js/components/BookList.Vue
<template>
  <div>
    <search-area @search="searchBooks($event)"></search-area>
    <v-layout wrap>
      <v-flex sm4 pa-2>{{from}}{{to}}件 / {{total}}</v-flex>
      <v-flex sm8>
        <v-layout justify-end>
          <v-pagination v-model="page" :length="length" :total-visible="10"></v-pagination>
        </v-layout>
      </v-flex>
    </v-layout>
    <v-layout wrap>
      <v-flex sm6 pa-2 v-for="(book, key, index) in books" :key="index">
        <book :title="book.title" :publishingYear="book.publishing_year"></book>
      </v-flex>
    </v-layout>
  </div>
</template>

<script>
// Book.vueとSearchArea.vue(検索エリア)を読み込みます。
import Book from "../components/Book.vue";
import SearchArea from "../components/SearchArea.vue";
export default {
  data() {
    return {
      books: [],    // 一覧データ
      page: 1,      // 表示中のページ(v-paginationにバインド)
      length: 0,    // ページネーションのリンクの数(v-paginationのprops)
      urlParams: "",// 検索パラメータ
      from: "",     // 一覧の開始
      to: "",       // 一覧の終了
      total: ""     // 件数
    };
  },
  methods: {
    // 検索ボタンをクリックしたら呼ばれる
    async searchBooks(params) {
      // 検索パラメータをURLに付与してapiを叩く
      this.urlParams = params;
      let url = "/api/books?page=" + this.page + "&" + this.urlParams;
      const response = await axios.get(url);
      // 戻り値をデータに代入すれば表示が変わってくれます
      let books = response.data.data;
      this.books = books;
      this.length = response.data.last_page;
      this.from = response.data.from;
      this.to = response.data.to;
      this.total = response.data.total;
    }
  },
  mounted() {
    // 初期表示
    this.searchBooks(this.urlParams);
  },
  watch: {
    // ページネーションのリンクをクリックするとpageが変わる。
    // pageを監視して、変更されたらsearchBooksを実行
    page: function(newPage) {
      this.searchBooks(this.urlParams);
    }
  },
  components: {
    Book,
    SearchArea
  }
};
</script>

BookList.Vueが今回の肝になるファイルで、ページネーションの処理はここで扱います。
pageというVue.jsで保持しているデータがトリガーになっていて、pageが変更されたら本を検索するapiが発火して、booksというデータが変更され、画面の表示が変わるという流れです。
通常のマルチページアプリケーションではページネーションのリンクのURLに直接遷移することが多いので、一番の違いはここではないでしょうか。

resources/js/components/Book.Vue
<template>
  <v-card class="indigo lighten-5">
    <v-card-title class="pa-1">タイトル:{{title}}</v-card-title>
    <v-card-text class="pa-1">発行年:{{publishingYear}}</v-card-text>
  </v-card>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      required: true
    },
    publishingYear: {
      type: Number,
      required: true
    }
  }
};
</script>

Book.vueはpropsにタイトルの発行年があるだけで簡単です。
<v-card>のclassは色をつけるためのもで、Vuetifyが提供してくれます。
たくさんあるので、これだけあれば困ることはないでしょう。
https://vuetifyjs.com/ja/styles/colors

resources/js/components/SearchArea.Vue
<template>
  <div>
    <v-layout wrap>
      <v-flex sm4 pa-2>
        <v-text-field v-model="searchForm.title" label="タイトル"></v-text-field>
      </v-flex>
      <v-flex sm4 pa-2>
        <v-text-field v-model="searchForm.publishing_year" label="発行年"></v-text-field>
      </v-flex>
      <v-flex pa-2>
        <v-btn @click="clickHandler">検索</v-btn>
      </v-flex>
    </v-layout>
  </div>
</template>

<script>
const querystring = require("querystring");
export default {
  data() {
    return {
      searchForm: {
        title: "",
        publishing_year: ""
      }
    };
  },
  methods: {
    clickHandler() {
      let params = querystring.encode(this.searchForm);
      this.$emit("search", params);
    }
  }
};
</script>

イベント名をsearch、引数をparams(検索パラメータ)にして検索ボタンにクリックイベントを定義しています。
paramsは検索項目に入力されている値をquerystringで文字列に変換してます。
title=A&publishing_year=2018みたいな文字列になります。

PHP

Laravel側で本を検索する処理を作ります。

app/Book.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
}

Modelはあれば良いので中身は空です。

app/Http/Controllers/BookController.php
<?php

namespace App\Http\Controllers;

use App\Book;
use Illuminate\Http\Request;

class BookController extends Controller
{
    public function index(Request $request)
    {
        $per_page = 5; // 1ページあたりの件数
        $input = $request->all();
        $books = Book::select('id', 'title', 'publishing_year');
        if (!empty($input['publishing_year'])) {
            $books = $books->where('publishing_year', $input['publishing_year']);
        }
        if (!empty($input['title'])) {
            $books = $books->where('title', 'LIKE', "%{$input['title']}%");
        }
        $books = $books->paginate($per_page);

        return response()->json($books);
    }
}

検索してます。それだけですw

一応これで完成です!めでたし!!

17
17
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
17
17