Todoリストを作ってみる。　

todoリスト（laravel + vite）を作ってみる。
環境はmacです。
（あらかじめlaravelとかphpとかnpmとかtodoリスト用のデータベースは用意しておきます。）
（LARAVEL VITEはphp 8以上、node 16以上が必要です）



まず、ターミナルで作りたい場所に移動してlaravelでプロジェクトを作成。
```
composer create-project --prefer-dist laravel/laravel todolist
```

できたtodolistのフォルダにある.envファイルにあらかじめ作っておいたデータベースの設定を書きます。

```.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=todolist
DB_USERNAME=[ユーザー名]
DB_PASSWORD=[パスワード]
```

ターミナルでtodolistのフォルダに移動して
```
php artisan make:model Item -m
```

"Model created successfully."が出たのを確認して、
database > migrations の中にある今日の日付のマイグレーションファイルを開いて、
upのfunctionに項目を追加します。


```
public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->boolean('completed')->default(false);
            $table->timestamp('completed_at')->nullable();
            $table->timestamps();
        });
    }
```

ターミナルで
```
//データベースにテーブルを作ります
php artisan migrate
//ついでにコントローラーも作ります
php artisan make:controller ItemController --resource
```


todoリストを動かすのに必要なapiを作成します。
routes > api.php
```api.php
//上の方に追加
use App\Http\Controllers\ItemController;

//省略

//ここから追加
//リストを取得するapi
Route::get('/items', [ItemController::class, 'index']);
Route::prefix('/item')->group( function () {
    //リストを追加するapi
    Route::post('/store', [ItemController::class, 'store']);
    //リストを変更するapi
    Route::put('/{id}', [ItemController::class, 'update']);
    //リストを削除するapi
    Route::delete('/{id}', [ItemController::class, 'destroy']);
});

```

todoリストを動かすのに必要なファンクションを作成します。
（すでにあるindex(),store(),update(),destroy()を変更します。）
app > Http > Controllers > ItemController.php
```ItemController.php
//上の方に追加
use App\Models\Item;
use Carbon\Carbon;

//省略

public function index()
    {
        return Item::orderBy('created_at', 'DESC')->get();
    }

//省略

public function store(Request $request)
    {
        $newItem = new Item;
        $newItem->name = $request->item["name"];
        $newItem->save();

        return $newItem;
    }

//省略

public function update(Request $request, $id)
    {
        $existingItem = Item::find( $id );
        if($existingItem) {
            $existingItem->completed = $request->item['completed'] ? true : false;
            $existingItem->completed_at = $request->item['completed'] ? Carbon::now() : null;
            $existingItem->save();
            return $existingItem;
        }

        return "Item not found";
    }

//省略

public function destroy($id)
    {
        $existingItem = Item::find( $id );
        if($existingItem) {
            $existingItem->delete();
            return "Item successfully deleted";
        }

        return "item not found";
    }


```


ここでできたらPostManとかでAPIのテストをしたほうがいいです。


ここからフロント部分をviteで作っていきます。
LaravelでViteを使うための laravel-vite をインストールします。
```
//いらないファイル削除
rm webpack.mix.js
npm remove laravel-mix
//PHPパッケージとViteプラグインのインストール
composer require "innocenzi/laravel-vite:0.2.*"
npm i -D vite vite-plugin-laravel @vitejs/plugin-vue
```



package.jsonのscriptsの部分を書き換えます。
```
"scripts": {
        "dev": "vite",
        "build": "vite build"
    },
```


package.jsonと同じ場所にvite.config.tsファイルを作ります。
```vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import laravel from 'vite-plugin-laravel'

export default defineConfig({
    plugins: [
        vue(),
        laravel()
    ]
})

```



viteに合わせて、require()をimportへ変更します。

```resources/js/bootstrap.js
import _ from 'lodash';

// 省略

import axios from 'axios'

axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

// 省略
```

app.jsにvueのマウント設定を追加します。
```resources/js/app.js
import './bootstrap'

import { createApp } from 'vue'
import App from './vue/App.vue'
const app = createApp(App)
app.mount('#app')
```

welcome.blade.phpを変更します。
```resources/views/welcome.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <!-- Fonts -->
        <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
    </head>
    <body>
        <div id="app"></div>
        @vite
    </body>
</html>
```

あらかじめfont-awsomeをインストールしておきます。
ターミナルで
```
npm i --save @fortawesome/fontawesome-svg-core
npm i --save @fortawesome/free-solid-svg-icons
npm i --save @fortawesome/vue-fontawesome@prerelease
```

app.jsにfont-awsomeを追加します。
```resources/js/app.js
// 省略
//ここから追加
import { library } from '@fortawesome/fontawesome-svg-core'
import { faSquarePlus, faTrash } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'

library.add(faSquarePlus, faTrash)

//ここから変更
const app = createApp(App)
app.component('font-awesome-icon', FontAwesomeIcon )
app.mount('#app')
```



resources/js/　の中にvueディレクトリを作り、その中に
・App.vue
・addItemForm.vue
・listView.vue
・listItem.vue
を作ります。


```resources/js/vue/App.vue
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
import addItemForm from "./addItemForm.vue"
import listView from "./listView.vue"

const items = ref([])
const getList = () => {
    axios.get('api/items')
    .then( response => {
        items.value = response.data
    })
    .catch( error => {
        console.log(error)
    })
}

onMounted(() => {
    getList();
})
</script>

<template>
  <div class="todoListContainer">
      <div class="heading">
          <h2 id="title">Todo List</h2>
          <add-item-form @reloadlist="getList()" />
      </div>
      <list-view :items="items"
                @reloadlist="getList()"
      />
  </div>
</template>

<style scoped>
.todoListContainer {
    width: 350px;
    margin: auto;
}
.heading {
    background: #e6e6e6;
    padding: 10px;
}
#title {
    text-align: center;
}
</style>
```

```resources/js/vue/addItemForm.vue
<script setup>
import { reactive } from 'vue'
import axios from 'axios'

const emit = defineEmits();
const item = reactive({ name: ""})
const addItem = () => {
    if(item.name=='') {
        return;
    }

    axios.post('api/item/store', {
        item: item
    })
    .then(response => {
        if( response.status == 201){
            item.name = "";
            emit('reloadlist');
        }
    })
    .catch( error => {
        console.log(error);
    })
}

</script>

<template>
    <div class="addItem">
        <input type="text" v-model="item.name" />
        <font-awesome-icon
            icon="square-plus"
            @click="addItem()"
            :class="[item.name ? 'active' : 'inactive', 'plus']"
        />
    </div>
</template>

<style scoped>
.addItem {
    display: flex;
    justify-content: center;
    align-items: center;
}
input {
    background: #f7f7f7;
    border: 0px;
    outline: none;
    padding: 5px;
    margin-right: 10px;
    width: 100%;
}
.plus {
    font-size: 20px;
}
.active {
    color: #00CE25;
}
.inactive {
    color: #999;
}
</style>
```

```resources/js/vue/listView.vue
<script setup>
    import listItem from "./listItem.vue"

    const props = defineProps({
        items: Object
    })
    const emit = defineEmits();
</script>

<template>
    <div>
        <div v-for="(item, index) in items" :key="index">
            <list-item
                :item="item"
                class="item"
                @itemchanged="emit('reloadlist')"
            />
        </div>
    </div>
</template>

<style scoped>
.item {
    background: #e6e6e6;
    padding: 5px;
    margin-top: 5px;
}
</style>
```

```resources/js/vue/listItem.vue
<script setup>
    import axios from "axios"

    const props = defineProps({
        item: Object
    })
    const emit = defineEmits();

    const updateCheck = () => {
        axios.put('api/item/' + props.item.id, {
            item: props.item
        })
        .then( response => {
            if( response.status == 200 ) {
                emit('itemchanged');
            }
        })
        .catch( error => {
            console.log(error);
        })
    }

    const removeItem = () => {
        axios.delete('api/item/' + props.item.id)
        .then( response => {
            if( response.status == 200 ) {
                emit('itemchanged');
            }
        })
        .catch( error => {
            console.log( error );
        })
    }
</script>

<template>
    <div class="item">
        <input type="checkbox"
                @change="updateCheck()"
                v-model="item.completed"
        />
        <span :class="[item.completed ? 'completed' : '', 'itemText']">
            {{ item.name }}
        </span>
        <button @click="removeItem()" class="trashcan">
            <font-awesome-icon icon="trash" />
        </button>
    </div>
</template>

<style scoped>
.completed {
    text-decoration: line-through;
    color: #999;
}
.itemText {
    width: 100%;
    margin-left: 20px;
}
.item {
    display: flex;
    justify-content: center;
    align-items: center;
}
.trashcan {
    background: #e6e6e6;
    border: none;
    color: #ff0000;
    outline: none;
}
</style>
```


これで完成なので、ターミナルでまずviteを立ち上げる
```
npm run dev
```

次に別のターミナルでlaravelを立ち上げます
```
php artisan serve
```

立ち上がったlaravelにtodoリストができてるはず。。



------------------------------------------------
------------------------------------------------

参考

https://blog.capilano-fw.com/?p=10168

https://laravel-vite.dev/guide/quick-start.html

https://github.com/FortAwesome/vue-fontawesome

https://qiita.com/matsu-matsu/items/89ca4a5374cb2404c4bf