サンプルコード

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>