點燈坊

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

使用 FP Style 實作 Todo List

Sam Xiao's Avatar 2024-02-11

Todo List 常用來練習不同前端 Framework,直接在 Hugo 平台使用 Petite-vue 以 FP Style 實現 Todo List。

Version

Hugo 0.121.2
Petite-vue 0.41

Todo List

todolist001

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      {{ $title := "Hugo X Petite-vue Lab" }}
    <title>{{ $title }}</title>
  </head>
  <body>
    <div>
      <input type="text" v-model="states.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>
  </body>
</html>

<script type="module">
import { createApp, reactive } from './js/petite-vue.es.js'

let states = reactive({
  newTodo: '',
  todos: [],
  filter: () => true
})

let filteredTodos = () => states.todos.filter(states.filter)
let active = () => states.todos.filter(x => !x.isCompleted)
let completed = () => states.todos.filter(x => x.isCompleted)

let onAdd = () => {
  if (!states.newTodo) return

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

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

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

let onDelete = item => states.todos = states.todos.filter(x => x.id !== item.id)
let onCancel = item => item.isEdit = false
let onFilterAll = () => states.filter = () => true
let onFilterActive = () => states.filter = x => !x.isCompleted
let onFilterCompleted = () => states.filter = x => x.isCompleted
let onClearCompleted = () => states.todos = active()

createApp({
  $delimiters: ['[[', ']]'],
  states,
  active,
  completed,
  filteredTodos,
  onAdd,
  onEdit,
  onDelete,
  onSave,
  onCancel,
  onFilterAll,
  onFilterActive,
  onFilterCompleted,
  onClearCompleted
}).mount()
</script>

Line 11

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

Line 12

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

Line 15

<ul>
  <li v-for="(item, i) in filteredTodos()" :key="i">
    <span>[[ item.todo ]]</span>
  </li>
</ul>
  • v-for :列舉 filteredTodos(),由於 Petite-vue 沒支援 computed(),只能以 function 模擬 computed
  • [[ ]]:以 delimiter 方式支援 單向綁定

Line 17

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

Line 18

<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 20

<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 32

<span> [[ active().length ]] items left</span>
  • 顯示目前尚未完成的 todo 筆數

由於 active 不是 Computed,只能使用 active()

Line 33

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

Line 34

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

Line 35

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

Line 36

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

由於 completed 不是 Computed,只能使用 completed()

Line 44

let states = reactive({
  newTodo: '',
  todos: [],
  filter: () => true
})
  • 將 state 都宣告在 reactive()
  • newTodo state:儲存新輸入的 todo
  • todos state:儲存所有 todo
  • filter state:儲存 AllActiveCompleted 所需要的 filter function

Line 50

let filteredTodos = () => states.todos.filter(states.filter)
  • 模擬 filteredTodos Computed,因為 filter 是 reactive,因此 filteredTodos() 也是 reactive

Line 51

let active = () => states.todos.filter(x => !x.isCompleted)
  • 模擬 active Computed,因為 todos 是 reactive,因此 active() 也是 reactive

Line 52

let completed = () => states.todos.filter(x => x.isCompleted)
  • 模擬 completed Computed,因為 todos 是 reactive,因此 completed() 也是 reactive

Line 54

let onAdd = () => {
  if (!states.newTodo) return

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

Line 70

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 75

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

Line 80

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

Line 82

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

Line 83

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

Line 84

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

Line 85

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

Line 87

createApp({
  $delimiters: ['[[', ']]'],
  states,
  active,
  completed,
  filteredTodos,
  onAdd,
  onEdit,
  onDelete,
  onSave,
  onCancel,
  onFilterAll,
  onFilterActive,
  onFilterCompleted,
  onClearCompleted
})
  • 使用 createApp() 整合所有 state 與 function
  • $delimiters:為了避開 Go template 的 delimiter

Conclusion

  • Petite-vue 沒支援 Composition API 的 computed() 比較可惜,只能使用 Getter,但 Getter 又只能用於類似 Options API 風格寫法
  • 所幸 Petite-vue 可使用 Composition API 的 reactive(),這使得 Computed 可改用 function 模擬,並且還能有 Reactive 特性
  • Petite-vue 由於最後仍須 createApp() 將所有 state 與 function 整合,整體行數大於 Alpine,但 JavaScript 部分可類似 FP 以很多短短的 function 組合而成
  • 由於 Petite-vue 不支援 Setup Script,因此必須在 createApp() 宣告很多 function,這算 Petite-vue 比較重大的缺點