2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vue3 / Composition API】Vue.jsのTodoMVCをシンプルにしてみた

Posted at

概要

こちらの記事を参考に、Vueの学習をしています。

上記の記事にもある通り、Vue.js公式サイトの実装例『TodoMVC』はUIが凝っていて初心者にはレベルが高いです。

そこで、Vueの機能の学習に注力するため、公式の実装例の複雑なUIをそぎ落として、シンプルなTodoリストを作成してみました。

実装した機能の一覧

以下の通りです。公式の実装例の機能は網羅できていると思います。

✅ Todoの追加・削除・編集
✅ 完了状態の変更
✅ ダブルクリックで編集開始
✅ 未完了の数を表示
✅ 完了済の非表示切り替え
✅ 完了済の一括削除
localStorageへの保存と復元
✅ 固有IDの付与(ただし実装例のDate.now()ではなく、より安全なUUIDを使用)

完成品

image.png

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の使い分け

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?