前言
最近在复习Vue3的时候写了一个todoList,现在分享给大家。
最终效果如下:
功能
- 对todo项进行增删改查
- 对todo项进行勾选以切换完成状态
- 能够将待办事项临时保存,刷新后不会消失
- 检测所有待办是否已完成/待办列表是否为空,出现相对应的提示
组件设计
- 最外层容器,用于组件的包装
- 输入框组件
todoCreator
,控制待办事项的添加 - 列表项组件
todoItem
,用于展示待办事项
单个组件解析
TodoCreator
输入框组件是一个包含输入框和按钮的容器。按钮是一个用插槽编写且可复用的组件,输入框通过监听输入来展示不同的效果。下面让我们来看一下如何实现这些效果:
按钮组件TodoButton
这个组件只是简单的定义了一个按钮的样式,写这个组件的目的除了方便复用之外,也复习了一下插槽的使用。
按钮(子组件)
<template><buttonclass="w-1/6 px-4 py-2 border-none bg-slate-200 hover:bg-slate-400 hover:text-white duration-150"><slot></slot></button></template><template> <button class="w-1/6 px-4 py-2 border-none bg-slate-200 hover:bg-slate-400 hover:text-white duration-150"> <slot></slot> </button> </template><template> <button class="w-1/6 px-4 py-2 border-none bg-slate-200 hover:bg-slate-400 hover:text-white duration-150"> <slot></slot> </button> </template>
使用(父组件)
通过匿名插槽插入自定义内容
<TodoButton @click="createTodo()"><template v-slot><div>添加</div></template></TodoButton><TodoButton @click="createTodo()"> <template v-slot> <div>添加</div> </template> </TodoButton><TodoButton @click="createTodo()"> <template v-slot> <div>添加</div> </template> </TodoButton>
输入框
添加待办事项的原理是通过监听输入框输入并通过触发事件将生成的待办事项添加到存放待办事项的数组中。
输入框只需要传递待办事项的内容即可,我们可以定义一个todoState
对象存放待办事项的内容及状态等其他内容,详情如下:
- todo:待办事项的内容
- invalid:布尔值,用于判断输入合法性。
true
表示输入内容不合法,反之合法。 - errMsg:用于显示输入非法后填充的信息,可以根据不同的错误修改此项要显示的内容。
通过vue的组件通信我们知道,要实现子组件向父组件传值,需要通过给子组件绑定事件来通信,通信过程如下:
父组件
父组件通过v-on
指令绑定事件名和处理函数,将接收到的参数交由父组件中新定义的处理函数进行处理。
<TodoCreator @create-todo="createTodo" /><script>// 事件处理函数const createTodo = (todo) => {// 此时todo已经作为参数被传递到父组件...}</script><TodoCreator @create-todo="createTodo" /> <script> // 事件处理函数 const createTodo = (todo) => { // 此时todo已经作为参数被传递到父组件 ... } </script><TodoCreator @create-todo="createTodo" /> <script> // 事件处理函数 const createTodo = (todo) => { // 此时todo已经作为参数被传递到父组件 ... } </script>
子组件
子组件通过defineEmits
定义自定义事件,并将输入框读取到的值作为参数传递出去。
const emit = defineEmits(['create-todo'])const createTodo = () => {if (todoState.todo !== '') {emit('create-todo', todoState.todo)todoState.todo = ''return}// 表单验证todoState.invalid = truetodoState.errMsg = 'todo内容不能为空'}const emit = defineEmits(['create-todo']) const createTodo = () => { if (todoState.todo !== '') { emit('create-todo', todoState.todo) todoState.todo = '' return } // 表单验证 todoState.invalid = true todoState.errMsg = 'todo内容不能为空' }const emit = defineEmits(['create-todo']) const createTodo = () => { if (todoState.todo !== '') { emit('create-todo', todoState.todo) todoState.todo = '' return } // 表单验证 todoState.invalid = true todoState.errMsg = 'todo内容不能为空' }
在创建待办事项的功能中,添加了一些简单的表单检查:通过对输入框的内容的监听,控制inValid
的变化,从而判断是否显现错误提示。实现效果如下:
TodoItem
这个组件相对而言复杂一些。因为该组件实现的功能比较多,如:
- 实现待办事项的显示
todo
(红框)- 需要再
input
(编辑)和p
(显示)状态间切换
- 需要再
- 实现待办事项的修改和删除
todo-actions
(蓝框) - 实现待办事项是否已完成的高亮显示
checkbox
(绿框)
待办事项的数据结构
{id:uid(),todo, // 子组件传过来的todo值isCompleted:null, // 是否已完成isEditing:null, // 是否处于编辑状态}{ id:uid(), todo, // 子组件传过来的todo值 isCompleted:null, // 是否已完成 isEditing:null, // 是否处于编辑状态 }{ id:uid(), todo, // 子组件传过来的todo值 isCompleted:null, // 是否已完成 isEditing:null, // 是否处于编辑状态 }
注:这里使用一个叫uid
的库为单个待办事项生成一个随机id,后面在实现删除功能时需要使用。
父组件实现待办事项的添加
const todoList = ref([])// 创建待办事项const createTodo = (todo) => {todoList.value.push({id: uid(),todo,// 是否已完成isCompleted: null,// 编辑状态isEditing: null,})}const todoList = ref([]) // 创建待办事项 const createTodo = (todo) => { todoList.value.push({ id: uid(), todo, // 是否已完成 isCompleted: null, // 编辑状态 isEditing: null, }) }const todoList = ref([]) // 创建待办事项 const createTodo = (todo) => { todoList.value.push({ id: uid(), todo, // 是否已完成 isCompleted: null, // 编辑状态 isEditing: null, }) }
待办事项的展示
此时新增的待办事项列表在父组件TodoView
中,现在子组件需要读取其值以便显示在页面上,此时需要实现父子组件通信,要通过给子组件绑定数据来通信。过程如下:
父组件通过v-bind
指令传递值。
<template>...<TodoItemv-for="(todo, index) in todoList":todo="todo":index="index"/></template><template> ... <TodoItem v-for="(todo, index) in todoList" :todo="todo" :index="index" /> </template><template> ... <TodoItem v-for="(todo, index) in todoList" :todo="todo" :index="index" /> </template>
todoList
即此时父组件通过TodoCreator
组件和createTodo
函数实现的待办事项列表。
子组件通过defineProps
来接收来自父组件的值,这些值可以用来进行列表渲染或者是事件处理等。
<template><div class="todo flex-1"><inputtype="text":value="todo.todo"/><span>{{ todo.todo }}</span></div></template><script setup>const props = defineProps({todo: {type: Object,required: true,},// 这是待办事项数组的indexindex: {type: Number,required: true,},})</script><template> <div class="todo flex-1"> <input type="text" :value="todo.todo" /> <span>{{ todo.todo }}</span> </div> </template> <script setup> const props = defineProps({ todo: { type: Object, required: true, }, // 这是待办事项数组的index index: { type: Number, required: true, }, }) </script><template> <div class="todo flex-1"> <input type="text" :value="todo.todo" /> <span>{{ todo.todo }}</span> </div> </template> <script setup> const props = defineProps({ todo: { type: Object, required: true, }, // 这是待办事项数组的index index: { type: Number, required: true, }, }) </script>
待办事项的修改
实现原理
展示和修改是两种状态,通过todoList
中的数据项中的isEditing
属性来控制,值为true
是修改状态。修改状态下,todoItem
由一个input
框和一个用于保存修改的按钮组成。按钮用于触发Emit
事件向父组件传值,让父组件接收到修改后的待办事项然后保存。
具体实现
子组件
<template><div class="todo flex-1"><inputtype="text"v-if="todo.isEditing":value="todo.todo"@input="$emit('update-todo', $event.target.value, index)/><span v-else>{{ todo.todo }}</span></div><div class="todo-actions flex gap-2 transition ease-in-out duration-150"><Iconv-if="todo.isEditing"icon="ph:check-circle"class="icon check-icon"color="#41b080"width="22"@click="$emit('edit-todo', index)"/><Iconv-elseicon="ph:pencil-fill"class="icon edit-icon"color="#41b080"width="22"@click="$emit('edit-todo', index)"/></div></template><script setup>defineEmits(['edit-todo', 'update-todo'])</script><template> <div class="todo flex-1"> <input type="text" v-if="todo.isEditing" :value="todo.todo" @input="$emit('update-todo', $event.target.value, index) /> <span v-else>{{ todo.todo }}</span> </div> <div class="todo-actions flex gap-2 transition ease-in-out duration-150"> <Icon v-if="todo.isEditing" icon="ph:check-circle" class="icon check-icon" color="#41b080" width="22" @click="$emit('edit-todo', index)" /> <Icon v-else icon="ph:pencil-fill" class="icon edit-icon" color="#41b080" width="22" @click="$emit('edit-todo', index)" /> </div> </template> <script setup> defineEmits(['edit-todo', 'update-todo']) </script><template> <div class="todo flex-1"> <input type="text" v-if="todo.isEditing" :value="todo.todo" @input="$emit('update-todo', $event.target.value, index) /> <span v-else>{{ todo.todo }}</span> </div> <div class="todo-actions flex gap-2 transition ease-in-out duration-150"> <Icon v-if="todo.isEditing" icon="ph:check-circle" class="icon check-icon" color="#41b080" width="22" @click="$emit('edit-todo', index)" /> <Icon v-else icon="ph:pencil-fill" class="icon edit-icon" color="#41b080" width="22" @click="$emit('edit-todo', index)" /> </div> </template> <script setup> defineEmits(['edit-todo', 'update-todo']) </script>
icon使用的是
@iconify/vue
父组件
<template><TodoItemv-for="(todo, index) in todoList":todo="todo":index="index"@edit-todo="toggleEditTodo"@update-todo="updateTodo"/></template><script setup>// 编辑待办事项const toggleEditTodo = (todoPosition) => {todoList.value[todoPosition].isEditing =!todoList.value[todoPosition].isEditing}// 更新待办事项const updateTodo = (todoVal, todoPos) => {/*** 传递两个参数:* todoVal todo的内容* todoPos todo所在的数组元素的index*/todoList.value[todoPos].todo = todoVal}</script><template> <TodoItem v-for="(todo, index) in todoList" :todo="todo" :index="index" @edit-todo="toggleEditTodo" @update-todo="updateTodo" /> </template> <script setup> // 编辑待办事项 const toggleEditTodo = (todoPosition) => { todoList.value[todoPosition].isEditing = !todoList.value[todoPosition].isEditing } // 更新待办事项 const updateTodo = (todoVal, todoPos) => { /** * 传递两个参数: * todoVal todo的内容 * todoPos todo所在的数组元素的index */ todoList.value[todoPos].todo = todoVal } </script><template> <TodoItem v-for="(todo, index) in todoList" :todo="todo" :index="index" @edit-todo="toggleEditTodo" @update-todo="updateTodo" /> </template> <script setup> // 编辑待办事项 const toggleEditTodo = (todoPosition) => { todoList.value[todoPosition].isEditing = !todoList.value[todoPosition].isEditing } // 更新待办事项 const updateTodo = (todoVal, todoPos) => { /** * 传递两个参数: * todoVal todo的内容 * todoPos todo所在的数组元素的index */ todoList.value[todoPos].todo = todoVal } </script>
待办事项的删除
实现原理
删除功能的实现也是使用了Emit
自定义事件,但与更新待办事项功能不同的是,删除功能传递的是生成的uid,因为删除是针对某一条待办做处理,如果用index的话,在倒序或者是使用其他关键字对待办列表进行排序,这样可能会出现删除出错或者删除的待办不是同一个等错误。
具体实现
子组件
<template><div class="todo flex-1"><span>{{ todo.todo }}</span></div><div class="todo-actions flex gap-2 transition ease-in-out duration-150"><Iconicon="ph:trash"class="icon trash-icon"color="#f95e5e"width="22"@click="$emit('delete-todo', todo.id)"/></div></template><script setup>defineEmits(['delete-todo'])</script><template> <div class="todo flex-1"> <span> {{ todo.todo }} </span> </div> <div class="todo-actions flex gap-2 transition ease-in-out duration-150"> <Icon icon="ph:trash" class="icon trash-icon" color="#f95e5e" width="22" @click="$emit('delete-todo', todo.id)" /> </div> </template> <script setup> defineEmits(['delete-todo']) </script><template> <div class="todo flex-1"> <span> {{ todo.todo }} </span> </div> <div class="todo-actions flex gap-2 transition ease-in-out duration-150"> <Icon icon="ph:trash" class="icon trash-icon" color="#f95e5e" width="22" @click="$emit('delete-todo', todo.id)" /> </div> </template> <script setup> defineEmits(['delete-todo']) </script>
父组件
<template><TodoItemv-for="(todo, index) in todoList":todo="todo":index="index"@delete-todo="deleteTodo"/></template><script setup>// 删除待办事项const deleteTodo = (todoId) => {/*** todoId 数组元素的id(uid)* filter* 将符合条件(todo.id !== todoId)的元素排除保留* 不符合条件(遍历的数组元素的id等于作为参数的id)将其删除*/todoList.value = todoList.value.filter((todo) => todo.id !== todoId)}</script><template> <TodoItem v-for="(todo, index) in todoList" :todo="todo" :index="index" @delete-todo="deleteTodo" /> </template> <script setup> // 删除待办事项 const deleteTodo = (todoId) => { /** * todoId 数组元素的id(uid) * filter * 将符合条件(todo.id !== todoId)的元素排除保留 * 不符合条件(遍历的数组元素的id等于作为参数的id)将其删除 */ todoList.value = todoList.value.filter((todo) => todo.id !== todoId) } </script><template> <TodoItem v-for="(todo, index) in todoList" :todo="todo" :index="index" @delete-todo="deleteTodo" /> </template> <script setup> // 删除待办事项 const deleteTodo = (todoId) => { /** * todoId 数组元素的id(uid) * filter * 将符合条件(todo.id !== todoId)的元素排除保留 * 不符合条件(遍历的数组元素的id等于作为参数的id)将其删除 */ todoList.value = todoList.value.filter((todo) => todo.id !== todoId) } </script>
管理待办完成状态
实现原理
通过监听todoList
中的数据项中的isComplete
属性,使用Emit
事件传递该值,如果该值为true
,显示待办事项的元素会添加一个叫completed-todo
的类,给待办事项文本添加删除线,表示该待办已经完成。
具体实现
子组件
<template><inputtype="checkbox"class="appearance-none w-[20px] h-[20px] bg-white rounded-full shadow-md checked:bg-emerald-500":checked="todo.isCompleted"@input="$emit('toggle-complete', index)"/><div class="todo flex-1"><span:class="{ 'completed-todo': todo.isCompleted }">{{todo.todo}}</span></div></template><script setup>defineEmits(['toggle-todo'])</script><template> <input type="checkbox" class="appearance-none w-[20px] h-[20px] bg-white rounded-full shadow-md checked:bg-emerald-500" :checked="todo.isCompleted" @input="$emit('toggle-complete', index)" /> <div class="todo flex-1"> <span :class="{ 'completed-todo': todo.isCompleted }"> {{todo.todo}} </span> </div> </template> <script setup> defineEmits(['toggle-todo']) </script><template> <input type="checkbox" class="appearance-none w-[20px] h-[20px] bg-white rounded-full shadow-md checked:bg-emerald-500" :checked="todo.isCompleted" @input="$emit('toggle-complete', index)" /> <div class="todo flex-1"> <span :class="{ 'completed-todo': todo.isCompleted }"> {{todo.todo}} </span> </div> </template> <script setup> defineEmits(['toggle-todo']) </script>
父组件
<template><TodoItemv-for="(todo, index) in todoList":todo="todo":index="index"@toggle-complete="toggleTodoComplete"/></template><script setup>// 编辑待办事项const toggleEditTodo = (todoPosition) => {todoList.value[todoPosition].isEditing =!todoList.value[todoPosition].isEditing}</script><template> <TodoItem v-for="(todo, index) in todoList" :todo="todo" :index="index" @toggle-complete="toggleTodoComplete" /> </template> <script setup> // 编辑待办事项 const toggleEditTodo = (todoPosition) => { todoList.value[todoPosition].isEditing = !todoList.value[todoPosition].isEditing } </script><template> <TodoItem v-for="(todo, index) in todoList" :todo="todo" :index="index" @toggle-complete="toggleTodoComplete" /> </template> <script setup> // 编辑待办事项 const toggleEditTodo = (todoPosition) => { todoList.value[todoPosition].isEditing = !todoList.value[todoPosition].isEditing } </script>
其他功能
保存待办事项
实际上使用vuex
或者pinia
等状态管理工具是保存临时缓存的最佳选择,由于篇幅有限,笔者使用了localStronge
这种比较简单的方法实现功能。实现的代码如下:
const todoList = ref([])// 监听todoList的变化,在其每次变化的时候触发,执行缓存存储函数watch(todoList,() => {setTodoListLocalStorge()},{deep: true,})const fetchTodoList = () => {const saveTodoList = JSON.parse(localStorage.getItem('todoList'))if (saveTodoList) {todoList.value = saveTodoList}}// 在页面加载的时候从缓存中获取待办列表fetchTodoList()const setTodoListLocalStorge = () => {localStorage.setItem('todoList', JSON.stringify(todoList.value))}const todoList = ref([]) // 监听todoList的变化,在其每次变化的时候触发,执行缓存存储函数 watch( todoList, () => { setTodoListLocalStorge() }, { deep: true, } ) const fetchTodoList = () => { const saveTodoList = JSON.parse(localStorage.getItem('todoList')) if (saveTodoList) { todoList.value = saveTodoList } } // 在页面加载的时候从缓存中获取待办列表 fetchTodoList() const setTodoListLocalStorge = () => { localStorage.setItem('todoList', JSON.stringify(todoList.value)) }const todoList = ref([]) // 监听todoList的变化,在其每次变化的时候触发,执行缓存存储函数 watch( todoList, () => { setTodoListLocalStorge() }, { deep: true, } ) const fetchTodoList = () => { const saveTodoList = JSON.parse(localStorage.getItem('todoList')) if (saveTodoList) { todoList.value = saveTodoList } } // 在页面加载的时候从缓存中获取待办列表 fetchTodoList() const setTodoListLocalStorge = () => { localStorage.setItem('todoList', JSON.stringify(todoList.value)) }
根据待办事项状态展现不一样的页面效果
每当todoList数组或其任何元素的isCompleted属性发生更改时,在父组件定义的计算属性todosCompleted都将自动更新。它提供了一种简便的方法来检查所有待办是否已经完成,而且父组件可以在其模板或逻辑中使用此计算属性,以根据是否完成所有待办来显示或执行操作。我们要实现的效果和代码如下:
<template><p v-if="todosCompleted && todoList.length > 0" class="todos-msg"><Icon icon="noto-v1:party-popper" /><span>恭喜你已经完成所有待办!</span></p></template><script setup>// 检查所有待办事项是否已经完成const todosCompleted = computed(() => {return todoList.value.every((todo) => todo.isCompleted)})</script><template> <p v-if="todosCompleted && todoList.length > 0" class="todos-msg"> <Icon icon="noto-v1:party-popper" /> <span>恭喜你已经完成所有待办!</span> </p> </template> <script setup> // 检查所有待办事项是否已经完成 const todosCompleted = computed(() => { return todoList.value.every((todo) => todo.isCompleted) }) </script><template> <p v-if="todosCompleted && todoList.length > 0" class="todos-msg"> <Icon icon="noto-v1:party-popper" /> <span>恭喜你已经完成所有待办!</span> </p> </template> <script setup> // 检查所有待办事项是否已经完成 const todosCompleted = computed(() => { return todoList.value.every((todo) => todo.isCompleted) }) </script>
写在最后
可能讲的并不是很清楚,有问题的话欢迎交流!
完整代码可参考:github