將 Todo List 以 Component 實現,將更接近實務上的使用。
Version
Vue 3.4
Component
- 整個 Todo List 包成
<TodoList>
component <TodoList>
內再拆分成<TodoHeader>
、<TodoMain>
與<TodoFooter>
三個 component<ToDoHeader>
、<TodoMain>
與<TodoFooter>
負責發 event 給<TodoList>
,由<TodoList>
負責呼叫 API function<TodoFooter>
提供 slot 傳入items left
App.vue
<template>
<TodoList />
</template>
<script setup>
import TodoList from '@/components/TodoList.vue'
</script>
- 只引用
<TodoList>
component,不包含任何代碼
TodoList
TodoList.vue
<template>
<TodoHeader v-model="newTodo" @addTodo="onAddTodo" />
<TodoMain
:dataSrc="filteredTodos"
@completedTodo="onCompletedTodo"
@saveTodo="onSaveTodo"
@delTodo="onDelTodo"
/>
<TodoFooter
:dataSrc="todos"
@filterAll="onFilterAll"
@filterActive="onFilterActive"
@filterCompleted="onFilterCompleted"
@clearCompleted="onClearCompleted"
>items left
</TodoFooter>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import TodoHeader from '@/components/TodoHeader.vue'
import TodoMain from '@/components/TodoMain.vue'
import TodoFooter from '@/components/TodoFooter.vue'
import { getTodos, addTodo, saveTodo, delTodo } from '@/api/todos.js'
let todos = ref([])
let newTodo = ref('')
let completed = computed(() => todos.value.filter((todo) => todo.isCompleted))
let filter = ref(() => true)
let filteredTodos = computed(() => todos.value.filter(filter.value))
onMounted(async () => (todos.value = await getTodos()))
let onAddTodo = async (val) => {
if (val === '') return
await addTodo(val)
todos.value = await getTodos()
newTodo.value = ''
}
let onCompletedTodo = async (item) => {
await saveTodo(item)
}
let onSaveTodo = async (item) => {
await saveTodo(item)
}
let onDelTodo = async (item) => {
await delTodo(item)
todos.value = await getTodos()
}
let onClearCompleted = async () => {
for (let item of completed.value) {
await delTodo(item)
}
todos.value = await getTodos()
}
let onFilterAll = () => (filter.value = () => true)
let onFilterActive = () => (filter.value = (todo) => !todo.isCompleted)
let onFilterCompleted = () => (filter.value = (todo) => todo.isCompleted)
</script>
- 使用
<TodoHeader>
、<TodoMain>
與<TodoFooter>
component - 負責呼叫 API function
Line 2
<TodoHeader />
<TodoMain />
<TodoFooter />
- 使用
<TodoHeader>
、<TodoMain>
與<TodoFooter>
component
Line 2
<TodoHeader v-model="newTodo" @addTodo="onAddTodo" />
- 使用
<TodoHeader>
component v-model
:將newTodo
state 以v-model
傳入@addTodo
:從addTodo
event 取得新輸入的 todo
Line 3
<TodoMain
:dataSrc="filteredTodos"
@completedTodo="onCompletedTodo"
@saveTodo="onSaveTodo"
@delTodo="onDelTodo"
/>
- 使用
<TodoMain>
component :dataSrc
:將 filter 過的 todos 從dataSrc
prop 傳入@completedTodo
:從completeTodo
event 取得 todo 的完成狀態@saveTodo
:從saveTodo
event 取得編輯過的 todo@delTodo
:從delTodo
event 取得要刪除的 todo
Line 9
<TodoFooter
:dataSrc="todos"
@filterAll="onFilterAll"
@filterActive="onFilterActive"
@filterCompleted="onFilterCompleted"
@clearCompleted="onClearCompleted"
>items left
</TodoFooter>
- 使用
<TodoFooter>
component :dataSrc
:將完整 todos 從dataSrc
prop 傳入@filterAll
:從filterAll
event 得知All
button 被按下@filterActive
:從filterActive
event 得知Active
button 被按下@filterCompleted
:從filterCompleted
event 得知Completed
button 被按下@clearCompleted
:從clearCompleted
event 得知Clear Completed
button 被按下
Line 26
let newTodo = ref('')
let todos = ref([])
newTodo
state:儲存新輸入的 todotodos
state:儲存所有 todo
Line 28
let completed = computed(() => todos.value.filter((todo) => todo.isCompleted))
active
computed:當todo
state 改變時,active
computed 會 reactive 跟著改變,只顯示所有未完成
todo
Line 30
let filter = ref(() => true)
let filteredTodos = computed(() => todos.value.filter(filter.value))
filter
state:儲存All
、Active
與Completed
所需要的 filter functionfilteredTodos
computed:當todo
改變時,filteredTodos
computed 會 reactive 跟著改變,只顯示所有目前filter
state 的 filter function 所指定的 todo
Line 33
onMounted(async () => (todos.value = await getTodos()))
onMounted()
:呼叫getTodos()
API function 取得所有 todos
Line 35
let onAddTodo = async (val) => {
if (val === '') return
await addTodo(val)
todos.value = await getTodos()
newTodo.value = ''
}
- 按下
Add
button 時被觸發 - 呼叫
addTodo()
API function 新增一筆 todo - 新增完呼叫
getTodos()
API function 重新取得所有 todo
Line 43
let onCompletedTodo = async (item) => {
await saveTodo(item)
}
當 todo 狀態被改變時被觸發
呼叫
saveTodo()
API function 儲存目前 todo 完成狀態
Line 47
let onSaveTodo = async (item) => {
await saveTodo(item)
}
- 按下
save
button 時被觸發 - 呼叫
saveTodo()
API function 儲存 todo
Line 51
let onDelTodo = async (item) => {
await delTodo(item)
todos.value = await getTodos()
}
- 按下
delete
button 時被觸發 - 呼叫
delTodo()
API function 刪除 todo - 刪除完呼叫
getTodos()
API function 重新取得所有 todo
Line 56
let onClearCompleted = async () => {
for (let item of completed.value) {
await delTodo(item)
}
todos.value = await getTodos()
}
- 按下
Clear Completed
button 時被觸發 - 使用
for
loop 一筆一筆呼叫delTodo()
API function 刪除 todo - 刪除完呼叫
getTodos()
API function 重新取得所有 todo
Line 64
let onFilterAll = () => (filter.value = () => true)
- 按下
All
button 時被觸發 - 顯示所有 todo,直接提供 filter function
Line 65
let onFilterActive = () => (filter.value = (todo) => !todo.isCompleted)
- 按下
Active
button 時被觸發 - 顯示尚未完成 todo,直接提供 filter function
Line 66
let onFilterCompleted = () => (filter.value = (todo) => todo.isCompleted)
- 按下
Completed
button 時被觸發 - 顯示已完成 todo,直接提供 filter function
TodoHeader
TodoHeader.vue
<template>
<input type="text" v-model="newTodo" />
<button @click="onAdd">Add</button>
</template>
<script setup>
let newTodo = defineModel()
let emit = defineEmits(['addTodo'])
let onAdd = () => {
if (newTodo.value === '') return
emit('addTodo', newTodo.value)
}
</script>
Line 2
<input type="text" v-model="newTodo" />
<button @click="onAdd">Add</button>
v-model
:將輸入的新 todo 綁定到newTodo
state- 新增一筆 todo,並將
click
event 指定到onAdd()
Line 7
let newTodo = defineModel()
defineModel()
:定義newTodo
state 給v-model
使用
Line 8
let emit = defineEmits(['addTodo'])
defineEmits()
:定義addTodo
event
Line 10
let onAdd = () => {
if (newTodo.value === '') return
emit('addTodo', newTodo.value)
}
- 新增一筆 todo
- 發出
addTodo
event 將新輸入 todo 傳到外層
TodoMain
<template>
<ul>
<li v-for="item in filteredTodos" :key="item.id">
<input type="checkbox" v-model="item.isCompleted" @change="onCompleted(item)" />
<span v-if="!item.isEdit">
<span>{{ item.todo }}</span>
<button @click="onEdit(item)">edit</button>
<button @click="onDel(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>
</template>
<script setup>
import { computed } from 'vue'
let props = defineProps({ dataSrc: Object })
let emit = defineEmits(['completedTodo', 'delTodo', 'saveTodo'])
let filteredTodos = computed(() => props.dataSrc)
let onCompleted = (item) => {
emit('completedTodo', item)
}
let onEdit = (item) => {
item.isEdit = true
item.todo_ = item.todo
}
let onDel = (item) => {
emit('delTodo', item)
}
let onSave = (item) => {
item.isEdit = false
item.todo = item.todo_
emit('saveTodo', item)
}
let onCancel = (item) => {
item.isEdit = false
}
</script>
Line 2
<ul>
<li v-for="item in filteredTodos" :key="item.id">
<span>{{ item.todo }}</span>
</li>
</ul>
v-for
:列舉filteredTodos
computed
Line 4
<input type="checkbox" v-model="item.isCompleted" @change="onCompleted(item)" />
v-model
:將 checkbox 綁定到item.isCompleted
@change
:當改變 checkbox 改變時,呼叫onCompleted()
Line 5
<span v-if="!item.isEdit">
<span>{{ item.todo }}</span>
<button @click="onEdit(item)">edit</button>
<button @click="onDel(item)">delete</button>
</span>
v-if
:若是非編輯模式
,則直接顯示 todo- Button 此時為
edit
與delete
Line 10
<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 22
let props = defineProps({ dataSrc: Object })
defineProps()
:定義dataSrc
prop,將傳入 filter 過的 todos 供列舉
Line 23
let emit = defineEmits(['completedTodo', 'delTodo', 'saveTodo'])
defineEmits()
:定義completedTodo
、delTodo
與saveTodo
event
Line 25
let filteredTodos = computed(() => props.dataSrc)
filterTodos
completed:能與dataSrc
prop 連動
Line 27
let onCompleted = (item) => {
emit('completedTodo', item)
}
- 改變 todo 狀態為
完成
或未完成
時觸發 - 將修改過的 todo 透過
completedTodo
event 傳出
Line 31
let onEdit = (item) => {
item.isEdit = true
item.todo_ = item.todo
}
- 編輯一筆 todo 時觸發
- 將 item 的
isEdit
設定為true
表示為編輯模式
,此時 button 會改為顯示save
與cancel
- 在 item 下新增
todo_
property 作為臨時 state,並將目前toto
值寫入todo_
,作為綁定<input>
用
Line 36
let onDel = (item) => {
emit('delTodo', item)
}
- 刪除一筆 todo 時觸發
- 將要刪除 todo 透過
delTodo
event 傳出
Line 40
let onSave = (item) => {
item.isEdit = false
item.todo = item.todo_
emit('saveTodo', item)
}
- 儲存一筆 todo 時觸發
- 將 item 的
isEdit
設定為false
表示為非編輯模式
,此時 button 會改為顯示edit
與delete
- 因為目前編輯的
<input>
綁定到item.todo_
,將資料從item.todo_
複製到item.todo
完成儲存 - 將要儲存 todo 透過
saveTodo
event 傳出
Line 47
let onCancel = (item) => {
item.isEdit = false
}
- 取消編輯一筆 todo 時觸發
- 將 item 的
isEdit
設定為false
表示為非編輯模式
,此時 button 會改為顯示edit
與delete
TodoFooter
TodoFooter.vue
<template>
<span>{{ active.length }} <slot></slot></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>
</template>
<script setup>
import { computed } from 'vue'
let props = defineProps({ dataSrc: Array })
let emit = defineEmits(['filterAll', 'filterActive', 'filterCompleted', 'clearCompleted'])
let todos = computed(() => props.dataSrc)
let active = computed(() => todos.value.filter((todo) => !todo.isCompleted))
let completed = computed(() => todos.value.filter((todo) => todo.isCompleted))
let onFilterAll = () => emit('filterAll')
let onFilterActive = () => emit('filterActive')
let onFilterCompleted = () => emit('filterCompleted')
let onClearCompleted = () => emit('clearCompleted')
</script>
Line 2
<span>{{ active.length }} <slot></slot></span>
- 顯示目前尚未完成的 todo 筆數
Line 3
<button @click="onFilterAll">All</button>
- 顯示所有 todo,並將
click
event 指定到onFilterAll()
Line 4
<button @click="onFilterActive">Active</button>
- 顯示尚未完成 todo,並將
click
event 指定到onFilterActive()
Line 5
<button @click="onFilterCompleted">Completed</button>
- 顯示已完成 todo,並將
click
event 指定到onFilterCompleted()
Line 6
<button v-if="completed.length" @click="onClearCompleted">Clear Completed</button>
v-if
:若有已完成 todo,則顯示Clear Completed
button- 將
click
event 指定到onClearCompleted()
Line 12
let props = defineProps({ dataSrc: Array })
let emit = defineEmits(['filterAll', 'filterActive', 'filterCompleted', 'clearCompleted'])
defineProps()
:定義dataSrc
prop,將傳入所有 todos 供列舉defineEmits()
:定義filterAll
、filterActive
、filterCompleted
、clearCompleted
event
Line 15
let todos = computed(() => props.dataSrc)
todos
computed:能與dataSrc
prop 連動
Line 16
let active = computed(() => todos.value.filter((todo) => !todo.isCompleted))
active
computed:當todo
state 改變時,active
computed 會 reactive 跟著改變,只顯示所有未完成
todo
Line 17
let completed = computed(() => todos.value.filter((todo) => todo.isCompleted))
completed
computed:當todo
state 改變時,completed
computed 會 reactive 跟著改變,只顯示所有已完成
todo
Line 19
let onFilterAll = () => emit('filterAll')
- 顯示所有 todo,發出
filterAll
event 通知外部
Line 20
let onFilterActive = () => emit('filterActive')
- 顯示尚未完成 todo,發出
filterActive
event 通知外部
Line 21
let onFilterCompleted = () => emit('filterCompleted')
- 顯示已完成 todo,發出
filterCompleted
event 通知外部
Line 22
let onClearCompleted = () => emit('clearCompleted')
- 刪除所有已完成 todo,發出
clearCompleted
event 通知外部
API Function
api/todos.js
import axios from 'axios'
export let getTodos = async () => {
let url = 'http://localhost:3000/todos'
try {
let { data } = await axios.get(url)
return data
} catch (e) {
console.error(e)
}
}
export let addTodo = async (todo) => {
let url = 'http://localhost:3000/todos'
let body = {
isCompleted: false,
todo
}
try {
let { data } = await axios.post(url, body)
return data
} catch (e) {
console.error(e)
}
}
export let saveTodo = async (item) => {
let url = `http://localhost:3000/todos/${item.id}`
let body = {
isCompleted: item.isCompleted,
todo: item.todo
}
try {
let { data } = await axios.put(url, body)
return data
} catch (e) {
console.error(e)
}
}
export let delTodo = async (item) => {
let url = `http://localhost:3000/todos/${item.id}`
try {
let { data } = await axios.delete(url)
return data
} catch (e) {
console.error(e)
}
}
Line 1
import axios from 'axios'
- 引用 Axios
Line 3
export let getTodos = async () => {
let url = 'http://localhost:3000/todos'
try {
let { data } = await axios.get(url)
return data
} catch (e) {
console.error(e)
}
}
- 取得目前所有 todo
- 使用
axios.get()
呼叫 GET API axios.get()
所回傳的資料都放在data
property 下,可使用{ data }
直接 Object Destructure
Line 14
export let addTodo = async (todo) => {
let url = 'http://localhost:3000/todos'
let body = {
isCompleted: false,
todo
}
try {
let { data } = await axios.post(url, body)
return data
} catch (e) {
console.error(e)
}
}
- 新增一筆 todo
- 使用
axios.post()
呼叫 POST API
Line 30
export let saveTodo = async (item) => {
let url = `http://localhost:3000/todos/${item.id}`
let body = {
isCompleted: item.isCompleted,
todo: item.todo
}
try {
let { data } = await axios.put(url, body)
return data
} catch (e) {
console.error(e)
}
}
- 儲存一筆 todo
- 在 url 提供 id
- 使用
axios.put()
呼叫 PUT API
Line 46
export let delTodo = async (item) => {
let url = `http://localhost:3000/todos/${item.id}`
try {
let { data } = await axios.delete(url)
return data
} catch (e) {
console.error(e)
}
}
- 刪除一筆 todo
- 在 url 提供 id
- 使用
axios.delete()
呼叫 DELETE API
Conclusion
- 實務上可能不需要將 component 切的這麼細,但亦可藉此練習很多 component 技巧,如 prop、event、v-model、slot … 等使用方法