點燈坊

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

使用 FP Style 實作 Todo List

Sam Xiao's Avatar 2024-02-10

Vue 3 的 Composition API 其實可以使用更 FP Style 實作 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(() => true)
let filteredTodos = computed(() => todos.value.filter(filter.value))

let active = computed(() => todos.value.filter((todo) => !todo.isCompleted))
let completed = computed(() => todos.value.filter((todo) => todo.isCompleted))

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

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

  newTodo.value = ''
}

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

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

let onDelete = (item) => (todos.value = todos.value.filter((todo) => todo.id !== item.id))
let onCancel = (item) => (item.isEdit = false)
let onFilterAll = () => (filter.value = () => true)
let onFilterActive = () => (filter.value = (todo) => !todo.isCompleted)
let onFilterCompleted = () => (filter.value = (todo) => todo.isCompleted)
let onClearCompleted = () => (todos.value = active.value)
</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(() => true)
let filteredTodos = computed(() => todos.value.filter(filter.value))
  • filter state:儲存 AllActiveCompleted 所需要的 filter function
  • filteredTodos computed:當 todo 改變時,filteredTodos computed 會 reactive 跟著改變,只顯示所有目前 filter state 的 filter function 所指定的 todo

Line 41

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

Line 42

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

Line 44

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

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

  newTodo.value = ''
}
  • 新增一筆 todo
  • 使用 Array Spread 新增 todo array
  • id 使用 Symbol() 實現 UUID

Line 59

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 64

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

Line 69

let onDelete = (item) => (todos.value = todos.value.filter((todo) => todo.id !== item.id))
  • 刪除一筆 todo
  • 使用 filter() 取代 splice() 刪除 array 中的 element

Line 70

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

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

Line 71

let onFilterAll = () => (filter.value = () => true)
  • 顯示所有 todo,直接提供 filter function

Line 72

let onFilterActive = () => (filter.value = (todo) => !todo.isCompleted)
  • 顯示尚未完成 todo,直接提供 filter function

Line 73

let onFilterCompleted = () => (filter.value = (todo) => todo.isCompleted)
  • 顯示已完成 todo,直接提供 filter function

Line 74

let onClearCompleted = () => (todos.value = active.value)
  • 看起來好像是刪除 completed todo,其實只要將 active todo 指定給 todos state 即可

Conclusion

  • filteredTodos computed 為 Todo List 的關鍵,只要任何 filter 條件改變,都會 reactive 跟著變動,而 filter() 的關鍵在於 filter function,既然 JavaScript 支援 First Class Function,filter state 乾脆直接儲存 filter function 就好
  • 可使用 filter() 取代 splice() 刪除 Array 中的 element
  • 因為 JavaScript 為動態語言,常對 Object 動態新增 property 作為臨時變數,用完可不必處理,因為每次 onEdit() 都會重新動態新增 property
  • 實務上 Computed 最好是顆粒越小越好,如此可增加 Computed 的重複使用機會