👩‍💻

Vue.js3でTODOリストを実装④

に公開

勉強の備忘録として記事を書いてます。
Vue.jsでTODOリストを開発していきます。Geminiでコード生成して、不明点や疑問点はchatGPTを頼り、補足してもらうような形で開発を進めました。
アウトプットして自分の中の知識を固めたいという思いで書いていますが、誰かのために役に立てれば嬉しいです。

今回は「タスク一覧が表示される部分とタスクごとの完了・編集・削除ができる」子コンポーネントTodoList.vueのコードをまとめていきます。


🐔 親コンポーネント(App.vue)

App.vue
<template>
  <TodoList
    :todos="filteredTodos"
    @toggle-complete="toggleComplete"
    @edit-todo="editTodo"
    @delete-todo="openModal" />
</template>

🐤 子コンポーネント(TodoList.vue)

TodoList.vue
<script setup>
import { ref, nextTick } from 'vue';

const props = defineProps({
    todos: {
        type: Array,
        required: true
    }
});

const emit = defineEmits([
    'toggle-complete',
    'edit-todo',
    'delete-todo'])

const editingText = ref('');

</script>

import { nextTick } from 'vue';
DOM更新後に確実に処理を実行したいときに使うVueの機能。今回は編集モードに切り替わったあとinputに自動でフォーカスするために使う。

propsemitの定義は前回と同様のため説明は省略します。
変更点としては子のtodosは、親の:todos="filteredTodos"が渡る部分。親の"filteredTodos"は、タスクの配列(全て・完了・未完了)なので子側では配列として受け取る必要があるため、type: Arrayを指定する。

const editingText = ref('');については、編集中の入力を一時的に保持するための変数。直接todo.titleを編集せずに入力内容をeditingTextに保存してから最終的に親へ送るため、空欄や空白のまま保存されるのを防げる。


TodoList.vue
<template>
<ul>
    <li>
        <!-- 入力したタスクが表示される -->
        <template>
            <span>{{ todo.title }}</span>
        </template>

        <!-- 編集モードにしたときの入力欄 -->
        <template>
            <input type="text" />
        </template>

        <!-- タスクに表示させるボタン -->
        <div class="actions">
            <button>完了</button>
            <button>編集</button>
            <button>保存</button>
            <button>削除</button>
        </div>
    </li>
</ul>
</template>

🎡 v-forを用いてタスクをループ表示させる

TodoList.vue
<template>
    <li
    v-for="todo in todos"
    :key="todo.id"
    :class="{ completed: todo.completed }"
    :data-id="todo.id">
    ...
    </li>
</template>

変数todotodosの配列を順番に取り出して表示。todo.idを`keyにすることでVueが効率よくレンダリングできる。

:class="{ completed: todo.completed }"
.todo.completetrueのときクラス名completedが付与される。完了済みスタイル(取り消し線や背景色など)を付けることができる。

:data-id="todo.id"
DOM側でタスクを特定するために必要な属性。nextTickの関数内で定義されている中身('${todo.id}')に登場してます。

const inputElement = document.querySelector(`.todo__list li[data-id='${todo.id}'] .edit__input`);

編集ボタンを押したときに特定のタスク(idが一致する)に対応する<input>を探して、そこに.focus()を当てるため。

みたいな感じでdata-idを付けておくと、複数のタスクが存在してもVue外の処理(DOM操作)で個別に扱いやすくなる。


🔘 編集モードの切り替え

TodoList.vue
<script setup>
const startEdit = (todo) => {
    todo.isEditing = true;
    editingText.value = todo.title;

    nextTick(() => {
        const inputElement = document.querySelector(`.todo__list li[data-id='${todo.id}'] .edit__input`);
        if (inputElement) inputElement.focus();
    });
};
</script>

todo.isEditingfalsetrueに変わると、<template><span>「表示」が<input>「編集」に切り替わる。

editingText.value = todo.title;で編集をする前にもともとのタスクを一時的にeditingTextに保存しておく。v-modeleditingTextにすることで、{{ todo.title }}が書き換わらないようにしてる。

nextTick()を使うことでDOM更新後に<input>にフォーカスを当てられる。


🌝 タスクごとのボタンを設定

TodoList.vue
<template>
<div>
    <button
        @click="emit('toggle-complete', todo.id)">
        完了
    </button>
    
    <button
        v-if="!todo.isEditing"
        @click="startEdit(todo)">
        編集
    </button>
    
    <button
        v-else
        @click="saveEdit(todo.id, todo.title)">
        保存
    </button>
    
    <button
        @click="emit('delete-todo', todo.id)">
        削除
    </button>
</div>
</template>

「完了」:子から親にtoggle-completeイベントを送信して親側で完了状態を切り替える。

「編集」:startEdit(todo)を実行し編集モードをtrueにする

「保存」:saveEdit(todo.id, todo.title)が実行され、edit-todoイベントとして親へ編集内容を送信

「削除」:子から親にdelete-todoイベントを送信して親側で削除モーダルを開く

TodoList.vue
<template>
<template v-if="!todo.isEditing">
    <span @click="emit('toggle-complete', todo.id)">
    {{ todo.title }}
    </span>
</template>

<template v-else>
    <input
    type="text"
    v-model="editingText" />
</template>
</template>

emit('toggle-complete', todo.id)
前回同様子から親にイベントを送る仕組みでクリックで発火する。

toggle-completeイベントを親の@toggle-completeに送る。
v-forでループすることでtodoには1件ずつオブジェクトがはいる。第2引数のtodo.idは「親に渡したいデータ(タスクのid)」を値として一緒に渡す。そうすると親側のtoggleCompletが実行される。

v-model="editingText"
「入力中は一時的にeditingTextに保存→保存ボタンやEnterで反映する」ことができる。入力中に直接todo.titleを書き換えないことで空欄保存を防げる。


🍳 編集した内容を親に渡す

TodoList.vue
<template>
<template v-else>
    <input type="text"
    v-model="editingText"
    @keyup.enter="saveEdit(todo.id, todo.title)"
    @blur="saveEdit(todo.id, todo.title)" />
</template>
<template>

ひとつ前の<template>input@keyup@blurを追加します。

ユーザーがEnterを押す@keyup.enter・編集状態から離れる(フォーカスを外す)@blurのタイミングsaveEdit()が呼ばれる。

TodoList.vue
<script setup>
const saveEdit = (id) => {
    const newTitle = editingText.value;
    if (newTitle.trim()) {
        emit('edit-todo', { id, newTitle: newTitle.trim() });
    } else {
        const todo = props.todos.find(t => t.id === id);
        if (todo) todo.isEditing = false;
    }
};
</script>

editingText.valuenewTitleを取り出す。
if (newTitle.trim())は空白を除いたあとに何か文字が残っているのかをチェックしているので、文字があればtrue(保存)・何も残っていなければfalse(保存しない)でif文をつくる。

trim()は空白を削除するため、if文で「その結果が空じゃない=入力がある」として使うことができる。

空じゃなければemit('edit-todo', { id, newTitle })を実行して親にデータを送る。親側ではedit-todoイベントを受け取りidが一致するタスクのtitleを上書きする。
空だった場合は、props.todosの配列の中からidが一致するタスクtodoを探し出して、見つかったらそのisEditing保存せずに false;で編集モードを解除する。

Discussion