點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Composition API 實作 Todo List

Sam Xiao's Avatar 2024-02-11

Vue 3 提供 Composition API 後,將以完全不同型態實作 Todo List。

Version

Vue 3.4

Todo List

todolist001

<template>
  <div>
    <input type="text" v-model="newTodo" />
    <button @click="onAdd">Add</button>
  </div>
  <div>
    <ul>
      <li v-for="(item, i) in filteredTodos" :key="i">
        <input type="checkbox" v-model="item.isCompleted" />
        <span v-if="!item.isEdit">
          <span>{{ item.todo }}</span>
          <button @click="onEdit(item)">edit</button>
          <button @click="onDelete(item)">delete</button>
        </span>
        <span v-else>
          <input type="text" v-model="item.todo_" />
          <button @click="onSave(item)">save</button>
          <button @click="onCancel(item)">cancel</button>
        </span>
      </li>
    </ul>
  </div>
  <div>
    <span> {{ active.length }} items left</span>
    <button @click="onFilterAll">All</button>
    <button @click="onFilterActive">Active</button>
    <button @click="onFilterCompleted">Completed</button>
    <button v-if="completed.length" @click="onClearCompleted">Clear Completed</button>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

let newTodo = ref('')
let todos = ref([])

let filter = ref('all')
let filteredTodos = computed(() => {
  return {
    all: todos.value,
    active: active.value,
    completed: completed.value
  }[filter.value]
})

let active = computed(() => {
  return todos.value.filter((todo) => !todo.isCompleted)
})

let completed = computed(() => {
  return todos.value.filter((todo) => todo.isCompleted)
})

let onAdd = () => {
  if (newTodo.value === '') return

  todos.value.push({
    id: Symbol(),
    isCompleted: false,
    todo: newTodo.value
  })
  newTodo.value = ''
}

let onEdit = (item) => {
  item.isEdit = true
  item.todo_ = item.todo
}

let onDelete = (item) => {
  let index = todos.value.findIndex((todo) => todo.id === item.id)
  todos.value.splice(index, 1)
}

let onSave = (item) => {
  item.isEdit = false
  item.todo = item.todo_
}

let onCancel = (item) => {
  item.isEdit = false
}

let onFilterAll = () => {
  filter.value = 'all'
}

let onFilterActive = () => {
  filter.value = 'active'
}

let onFilterCompleted = () => {
  filter.value = 'completed'
}

let onClearCompleted = () => {
  completed.value.forEach((item) => {
    let index = todos.value.findIndex((todo) => todo.id === item.id)
    todos.value.splice(index, 1)
  })
}
</script>

Line 3

<input type="text" v-model="newTodo" />
  • v-model:將輸入的新 todo 綁定到 newTodo state

Line 4

<button @click="onAdd">Add</button>
  • 新增一筆 todo,並將 click event 指定到 onAdd()

Line 8

<ul>
  <li v-for="(item, i) in filteredTodos" :key="i">
    <span>{{ item.todo }}</span>
  </li>
</ul>
  • v-for :列舉 filteredTodos computed

Line 9

<input type="checkbox" v-model="item.isCompleted" />
  • v-model:將 checkbox 綁定到 item.isCompleted

Line 10

<span v-if="!item.isEdit">
  <span>{{ item.todo }}</span>
  <button @click="onEdit(item)">edit</button>
  <button @click="onDelete(item)">delete</button>
</span>
  • v-if:若是 非編輯模式,則直接顯示 todo
  • Button 此時為 editdelete

Line 15

<span v-else>
  <input type="text" v-model="item.todo_" />
  <button @click="onSave(item)">save</button>
  <button @click="onCancel(item)">cancel</button>
</span>
  • v-else:若是 編輯模式,則以 <input> 顯示 todo,注意此時 v-model 綁定到 item.todo_ 而非 item.todo,此為儲存 暫時 的 todo,主要為了 cancel 所用
  • Button 此時為 savecancel

Line 24

<span> {{ active.length }} items left</span>
  • 顯示目前尚未完成的 todo 筆數

Line 25

<button @click="onFilterAll">All</button>
  • 顯示所有 todo,並將 click event 指定到 onFilterAll()

Line 26

<button @click="onFilterActive">Active</button>
  • 顯示尚未完成 todo,並將 click event 指定到 onFilterActive()

Line 27

<button @click="onFilterCompleted">Completed</button>
  • 顯示已完成 todo,並將 click event 指定到 onFilterCompleted()

Line 28

<button v-if="completed.length" @click="onClearCompleted">Clear Completed</button>
  • v-if:若有已完成 todo,則顯示 Clear Completed button
  • click event 指定到 onClearCompleted()

Line 35

let newTodo = ref('')
let todos = ref([])
  • newTodo state:儲存新輸入的 todo
  • todos state:儲存所有 todo

Line 38

let filter = ref('all')
let filteredTodos = computed(() => {
  return {
    all: todos.value,
    active: active.value,
    completed: completed.value
  }[filter.value]
})
  • filter state:儲存 AllActiveCompleted button 所需的 state
  • filteredTodos computed:當 todo state 改變時,filteredTodos computed 會 reactive 跟著改變,只顯示根據目前 filter state 所指定的 todo

Line 47

let active = computed(() => {
  return todos.value.filter(todo => !todo.isCompleted)
})
  • active computed:當 todo state 改變時,active computed 會 reactive 跟著改變,只顯示所有 未完成 todo

Line 51

let completed = computed(() => {
  return todos.value.filter(todo => todo.isCompleted)
})
  • completed computed:當 todo state 改變時,completed computed 會 reactive 跟著改變,只顯示所有 已完成 todo

Line 55

let onAdd = () => {
  if (newTodo.value === '') return

  todos.value.push({
    id: Symbol(),
    isCompleted: false,
    todo: newTodo.value
  })
  newTodo.value = ''
}
  • 新增一筆 todo
  • 使用 push 新增 todo array
  • id 使用 Symbol() 實現 UUID

Line 66

let onEdit = item => {
  item.isEdit = true
  item.todo_ = item.todo
}
  • 編輯一筆 todo
  • 在 item 下新增 isEdit property 為 true 表示為 編輯模式,此時 button 會改為顯示 savecancel
  • 在 item 下新增 todo_ property 作為臨時 state,並將目前 toto 值寫入 todo_,作為綁定 <input>

由於 JavaScript 為 動態語言,我們可對 Object 動態新增 property 作為臨時變數,此為 JavaScript 慣用手法

Line 71

let onDelete = item => {
  let index = todos.value.findIndex(todo => todo.id === item.id)
  todos.value.splice(index, 1)
}
  • 刪除一筆 todo
  • 若要使用 splice() 直接對 todos state 進行刪除,則必須找到要刪除 todo 的 index
  • 使用 findIndex() 由目前 todo 的 id 找到要刪除 todo 的 index

Line 76

let onSave = item => {
  item.isEdit = false
  item.todo = item.todo_
}
  • 儲存一筆 todo
  • 將 item 的 isEdit 設定為 false 表示為 非編輯模式,此時 button 會改為顯示 editdelete
  • 因為目前編輯的 <input> 綁定到 item.todo_,將資料從 item.todo_ 複製到 item.todo 完成儲存

Line 81

let onCancel = item => {
  item.isEdit = false
}
  • 取消編輯一筆 todo
  • 將 item 的 isEdit 設定為 false 表示為 非編輯模式,此時 button 會改為顯示 editdelete

亦可以使用 delete item.todo_ 動態刪除 _todo property 或 item.todo_ = '' 清空,也可以不處理,反正每次 onEdit() 都會重新動態新增 todo_ property

Line 85

let onFilterAll = () => {
  filter.value = 'all'
}
  • 指定 filter state 為 all 顯示所有 todo

Line 89

let onFilterActive = () => {
  filter.value = 'active'
}
  • 指定 filter state 為 active 顯示未完成 todo

Line 93

let onFilterCompleted = () => {
  filter.value = 'completed'
}
  • 指定 filter state 為 completed 顯示已完成 todo

Line 97

let onClearCompleted = () => {
  completed.value.forEach(item => {
    let index = todos.value.findIndex(todo => todo.id === item.id)
    todos.value.splice(index, 1)
  })
}
  • 為了使用 splice() 刪除 todo state 中的 element,所以使用 forEach() 根據已完成 todo 一筆一筆尋找 index

Conclusion

  • filteredTodos computed 為 Todo List 的關鍵,只要任何 filter state 條件改變,都會 reactive 跟著變動
  • 因為 JavaScript 為動態語言,常對 Object 動態新增 property 作為臨時變數,用完可不必處理,因為每次 onEdit() 都會重新動態新增 property
  • 為了使用 splice() 刪除 Array 中的 element,必須先取得 index