動画はこちら↓
動画で作成したTODOアプリの完成系コードです!
TodoApp.vue
<template> <div class="todoApp"> <TodoInput></TodoInput> <TodoListView></TodoListView> </div> </template> <script setup> import TodoInput from "./TodoInput.vue"; import TodoListView from "./TodoListView.vue"; </script> <style scoped> .todoApp > * { margin: 2rem 0 0 0; } </style>
TodoInput.vue
<template> <div> <p v-if="isErrMsg">タスク・期限を両方入力してください。</p> <form @submit="onSubmitForm"> <label>やること<input type="text" v-model="input" /></label><br /> <label>期限<input type="date" v-model="inputDate" /></label><br /> <input class="submit" type="submit" value="登録!" /> </form> </div> </template> <script setup> import { ref } from "vue"; import { statuses } from "../const/statuses"; const input = ref(""); const inputDate = ref(""); const isErrMsg = ref(false); function onSubmitForm() { if (input.value == "" || inputDate.value == "") { isErrMsg.value = true; event.preventDefault(); return; } const items = JSON.parse(localStorage.getItem("items")) || []; const newItem = { id: items.length, content: input.value, limit: inputDate.value, state: statuses.NOT_START, onEdit: false, }; items.push(newItem); localStorage.setItem("items", JSON.stringify(items)); } </script> <style scoped> input { width: 70%; } label { display: flex; justify-content: space-between; } .submit { width: 100%; } </style>
TodoListView.vue
<template> <div> <div v-if="isShowModal" class="modal"> <div class="modal-content"> <p>{{ deleteItemContent }}を削除してもよろしいですか?</p> <button @click="onDeleteItem()">はい</button> <button @click="onHideModal()">キャンセル</button> </div> </div> <p v-if="isErrMsg">{{ errMsg }}</p> <table> <tr class="title"> <th class="th-id">ID<button @click="sortById()">↓</button></th> <th class="th-value">やること</th> <th class="th-limit">期限<button @click="sortByLimit()">↓</button></th> <th class="th-state">状態</th> <th class="th-edit">編集</th> <th class="th-delete">削除</th> </tr> <!--タスクの件数分表示--> <tr v-for="item in items" :key="item.id" :class="{ red: new Date(item.limit) < today }" > <td>{{ item.id }}</td> <td> <span v-if="!item.onEdit">{{ item.content }}</span> <!-- <input v-else type="text" /> --> <input v-else v-model="inputContent" type="text" /> </td> <td> <span v-if="!item.onEdit">{{ item.limit }}</span> <!-- <input v-else type="date" /> --> <input v-else v-model="inputLimit" type="date" /> </td> <td> <span v-if="!item.onEdit">{{ item.state.value }}</span> <select v-else v-model="inputState"> <!-- <select v-else> --> <option v-for="state in statuses" :key="state.id" :value="state" :selected="state.id == item.state.id" > {{ state.value }} </option> </select> </td> <td> <button class="btn" v-if="!item.onEdit" @click="onEdit(item.id)"> 編集 </button> <button class="btn" v-else @click="onUpdate(item.id)">完了</button> </td> <td> <button class="btn" @click="showDeleteModal(item.id)">削除</button> </td> </tr> <!--タスクの件数分表示--> </table> </div> </template> <script setup> import { statuses } from "../const/statuses"; import { ref } from "vue"; let items = ref(JSON.parse(localStorage.getItem("items")) || []); let inputContent = ref(); //タスクの内容 let inputLimit = ref(); //タスクの期限 let inputState = ref(); //タスクのステータス let isErrMsg = ref(false); let isShowModal = ref(false); let errMsg = ref(""); //エラーメッセージの内容 let deleteItemId = ""; //削除対象のItemのID let deleteItemContent = ref(); //削除対象のItemの内容 const today = new Date(); function onEdit(id) { let isOnEditOther = false; items.value.map((item) => { if (item.onEdit) { isOnEditOther = true; return; } }); if (isOnEditOther) { errMsg.value = "他に編集中のタスクがあります"; isErrMsg.value = true; return; } inputContent.value = items.value[id].content; inputLimit.value = items.value[id].limit; inputState.value = items.value[id].state; items.value[id].onEdit = true; } function onUpdate(id) { if (inputContent.value == "" || inputLimit.value == "") { errMsg.value = "タスクの内容と期限を入力してください。"; isErrMsg.value = true; return; } const newItem = { id: id, content: inputContent.value, limit: inputLimit.value, state: inputState.value, onEdit: false, }; items.value.splice(id, 1, newItem); localStorage.setItem("items", JSON.stringify(items.value)); isErrMsg.value = false; } function showDeleteModal(id) { isShowModal.value = true; deleteItemId = id; deleteItemContent = items.value[id].content; } function onDeleteItem() { //タスクを削除する処理 items.value.splice(deleteItemId, 1); //IDを振り直す items.value = items.value.map((item, index) => ({ id: index, content: item.content, limit: item.limit, state: item.state, onEdit: item.onEdit, })); localStorage.setItem("items", JSON.stringify(items.value)); isShowModal.value = false; } function onHideModal() { isShowModal.value = false; } function sortByLimit() { //日付でソートする items.value.sort((a, b) => new Date(a.limit) - new Date(b.limit)); localStorage.setItem("items", JSON.stringify(items.value)); } function sortById() { //IDでソートする items.value.sort((a, b) => a.id - b.id); localStorage.setItem("items", JSON.stringify(items.value)); } </script> <style scoped> .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; } .modal-content { background: #fff; padding: 20px; border-radius: 8px; } .red { color: red; } table > * > th { width: 16.666%; } table { width: 100%; } .btn { width: 100%; } button { border: none; border-radius: 5px; } .title { background-color: rgb(158, 212, 158); } </style>