Todo List 常用來練習不同前端 Framework,直接在 Hugo 平台使用 Petite-vue 以 FP Style 實現 Todo List。
Version
Hugo 0.121.2
Petite-vue 0.41
Todo List
<!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 此時為
edit
與delete
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 此時為
save
與cancel
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:儲存新輸入的 todotodos
state:儲存所有 todofilter
state:儲存All
、Active
與Completed
所需要的 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 會改為顯示save
與cancel
- 在 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 會改為顯示edit
與delete
- 因為目前編輯的
<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 比較重大的缺點