Vue3 - 做個簡單的 TodoList
tags: Vue
category: Front-End
description: Vue3 - 做個簡單的 TodoList
created_at: 2022/07/16 22:00:00
回到 手把手開始寫 Vue3
前言
前面把一些基本功能都先帶過了,差不多該把前面提到的東西組合成一個小 APP
了。
不會做得太複雜,複雜的留到之後另一個章節(因為目前提到的功能也不多)。
成品的長相,真的很陽春。 DEMO
初始化專案
首先跟第一篇提到的一樣,弄好環境
建立專案
$ npm init vue@latest
而這次我的配置是讓他幫我設定好 ESLint
與 Prettier
- √ Add ESLint for code quality? ... No /
Yes
- √ Add Prettier for code formatting? ... No /
Yes
之後就把你的專案打開,安裝相依套件 (也可以使用 yarn
)
$ npm install
在來安裝 tailwindcss
$ npx lai-cmd init vue-tailwindcss
然後老樣子的把專案清理一下。
假設前置作業都做好了。
開始
先簡單建立一個 header
來用
src/components/TheHeader.vue
<script setup></script>
<template>
<header class="bg-indigo-600 text-white px-4 py-2 mb-4">
<h1 class="text-4xl">TodoList</h1>
</header>
</template>
再來做一個簡單的表單放在畫面中間
這時候的 src/App.vue
<script setup>
import { ref } from "vue";
import TheHeader from "./components/TheHeader.vue";
const name = ref("");
const addTodo = () => {
const val = name.value;
if (!val) return;
name.value = "";
console.log(val);
};
</script>
<template>
<TheHeader />
<main class="flex flex-col items-center">
<form @submit.prevent="addTodo">
<div class="flex items-center justify-center">
<input
id="todo-input"
class="border-b outline-none w-96 px-4 py-2"
type="text"
v-model="name"
placeholder="Please input todo name..."
/>
<button class="bg-blue-500 text-white px-4 py-2">Add</button>
</div>
</form>
</main>
</template>
會看到每次輸入完後送出表單都會在 console.log
印出輸入的值。
再來需要建立一個存代辦事項的陣列。
假設要額外建立一個 model
,存單一 todo
的資料結構
src/models/Todo.js
export default class Todo {
constructor(name, done = false) {
this.name = name;
this.done = done;
}
}
那麼現在的 App.vue
的 js
部分會變成這樣
import { reactive, ref } from "vue";
import TheHeader from "./components/TheHeader.vue";
import Todo from "./models/Todo";
const name = ref("");
const todos = reactive([]);
const addTodo = () => {
const val = name.value;
if (!val) return;
name.value = "";
todos.push(new Todo(val));
};
每當輸入完後送出一次表單,就會往 todos
裡面塞一個新的代辦事項
再來需要一個 TodoList
元件去顯示這些代辦事項
src/components/TodoList.vue
<script setup>
defineProps({
todos: {
type: Array,
default() {
return [];
},
},
});
</script>
<template>
<div class="w-96 py-2">
<div class="text-center my-4">
<button class="bg-red-500 text-white px-3 py-1">Batch delete.</button>
</div>
<ul>
<li
class="flex justify-between border-b mb-2"
v-for="todo in todos"
:key="todo.name"
>
<label class="flex-grow py-1">
<input type="checkbox" />
{{ todo.name }}
</label>
<div class="flex">
<button class="bg-green-500 text-white px-3 py-1" v-if="!todo.done">
Done
</button>
<button class="bg-yellow-500 text-white px-3 py-1" v-else>
Undone
</button>
<button class="bg-red-500 text-white px-3 py-1">x</button>
</div>
</li>
</ul>
</div>
</template>
再來先嘗試對 checkbox
綁上 v-model
,去抓出選取的代辦事項的索引值。
<script setup>
import { ref } from "vue";
// ...
const selectedIndexes = ref([]);
</script>
<!-- 略... -->
<li
class="flex justify-between border-b mb-2"
v-for="(todo, index) in todos"
:key="todo.name"
>
<label class="flex-grow py-1">
<input type="checkbox" v-model="selectedIndexes" :value="index" />
{{ todo.name }}
</label>
<!-- ...略 -->
記得要把 v-for
的索引值拿出來,還有設定一下 input
的 value
抓出索引之後,就可以開始做切換代辦事項的狀態(done
/undone
)或是刪除了。
先在 src/App.vue
寫入函數定義
<script setup>
// 略...
const switchTodoStatusByIndex = (index) => {
todos[index].done = !todos[index].done;
};
const deleteTodoByIndexes = (...indexes) => {
const newTodos = todos.filter((_, i) => !indexes.includes(i));
todos.splice(0, todos.length, ...newTodos);
};
</script>
<template>
<!-- 略... -->
<TodoList
:todos="todos"
@deleteByIndexes="deleteTodoByIndexes"
@switchStatusByIndex="switchTodoStatusByIndex"
/>
<!-- ...略 -->
</template>
從上層傳入兩個事件,讓子元件有能力可以對代辦事項做操作。
而子元件的部分主要多了這幾段
<script setup>
// 略...
const emit = defineEmits(["deleteByIndexes", "switchStatusByIndex"]);
const batchDelete = () => {
emit("deleteByIndexes", ...selectedIndexes.value);
selectedIndexes.value.length = 0;
};
const deleteByIndex = (index) => {
const idx = selectedIndexes.value.indexOf(index);
if (idx !== -1) {
selectedIndexes.value.splice(idx, 1);
}
selectedIndexes.value = selectedIndexes.value.map((idx) =>
idx > index ? idx - 1 : idx
);
emit("deleteByIndexes", index);
};
</script>
<template>
<!-- 略... -->
<button class="bg-red-500 text-white px-3 py-1" @click="batchDelete">
Batch delete.
</button>
<!-- 略... -->
<div class="flex">
<button
class="bg-green-500 text-white px-3 py-1"
v-if="!todo.done"
@click="$emit('switchStatusByIndex', index)"
>
Done
</button>
<button
class="bg-yellow-500 text-white px-3 py-1"
v-else
@click="$emit('switchStatusByIndex', index)"
>
Undone
</button>
<button
class="bg-red-500 text-white px-3 py-1"
@click="deleteByIndex(index)"
>
x
</button>
</div>
<!-- 略... -->
</template>
會看到中間那一段 v-if
、v-else
,大多地方是重複的,所以可以再換個寫法,就看個人喜歡
<button
class="text-white px-3 py-1"
:class="todo.done ? 'bg-yellow-500' : 'bg-green-500'"
@click="$emit('switchStatusByIndex', index)"
>
{{ todo.done ? "Undone" : "Done" }}
</button>
如果需要,還可以再自己包成計算屬性(computed
)去寫。
基本上到這邊簡單的 TodoList
就完成了。
再來就是一些函數的說明
switchTodoStatusByIndex()
const switchTodoStatusByIndex = (index) => {
todos[index].done = !todos[index].done;
};
這個應該是裡面最簡單的,就很單純抓出特定索引的物件,把 done
這個欄位做 反向(not
)運算。
也就是 true
變成 false
,反之亦然。
deleteTodoByIndexes()
const deleteTodoByIndexes = (...indexes) => {
const newTodos = todos.filter((_, i) => !indexes.includes(i));
todos.splice(0, todos.length, ...newTodos);
};
這邊傳入的是陣列的資料(indexes
),然後因為 todos
是一個 reactive
的常數資料,所以不能直接蓋過他,只能使用陣列的函數來影響內容,所以選擇這樣的方式覆蓋原來的資料。
整個流程大概是:
newTodos
會先使用 filter
找出哪些是留下來的代辦事項。
再來使用 splice
把陣列中所有資料刪除,並放入 newTodos
的資料。
batchDelete()
const batchDelete = () => {
emit("deleteByIndexes", ...selectedIndexes.value);
selectedIndexes.value.length = 0;
};
再這邊批量刪除反而比較簡單,所以先拿出來講,因為有 selectedIndexes
紀錄哪些 index
的資料有被選取,也就是要刪除的資料,既然剛好對上了,那就只要在呼叫完父元件的 deleteByIndexes
後,把 selectedIndexes
直接清空就好。
deleteByIndex()
const deleteByIndex = (index) => {
const idx = selectedIndexes.value.indexOf(index);
if (idx !== -1) {
selectedIndexes.value.splice(idx, 1);
}
selectedIndexes.value = selectedIndexes.value.map((idx) =>
idx > index ? idx - 1 : idx
);
emit("deleteByIndexes", index);
};
這個應該是這次相對複雜的函數了,因為在刪除單一個的時候可能有些目前是被選取的,要做額外的處理。
主要是兩種情況要處理,分別是:
- 選起來的剛好被刪掉,那要刪除這個
index
的紀錄,避免下一筆自動被選取。 - 之後索引比較大的都要往前遞移一項,避免選錯索引。
總結
以上就是這次的小練習了,等未來補上像是 Router
、漸變動畫、狀態管理...之類的再來做比較複雜的。
這篇文章的成品在 Github