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ファイルにあらかじめ作っておいたデータベースの設定を書きます。
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
//上の方に追加
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
//上の方に追加
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ファイルを作ります。
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へ変更します。
import _ from 'lodash';
// 省略
import axios from 'axios'
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// 省略
app.jsにvueのマウント設定を追加します。
import './bootstrap'
import { createApp } from 'vue'
import App from './vue/App.vue'
const app = createApp(App)
app.mount('#app')
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を追加します。
// 省略
//ここから追加
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
を作ります。
<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>
<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>
<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>
<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リストができてるはず。。
参考