Laravel6.x + Vue.js(TypeScript)のSPAでページネーション付きのテーブル作成

# 冒頭

- 個人開発でLaravel6.x + Vue.js(TypeScript)のSPA作ったので一部抜粋
- CSSフレームワークはVuetify2を採用
- 環境構築については一旦スルーします
- ソースコード例は[こちら](https://github.com/ProjectEuropa/europa-laravel-vuetify-ts)

## ディレクトリ構造

フロント側の構造のみ抜粋

```
resources/ts
├── App.vue
├── app.ts
├── components
│   └── pages
│       └── Search.vue
├── laravel-data-entity
│   └── PaginateObject.d.ts
├── plugins
│   └── http.ts
├── router.ts
├── types
│   └── index.d.ts
└── vue-data-entity
    └── DataObject.d.ts
```

## ソースコード

### 型定義ファイル

- データベースのテーブルに用意してるテーブルの構造を定義する

```resources/ts/vue-data-entity/DataObject.d.ts
export interface FileDataObject {
  id: number;
  hoge: string;
}
```

- Laravelのページネーションオブジェクトを定義する

```resources/ts/laravel-data-entity/PaginateObject.d.ts
import { DataObject } from "../vue-data-entity/FDataObject"

export interface PaginateObject {
  current_page: number;
  data: DataObject[];
  first_page_url: string;
  from: number;
  last_page: number;
  last_page_url: string;
  next_page_url: string;
  path: string;
  per_page: number;
  prev_page_url: string | null;
  to: number;
  total: number;
}
```

### Vue Routerの設定

```resources/ts/router.ts
import Vue from 'vue'
import Router from 'vue-router'
import Search from './components/pages/Search.vue'
Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/search',
      name: 'Search',
      component: Search,
    },
  ]
});
export default router;
```

### Axiosをラップしたプラグインを作る

```resources/ts/plugins/http.ts
import _Vue from 'vue';
import axios from 'axios';

export default {
  install(Vue: typeof _Vue): void {
    const http = axios.create({
      responseType: "json"
    });
    Vue.prototype.$http = http;
  },
};

```

### プラグインをインストール、ルーターを設定

```resources/ts/app.ts
import Vue from "vue";
import router from './router'
import App from "./App.vue";
~~省略~~
import http from './plugins/http';

Vue.use(http);

~~省略~~

new Vue({
  router: router,
  render: h => h(App),
~~省略~~
}).$mount('#app')
```

### Vue側のソースコード例

```resources/ts/components/pages/Search.vue
<template>
  <v-content>
    <v-container fluid>
      <v-simple-table dense>
        <template v-slot:default>
          <thead>
            <tr>
              <th class="text-left">id</th>
              <th class="text-left">hoge</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(item, index) in data" :key="index">
              <td>{{ item.id }}</td>
              <td>{{ item.hoge }}</td>
            </tr>
          </tbody>
        </template>
      </v-simple-table>
      <v-pagination v-model="page" :length="pageLength"></v-pagination>
    </v-container>
    <v-overlay :value="overlay">
      <v-progress-circular color="primary" indeterminate size="64"></v-progress-circular>
    </v-overlay>
  </v-content>
</template>

<script lang="ts">
import { DataObject } from "../../vue-data-entity/DataObject";
import { PaginateObject } from "../../laravel-data-entity/PaginateObject";
import { Vue, Component, Watch } from "vue-property-decorator";
import VueRouter from "vue-router";
import { AxiosError, AxiosResponse } from "axios";

// registerHooksを登録しないとbeforeRouteUpdateが定義できません
Component.registerHooks(["beforeRouteUpdate"]);
export default class Search extends Vue {
  data: FileDataObject[] = [];
  overlay: boolean = false;
  pageLength: number = 1;
  page: number = 1;

  /**
   * created
   */
  public created() {
    this.search();
  }

  /**
   * watch
   * ルーティングに反映するためにWatchを使います。
   */
  @Watch("page")
  onPageChanged() {
    this.$router
      .push({
        name: "Search",
        query: {
          page: this.page.toString()
        }
      })
  }


  /**
   * search
   */
  public search() {
    this.page = isNaN(Number(this.$route.query.page))
      ? 1
      : Number(this.$route.query.page);
    this.overlay = true;
    Vue.prototype.$http
      .get(
        `/api/search?page=${this.page}`
      )
      .then((res: AxiosResponse<PaginateObject>): void => {
        this.data = res.data.data;
        this.pageLength = res.data.last_page;
        this.overlay = false;
      })
      .catch((error: AxiosError): void => {
        alert("検索実行時にエラーが発生しました");
        this.overlay = false;
      });
  }

  /**
   * beforeRouteUpdate
   * ルーティングが更新される毎に(this.pageが変更する毎に検索を実行します)
   */
  public beforeRouteUpdate(to: VueRouter, from: VueRouter, next: any) {
    next();
    this.search();
  }

}
</script>
```

```resources/ts/App.vue
<template>
  <v-app id="inspire">
    <router-view />
  </v-app>
</template>
```

### Laravel側

### APIのルーティングを定義

```routes/api.php
Route::get('search', 'Api\SearchController@search');
```

### APIのコントローラーでページネーションオブジェクトを返却する

```app/Http/Controllers/Api/SearchController.php
<?php

namespace App\Http\Controllers\Api;

use App\Data; //モデルを定義しといてね
use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

class SearchController extends Controller
{
    /**
     *
     *
     * @return
     */
    public function search(Request $request)
    {
        return Data::select('id', 'hoge')->paginate(10);
    }
}
```

```resources/views/index.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
  <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# website: http://ogp.me/ns/website#">
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <meta name="robots" content="index,follow">
    <title>~~~~</title>
  </head>
    <body>
      <div id="app" >
      </div>
      <script src="{{ mix('/js/app.js') }}"></script>
    </body>
</html>
```

##動作例


![n9o60-s67f1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133718/a8ed01f3-08a7-49db-616b-a1dc0caeebdf.gif)