本人已参与[新人创作礼]活动,一起开启掘金创作之路
很多前端人都做过todoList的案例,今天用vue3重写一个todoList小项目,目的是温习一下vue3的语法,快速入门
在这个案例里,能够接触到的知识点有:
- setup语法糖的使用
- ref、computed,reactive,torefs等api的使用
- 父子之间传参
- provide、inject祖孙组件之间传参
- typescript语法的使用
项目分析
* 项目可以有一个父组件包裹三个子组件
* 组件一:头部组件,用来输入用户的计划
* 组件二:内容组件,用来展示用户的计划内容
* 组件三:底部组件,用来统计用户计划总数和完成总数,附带计划圈选和清除完成计划功能
实现思路
* 通过vue3脚手架工具迅速创建一个vue3项目
* components中生成三个子组件
* 因为三个子组件都需要操作用户制定的计划,所以很容易想到,将该数据存储在父组件中,通过props分别向子组件中传入,各自组件通过修改该数据,从而重新渲染页面
具体实现
header组件实现
* header组件内容为一个input输入框,用户输入具体的计划后,通过回车将数据添加到计划数组中
* 因此需要在父组件中创建一个数据追加函数,并传给header组件,监听用户按下回车键的时候,触发追加,然后令input中内容清除
<template>
<div class="header-wrap">
<input type="text" v-model="todoItem" placeholder="输入待完成任务,按回车键添加" @keyup.enter="add" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref ,PropType} from 'vue'
import {Todo,addToPlanFn} from "@/types/todo"
export default defineComponent({
name:'Header',
props: {
addToPlan: {
type: Function as PropType<addToPlanFn>,
required: true
}
},
setup(props) {
const todoItem = ref('')
function add(){
let value = todoItem.value
if(!value.trim()) return
const todo:Todo = {
id:Date.now(),
title:value.trim(),
isCompleted:false
}
props.addToPlan(todo)
todoItem.value = ''
}
return {
add,
todoItem
}
},
})
</script>
<style scoped>
.header-wrap input {
width: 100%;
padding: 5px;
border-radius: 5px;
outline: none;
border: 1px solid lightblue;
box-shadow: 1px 1px 1px rgba(0,0,0,.2);
}
</style>
* addToPlan为父组件传递过来的追加计划的函数,由于ts类型约束,不能简单将addToPlan归纳为Function类型,还需要更详细的描述,这里就需要给该函数定义一个接口,通过PropType泛型来描述该类型
* 通过数据绑定拿到input后的数据后,监听用户回车,执行后构建一个Todo类型的数据,包含了用户的计划和时间等信息,追加到计划集合中
- 在ts中每一种数据类型都会有检查和约束,因此在定义类和函数等类型时常需要为其定义一个接口,依次来约束和描述这种类型,在本项目中华,所有需要描述的类型定义在@/types/todo.ts文件中,后面不再重复叙述
//定义一个接口
export interface Todo {
id:number,
title:string,
isCompleted:boolean
}
export interface delTodoFn {
(index:number):void
}
export interface updateStateFn{
(todoItem:Todo,val:boolean):void
}
export interface addToPlanFn {
(todo:Todo):void
}
export interface clearCompletedFn {
():void
}
export interface clearAllFn {
(val:boolean):void
}
内容组件的实现
- 内容组件不仅用来展示数据,同时还能通过check框,来修改数据状态,表示其是否完成,同时通过删除操作,能够把数据删除
- 内容组件List中每一条数据都是一个独立的类型,因此可以再为其添加一个子组件ListItem,List获取父组件的所有用户计划集合,然后循环ListItem
<template>
<div class="todo-main">
<ul class="todo-list">
<TodoList
v-for="(items, index) in todos"
:todo-item="items"
:index="index"
:key="items.id"
/>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import TodoList from "./ListItem.vue";
export default defineComponent({
name: "List",
components: {
TodoList,
},
props: ["todos"],
setup() {
return {};
},
});
</script>
<style>
.todo-list{
width: 100%;
}
</style>
- ListItem组件负责修改其对应的数据,通过监视勾选框,修改用户计划的的完成状态,通过删除按钮,删除对用的数据,这两个操作通过inject获取最外围的父组件传入的函数来执行数据
<template>
<li class="todo-item">
<div>
<input type="checkbox" v-model="isCompleted" />
<span>{{ todoItem.title }}</span>
</div>
<button class="delBtn" @click="del(index)">删除</button>
</li>
</template>
<script lang="ts">
import { defineComponent, inject, reactive } from "vue";
import { Todo ,delTodoFn,updateStateFn} from "@/types/todo";
import { computed } from "@vue/reactivity";
export default defineComponent({
name: "ListItem",
props: {
todoItem: {
type: Object as () => Todo,
required: true,
},
index: {
type: Number,
required:true
},
},
setup(props, context) {
//从祖组件获取操作函数
const delTodo: delTodoFn |undefined = inject("delTodo");
const updateState: updateStateFn |undefined = inject("updateState");
const del = (index: number) => {
if (window.confirm("确认删除吗")) {
if (typeof delTodo == "function") {
delTodo(index);
}
}
};
const isCompleted = computed({
get() {
return props.todoItem.isCompleted;
},
set(val:boolean) {
if (typeof updateState == "function") {
updateState(props.todoItem, val);
}
},
});
return {
isCompleted,
del,
};
},
});
</script>
<style>
.todo-item {
border: 1px solid lightblue;
list-style: none;
width: 100%;
padding: 5px;
margin-left: -40px;
border-radius: 5px;
display: flex;
justify-content: space-between;
}
.delBtn {
background-color: #e95b47;
color: #fff;
border-radius: 5px;
border: none;
display: none;
}
.todo-item:hover {
color:lightcoral;
background-color: rgba(137,190,78,.3);
}
.todo-item:hover .delBtn {
display: block;
}
</style>
底部组件
- 底部组件需要统计计划总数及完成的计划数,因此需要父组件传入计划集合,同时有一个清除所有操作和清除已完成操作,需要父组件传入这两个函数
- 全选操作:当用户把所有计划都完成后,该勾选框自动勾选,表示已全选,反之,当用户勾选这个框,所有的计划的勾选框相同改变
<template>
<div class="todo-footer">
<div>
<label for="">
<input type="checkbox" v-model="isCheckAll" />
</label>
<span class="todo-tag">
<span>已完成 {{ count }}</span
>/全部 {{ todos.length }}
</span>
</div>
<button class="btn btn-danger" @click="clearCompleted">
清除已完成任务
</button>
</div>
</template>
<script lang="ts">
import { defineComponent ,computed,PropType} from "vue";
import { Todo,clearCompletedFn,clearAllFn } from "@/types/todo";
export default defineComponent({
name: "Footer",
props: {
todos: {
type: Array as PropType<Todo[]>,
required: true,
},
clearAll: {
type: Function as PropType<clearAllFn>,
required: true,
},
clearCompleted: {
type: Function as PropType<clearCompletedFn>,
reqquired: true,
},
},
setup(props, context) {
const count = computed(() => {
return props.todos.filter((todo) => todo.isCompleted).length;
});
//全选是否为true
const isCheckAll = computed({
get() {
return count.value>0 && count.value === props.todos.length
},
set(val:boolean){
props.clearAll(val)
}
})
return {
count,
isCheckAll
}
},
});
</script>
<style scoped>
.todo-footer {
display: flex;
justify-content: space-between;
}
.btn-danger {
color: #fff;
background-color: #e95b47;
border: none;
border-radius: 5px;
padding: 5px;
cursor: pointer;
}
.todo-tag {
margin-left: 15px;
}
</style>
父组件实现
- 父组件包含数据集合以及所有和数据相关的操作函数
<template>
<div class="todo-wrap">
<h2>todoList</h2>
<Header :addToPlan="addToPlan"/>
<List :todos="todos"/>
<Footer :todos="todos" :clearCompleted="clearCompleted" :clearAll="clearAll"/>
</div>
</template>
<script lang="ts">
import { defineComponent, provide, reactive, toRef, toRefs} from 'vue';
import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
import List from '@/components/List.vue'
import { addToPlanFn, Todo,delTodoFn,updateStateFn } from '@/types/todo'
export default defineComponent({
name: 'App',
components:{
Header,
Footer,
List
},
setup(props){
const state = reactive<{todos:Todo[]}>({
todos:[]
})
const addToPlan:addToPlanFn = (todo:Todo)=>{
state.todos.unshift(todo)
}
const clearCompleted =()=>{
state.todos = state.todos.filter((todo)=>{
if(todo.isCompleted){
return false
}
return true
})
}
const clearAll = (val:boolean) => {
state.todos.forEach(todo => {
todo.isCompleted = val
})
}
const delTodo = (index:number)=>{
state.todos.splice(index,1)
}
const updateState = (todo:Todo,val:boolean)=>{
todo.isCompleted = val
}
provide('delTodo',delTodo)
provide('updateState',updateState)
return {
...toRefs(state),
addToPlan,
clearCompleted,
clearAll
}
}
})
</script>
<style >
.todo-wrap {
width: 500px;
margin: 0 auto;
border: 1px solid lightblue;
border-radius: 5px;
padding: 20px;
}
.todo-wrap h2 {
text-align: center;
}
</style>