拥抱Vue3 (五) 组件通信 provide与inject 附Vue3版本TodoList源代码

看完了前面四篇文章后我们就可以尝试做一下传统的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>
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值