看完了前面四篇文章后我们就可以尝试做一下传统的TodoList案例了。接下来的内容基于你曾经写过或者听说过TodoList这个案例,如果还没有手动实现过,建议先试试在Vue2中练练手。我认为这一个案例的经典程度不亚于原生js写轮播图。
创建静态样式、抽离组件等过程在这里就先跳过,这些内容和Vue2无甚差别,在文章的最后我将贴出全部代码。
当写到todo-item组件中根据checkbox修改待办/完成状态时,按照传统的思路,我们会在数据的拥有者组件中放置todos数据,然后创建一个函数,将函数名传递给子组件list再传递给孙子组件item,在孙子组件发生了状态切换的时候执行该函数,并传回待修改的数据id。
首先贴出Vue2版本中的实现代码进行复习。
App.vue
MyList.vue
MyItem.vue
现在问题来了,在Vue3中如何调用传递进来的函数呢?我们已经知道,由于生命周期setup的特殊性,在这个函数当中无法使用this获取到组件本身,也就无法像上图一样直接通过this.checkTodo就能调用到父组件传递进来的函数。
虽然无法通过实例本身去调用函数,但是setup提供了一个props参数,在这个参数中我们可以获取到父组件传递进来的参数,也就是说通过props.checkTodo(id)是可以通知到父组件的。
实现了基本的组件通信,我们可以考虑简化过程的问题。
在Vue2中有eventBus,Pubsub等方法可以实现兄弟、祖孙组件之间的通信,在Vue3中有一个新的内置方法provide与inject实现这样的通信。
祖先组件
孙组件
我们可以看到,当在父组件中通过provide发布一个数据后,子孙组件只要inject这个数据就能直接使用。
接下来贴出Vue3版本的TodoList案例,在这个案例中实现了增加、删除、修改功能,创建了自定义指令,复习了css选择器相关的知识点,还是比较详细的。一些思路操作写在了代码注释中,这里就不过多赘述了。
首先贴出成品图。
// main.js
// 引入的不再是Vue构造函数了,而是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
app.directive('my-focus',{
created(element,binding){
element.value = binding.value
},
mounted(element,binding){
element.focus()
},
updated(element,binding){
element.value = binding.value
}
})
// App.vue
<template>
<main>
<div class="container">
<h1>黑猫几绛TodoList !</h1>
<todo-add @addTodo="addTodo"></todo-add>
<todo-filter :handleFilter="handleFilter" :filterLabel="label"></todo-filter>
<!-- 在template中会自动解析出value值,所以传递值的时候不用.value -->
<todo-list :todos = "filterTodos"
:changeToDoStatus = "changeToDoStatus"
:editTodo = "editTodo"
:deleteTodo = "deleteTodo"
></todo-list>
</div>
</main>
</template>
<script>
import { computed, ref } from 'vue'
import TodoAdd from './components/TodoAdd.vue'
import TodoFilter from './components/TodoFilter.vue'
import TodoList from './components/TodoList.vue'
export default {
components: { TodoAdd, TodoFilter, TodoList },
name: 'App',
setup() {
const todos = ref([])
const label = ref('all')
// 面对基本数据类型的数据,需要通过.value拿到数据本身后再进行操作
const addTodo = (todo) => todos.value.push({
id: todos.value.length,
content: todo,
completed: false
})
function changeToDoStatus(id) {
// 无法直接操作ref包裹的数据
todos.value.forEach(todo => {
if(todo.id === id) todo.completed = !todo.completed
});
}
function handleFilter(choice){
label.value = choice
}
const filterTodos = computed(()=>{
switch (label.value) {
case 'done':
return todos.value.filter(todo=>todo.completed)
case 'notDone':
return todos.value.filter(todo=>!todo.completed)
default:
return todos.value
}
})
function editTodo(id,value){
todos.value.forEach(todo => {
if(todo.id === id) todo.content = value
});
}
function deleteTodo(id){
// 删除数组的第一种方法
// todos.value = todos.value.filter(todo => {
// return todo.id != id
// })
// 删除数组的第二种方法
// 第一个参数为起始,第二个为删除个数
todos.value.splice(id,1)
}
return {
todos,
changeToDoStatus,
addTodo,
handleFilter,
editTodo,
deleteTodo,
label,
filterTodos
}
},
}
</script>
<style>
*{
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
}
main{
width: 100vw;
min-height: 100vh;
display: grid;
align-items: center;
justify-items: center;
background: rgb(203,210,240);
}
.container{
width: 60%;
max-width: 400px;
box-shadow: 0 0 24px rgba(0,0,0,.15);
border-radius: 24px;
padding: 48px 28px;
background-color: rgb(245,246,252);
}
</style>
//TodoAdd.vue
<template>
<div class="input-add">
<input type="text" v-model="todoContent" @keyup.enter="emitAddTodo">
<button @click="emitAddTodo">
<i class="plus"></i>
</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'TodoAdd',
setup(props, context){
const todoContent = ref('')
function emitAddTodo() {
if(todoContent.value.trim()){
// 这一步有问题,对于基本数据类型,如果要使用它的值需要.value
// context.emit('addTodo', todoContent)
context.emit('addTodo', todoContent.value)
todoContent.value = ''
}
return
}
// 创建的数据或者是方法需要通过 return 传递出去
return {
todoContent,
emitAddTodo
}
}
}
</script>
<style>
.input-add{
position: relative;
display: flex;
align-items: center;
}
.input-add input{
padding: 16px 52px 16px 18px;
border-radius: 48px;
border: none;
outline: none;
box-shadow: 0 0 24px rgba(0,0,0,.08);
width: 100%;
font-size: 16px;
color: #626262;
}
.input-add button{
width: 46px;
height: 46px;
border-radius: 50%;
background: linear-gradient(#c0a5f3,#7f95f7);
border: none;
outline: none;
color: white;
position: absolute;
right: 0px;
cursor: pointer;
}
.input-add .plus{
display: block;
width: 100%;
height: 100%;
/* linear-gradient可以相当于加入多个bg-image */
background: linear-gradient(#fff,#fff),linear-gradient(#fff,#fff);
/* 第一个是横,第二个是竖 */
background-size: 50% 2px,2px 50%;
background-position: center;
background-repeat: no-repeat;
}
</style>
// TodoFilter.vue
<template>
<div class="filters">
<span class="filter"
:class="{'active': filterLabel === filter.value}"
v-for="filter in filters"
:key="filter.value"
@click="changeFilter(filter.value)"
>
{{filter.label}}
</span>
</div>
</template>
<script>
export default {
name: 'TodoFilter',
props:['handleFilter','filterLabel'],
setup(props){
const filters = [
{label: '全部', value: 'all'},
{label: '已完成', value: 'done'},
{label: '未完成', value: 'notDone'}
]
function changeFilter(value){
props.handleFilter(value)
}
return {
filters,
changeFilter
}
}
}
</script>
<style>
.filters{
display: flex;
margin: 24px 2px;
color: #c0c2ce;
font-size: 14px;
}
.filters .filter{
margin-right: 14px;
transition: .8s;
}
.filters .filter.active{
color:#6b729c;
transform: scale(1.2);
}
</style>
<template>
<div class="todo-list">
<todo-list-item
v-for="item in todos"
:key="item"
:todo = "item"
:changeToDoStatus = "changeToDoStatus"
:editTodo="editTodo"
:deleteTodo="deleteTodo"
></todo-list-item>
</div>
</template>
<script>
import TodoListItem from './TodoListItem.vue'
export default {
name: 'TodoList',
components: {TodoListItem},
props:['todos','changeToDoStatus','editTodo','deleteTodo'],
setup(props,context){
}
}
</script>
<style>
.todo-list{
display: grid;
row-gap: 14px;
}
</style>
//TodoListItem.vue
<template>
<div class="todo-item" :class="{done: todo.completed}">
<div class="label">
<input type="checkbox" class="checkbox"
:checked="todo.completed"
@click="handleCheck(todo.id)"
>
<span class="check-button" @click="handleCheck(todo.id)"></span>
<span v-if="!isEdit" @dblclick="isEdit = !isEdit">{{todo.content}}</span>
<input type="text"
v-if="isEdit"
v-model="editContent"
@blur="editTodoItem(todo.id)"
v-my-focus:value="todo.content"
>
<div class="delete btn" @click="deleteTodoItem(todo.id)">delete</div>
</div>
</div>
</template>
<script>
import {ref} from 'vue'
export default {
name: 'TodoListItem',
props:['todo', 'changeToDoStatus','editTodo','deleteTodo'],
setup(props, context) {
let isEdit = ref(false)
let editContent = ref('')
function handleCheck(id) {
props.changeToDoStatus(id)
}
function editTodoItem(id){
if(editContent.value.trim()){
props.editTodo(id,editContent.value)
}
isEdit.value = !isEdit.value
}
function deleteTodoItem(id){
props.deleteTodo(id)
}
return{
isEdit,
editContent,
handleCheck,
editTodoItem,
deleteTodoItem
}
}
}
</script>
<style>
.todo-item .btn{
display: none;
}
.todo-item{
background: #fff;
padding: 16px;
border-radius: 8px;
color: #626262;
transition: .8s;
}
.todo-item .label{
position: relative;
display: flex;
align-items: center;
}
.todo-item .check-button{
position: absolute;
top: 0;
}
.todo-item.done .label{
text-decoration: line-through;
}
.todo-item .check-button::before,
.todo-item .check-button::after{
content: "";
display: block;
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
}
.todo-item .check-button::before{
border: 1px solid #b382f9;
}
.todo-item .check-button::after{
transition: .4s;
background: #4b054e;
transform: translate(1px, 1px) scale(.8);
opacity: 0;
}
.todo-item .checkbox{
margin-right: 16px;
opacity: 0;
}
.todo-item input[type="checkbox"]:checked + span.check-button::after{
opacity: 1
}
.todo-item .editInput{
outline: none;
border-bottom: 1px solid #000;
}
.todo-item:hover{
background-color: rgb(216, 230, 241);
}
.todo-item:hover .delete{
position: absolute;
right: 0;
display: block;
padding: 10px 5px;
border-radius: 5px;
color: #fff;
background-color: rgb(107, 60, 138);
cursor: pointer;
}
</style>