概要
こちらの記事を参考に、Vueの学習をしています。
上記の記事にもある通り、Vue.js公式サイトの実装例『TodoMVC』はUIが凝っていて初心者にはレベルが高いです。
そこで、Vueの機能の学習に注力するため、公式の実装例の複雑なUIをそぎ落として、シンプルなTodoリストを作成してみました。
実装した機能の一覧
以下の通りです。公式の実装例の機能は網羅できていると思います。
✅ Todoの追加・削除・編集
✅ 完了状態の変更
✅ ダブルクリックで編集開始
✅ 未完了の数を表示
✅ 完了済の非表示切り替え
✅ 完了済の一括削除
✅ localStorage
への保存と復元
✅ 固有IDの付与(ただし実装例のDate.now()
ではなく、より安全なUUIDを使用)
完成品
Vue SFC Playgroundでコードと動作を確認できます。
コード
<script setup>
import { ref, computed, watchEffect } from 'vue'
// ローカルストレージからの取り出し
const savedTodos = localStorage.getItem('todos')
// 状態
const todos = ref(savedTodos ? JSON.parse(savedTodos) : [] )
const newTodo = ref('')
const editingId = ref(0);
const isShowCompleted = ref(true)
// フィルタ済のTodoリスト
// todosやisCompletedが変わるたびに変更したいため、computedを用いて再計算させる
const displayTodos = computed(() =>
isShowCompleted.value ? todos.value : todos.value.filter((t) => !t.done)
)
// 未完了の数
const unCompletedNum = computed(() =>
todos.value.filter((t) => !t.done).length
)
// ローカルストレージへの保存
watchEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos.value))
})
// メソッド
const addTodo = () => {
const title = newTodo.value.trim()
if (title) {
todos.value.push({
id: window.crypto.randomUUID(),
title: newTodo.value,
done: false,
})
}
newTodo.value = ""
}
const startEdit = (id) => {
editingId.value = id;
}
const doneEdit = (todo, e) => {
const newTitle = e.target.value.trim()
if (newTitle) {
todo.title = newTitle
} else {
removeTodo(todo.id)
}
editingId.value = 0
}
const removeTodo = (id) => {
todos.value = todos.value.filter((t) => t.id !== id)
}
const removeCompleted = () => {
todos.value = todos.value.filter((t) => !t.done)
}
const toggleShowCompleted = () => {
isShowCompleted.value = !isShowCompleted.value
}
</script>
<template>
<div style="max-width: 400px; margin: auto; padding: 1em;">
<h2>Todos</h2>
<div>
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="What needs to be done?"
>
<button @click="addTodo">
Add
</button>
</div>
<div style="margin-top: 0.5em;">
<button @click="toggleShowCompleted">
{{ isShowCompleted ? "Hide" : "Show"}} completed
</button>
</div>
<p>
<strong>{{ unCompletedNum }}</strong> {{ unCompletedNum === 1 ? "item" : "items" }} left
</p>
<ul>
<li v-for="todo in displayTodos" :key="todo.id">
<input type="checkbox" v-model=todo.done>
<span
v-if="editingId !== todo.id"
:class="{ 'done': todo.done }"
@dblclick="startEdit(todo.id)"
>
{{ todo.title }}
</span>
<input
v-else
:value=todo.title
@keyup.enter="doneEdit(todo, $event)"
@blur="doneEdit(todo, $event)"
class="edit-input"
>
<button @click="removeTodo(todo.id)">🗑</button>
</li>
</ul>
<button @click="removeCompleted()" v-show="todos.length > unCompletedNum">Clear completed</button>
</div>
</template>
<style scoped>
.done {
text-decoration: line-through;
}
.edit-input {
margin-right: 0.5em;
padding: 2px 4px;
font-size: 1em;
}
input {
margin-right: 0.5em;
}
button {
margin-left: 0.5em;
}
</style>
参考
HTML クラスのバインディング
v-showとv-ifの違い
ブラウザでUUID生成
ローカルストレージ
watchとwatchEffectの使い分け