Laravel チュートリアルのレイアウトをVue.jsで置き換える

Laravel + Vue.js開発の勉強として
Laravelチュートリアル(基本のタスクリスト)のレイアウトをVue.jsで置き換えてみた作業記録

# 構成

- Laravel：APIサーバー(DBのCRUD処理を提供)
- Vue.js：フロントエンド処理一式


# 元ネタ

- 基本のタスクリスト 5.1 Laravel
https://readouble.com/laravel/5.1/ja/quickstart.html

- Laravel 5.5 API From Scratch Using Resources
https://www.youtube.com/watch?v=4pc6cgisbKE

- Full Stack Vue.js & Laravel
https://www.youtube.com/watch?v=DJ6PD_jBtU0


# バージョン
- Laravel 5.7
- Vue.js 2.5.17

# 完成品
https://github.com/thanatos27/laravel-vue-quickstart

# Laravel API作成

API仕様

|Method  |URI  |Action  |Description
|---|---|---|---|
|POST  |api/task  | App\Http\Controllers\TaskController@store  |新規タスク作成|
|DELETE  |api/task{id}  |App\Http\Controllers\TaskController@destroy  |タスク削除|
|GET  |api/tasks | App\Http\Controllers\TaskController@index  |タスク一覧取得|

## Taskテーブルの設定

``` bash:terminal
php artisan make:migration create_tasks_table --create=tasks
```

``` php:database\migrations\xxxx_create_tasks_table.php
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->timestamps();
        });
    }

```

マイグレーション実行

``` bash:terminal
php artisan migrate
```


Taskモデルを作成

```bash:terminal
php artisan make:model Task
```

## ルーティング

```php:routes\api.php
/**
 * Display All Tasks
 */
Route::get('tasks', 'TaskController@index');

/**
 * Add A New Task
 */
Route::post('task', 'TaskController@store');

/**
 * Delete An Existing Task
 */
Route::delete('task/{id}', 'TaskController@destroy');

```



## Taskコントローラー作成

```bash:terminal
php artisan make:controller TaskController --resource
```

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Task;
use App\Http\Resources\TaskResource;

class TaskController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $tasks = Task::orderBy('created_at', 'asc')->get();

        return TaskResource::collection($tasks);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $task = new Task;

        $task->name = $request->name;

        if($task->save()) {
            return new TaskResource($task);
        }
    } 

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $task = Task::findOrFail($id);

        if ($task->delete()) {
            return new TaskResource($task);
        }
    }
}
```


## リソース作成

```bash:terminal
php artisan make:resource TaskResource
```

レスポンスを送り返す時に、JSONへ変換する必要のある属性の配列を返す、
`toArray`メソッドをリソースクラスで定義する。

```php:app\Http\Resources\TaskResource.php
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
        ];
    }
```

# Taskリストページ作成

## ルーティング

```php:routes\web.php
Route::get('/', function () {
    return view('tasks');
});
```

## View

ポイントを抜粋して解説、
ファイル全体は以下を参照
https://github.com/thanatos27/laravel-vue-quickstart/blob/master/resources/views/tasks.blade.php

### CSRF保護

```php:resources\views\tasks.blade.php
        <!-- CSRF Protection -->
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <script>window.Laravel = { csrfToken: '{{ csrf_token() }}' }</script>
```

### Vueカスタム要素呼び出し部分
`<navbar></navbar>`：ナビゲーションバー
`<tasks></tasks>`：タスクリスト

```html:resources\views\tasks.blade.php
        <div id="app">
            <navbar></navbar>
            <div class="container">
                <tasks></tasks>
            </div>
        </div>
```

# フロントエンド(Vue.js)

quickstart-basicのテンプレートファイルをベースにVue.jsでリプレース
https://github.com/laravel/quickstart-basic/blob/master/resources/views/tasks.blade.php

## コンポーネント登録

```js:resources\js\app.js
Vue.component('navbar', require('./components/Navbar.vue'));
Vue.component('questionnaires', require('./components/Tasks.vue'));
```

## ナビゲーションバー

```vue:resources\js\components\Navbar.vue
<template>
    <nav class="navbar navbar-default">
        <div class="container">
            <div class="navbar-header">

                <!-- Branding Image -->
                <a class="navbar-brand" href="/">
                    Task List
                </a>
            </div>
        </div>
    </nav>
</template>
```

## タスクリスト

ポイントを抜粋して解説、
ファイル全体は以下を参照
https://github.com/thanatos27/laravel-vue-quickstart/blob/master/resources/js/components/Tasks.vue

### 表示/非表示

Laravelの`@if`をVueの`v-show`構文で置き換え

```vue:resources/js/components/Tasks.vue
<!-- Current Tasks -->
<div v-show="tasks.length">
    <div class="panel panel-default">
        <div class="panel-heading">
            Current Tasks
        </div>
```


### 繰り返し制御

Laravelの`@foreach`をVueの`v-for`構文で置き換え

```vue:resources/js/components/Tasks.vue
<tr v-for="task in tasks" v-bind:key="task.id">
    <td class="table-text"><div>{{ task.name }}</div></td>

    <!-- Task Delete Button -->
    <td>
        <button @click="deleteTask(task.id)" class="btn btn-danger">
            <i class="fa fa-btn fa-trash"></i>Delete
        </button>
    </td>
</tr>
```

### ボタンアクション

`@click`で対応するmethodsを呼び出す

```vue:resources/js/components/Tasks.vue
<!-- Add Task Button -->
<div class="form-group">
  <div class="col-sm-offset-3 col-sm-6">
        <button @click="addTask()" class="btn btn-default">
            <i class="fa fa-btn fa-plus"></i>Add Task
        </button>
    </div>
</div>
```

### methods

[fetchAPI](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch)で各種Laravel APIにリクエストをする

#### fetchTasks


タスク一覧取得
URI:`api/tasks`
Method:`GET`

```js:resources/js/components/Tasks.vue
fetchTasks() {
    fetch('api/tasks')
    .then(res => res.json())
    .then(res => {
        this.tasks = res.data;
    })
    .catch(err => console.log(err))
},
```

#### addTask

新規タスク追加
URI:`api/task`
Method:`POST`

```js:resources/js/components/Tasks.vue
addTask() {
    this.errors = [];
    if(!this.task.name) {
        this.errors.push('Name required.');
        return;
    }

    fetch('api/task', {
        method: 'post',
        body: JSON.stringify(this.task),
        headers: {
            'content-type': 'application/json'
        }
    })
    .then(res => res.json())
    .then(data => {
        this.task.name = '';
        this.fetchTasks();
    })
    .catch(err => console.log(err))
},
```

#### deleteTask

タスク削除
URI:`api/task/{id}`
Method:`DELETE

```js:resources/js/components/Tasks.vue
deleteTask(id) {
    fetch(`api/task/${id}`, {
        method: 'delete'
    })
    .then(res => res.json())
    .then(data => {
        this.fetchTasks();
    })
    .catch(err => console.log(err))
}
```

### 簡易的バリデーション処理
```js:resources/js/components/Tasks.vue
addTask() {
    this.errors = [];
    if(!this.task.name) {
        this.errors.push('Name required.');
        return;
    }
```

```vue:resources/js/components/Tasks.vue
<!-- Display Validation Errors -->
<div v-show="errors.length" class="alert alert-danger">
    <strong>Whoops! Something went wrong!</strong>
    <br><br>

    <ul>
        <li v-for="error in errors" v-bind:key="error">{{ error }}</li>
    </ul>
</div>
```

以上