头图

源码在文末。

前言

上篇文章写了个V利用Props进行组件之间的通信,这不立马就安排上这个案例拉丫。光学不敲等于没学哈(资深大佬除外哈)

目标就是实现如下的样子:

image-20211114224054755

能够进行增删改查,并且是在各个组件之间。

一、环境准备

针对这个页面,我们将他们划分为下面四个组件哈。其实也不是固定的,只是为了更好的展示组件之间的通信。

image-20211114224339858

项目结构:

image-20211114224932949

准备静态页面

MyTodoHeader头部组件:

<template>
  <div class="todo-header-box">
    <input type="text" placeholder="请输入你的任务名称,按回车键确认"  v-model="title"/>
  </div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
export default {
  props: {
  },
  data () {
    return {
      title: ''
    }
  },
  methods: {
  }
}
</script>
<style scoped>
.todo-header-box{
  width: 500px;
  height: 40px;
  margin-top:10px;
}
.todo-header-box input{
  width: 460px;
  height: 40px;
  margin-left: 10px;
  border: 1px solid #43B984;
  border-radius: 8px;
  padding-left: 10px;
}
:focus-visible{
outline:none;
}
</style>

MyTodoList组件,另外组件内还包含着MyTodoItem组件

<template>
<ul class="todo-main">
    <TodoItem v-for="(todo, index) in todos" :key="index" :todo="todo"/>
  </ul>
</template>
<script>
import TodoItem from './MyTodoItem'
export default {
  components: {
    TodoItem
  },
  props: {
  },
  methods: {
  }
}
</script>
<style scoped>
/*main*/
.todo-main {
  margin-top: 10px;
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}

/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}
</style>

MyTodoItem组件

<template>
  <li :style="{background: bgColor}">
    <label>
      <input type="checkbox"  :checked="todo.done"  />
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" style="display:none" v-show="isShow">删除</button>
  </li>
</template>

<script>
export default {
  props: {
    todo: Object
  },
  data () {
    return {
      bgColor: 'white',
      isShow: false
    }
  },
  methods: {
  }
}
</script>

MyTodoFooter组件

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <!--  // 第一种方式:通过dom元素来判断有没有进行勾选 不是最佳方式 -->
      <!-- <input type="checkbox" :checked="isAllCheck" @click="checkAll" /> -->
      <!-- 第二种方式: 通过绑定计算属性来进行展示 -->
      <input type="checkbox" v-model="isAllCheck" />
    </label>
    <span
      >已完成{{ doneTotal }}<span> / 全部{{ todos.length }} </span>
    </span>
    <button
      class="btn btn-danger"
    >
      清除已完成任务
    </button>
  </div>
</template>

<script>
export default {
  props: {
    todos: Array,
    clearDoneTodos: Function,
    checkAllTodos: Function
  },
  computed: {
    total () {
      return this.todos.length
    },
    doneTotal () {
      return this.todos.reduce((preTotal, todo) => preTotal + (todo.done ? 1 : 0), 0)
    },
    isAllCheck: {
      get () {
        return this.doneTotal === this.todos.length && this.doneTotal > 0
      }
    }
  },
  methods: {
  }
}
</script>
/*footer*/
<style scoped>
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}
</style>

App组件

<template>
  <div class="todo-container">
    <!-- header模块 -->
    <TodoHeader/>
    <!-- main 模块 -->
    <TodoList :todos="todos"/>
    <!-- 主要的内容模块 -->
    <TodoFooter :todos="todos"/>
    <!-- footer模块 -->
  </div>
</template>
<script>
import TodoHeader from './components/MyTodoHeader'
import TodoList from './components/MyTodoList'
import TodoFooter from './components/MyTodoFooter'

export default {
  components: {
    TodoHeader,
    TodoList,
    TodoFooter
  },
  data () {
    return {
      todos: [
        { id: '001', title: '吃饭', done: true },
        { id: '002', title: '睡觉', done: false },
        { id: '003', title: '敲代码', done: true }
      ]
    }
  },
  methods: {
  }

}
</script>
<style>
* {
  margin: 0 0;
  padding: 0 0;
}
.todo-container {
  margin: 0 auto;
  margin-top: 10px;
  width: 500px;
  height: 500px;
  background-color: #ccc;
  border: 1px solid #ddd;
  border-radius: 8px;
}
</style>

二、在头部组件中实现增加方法

首先说说我们的需求:

就是在头部组件中的输入框中进行输入,然后按下回车键就将数据增加到todos数组中,并在下面的列表中展示出来。

12313131

思路大致如下:

  1. 首先我们要明确数据我们是存储在App组件中的,那么我们真实修改的方法也应该写在App组件中。由App组件将方法传递给子组件(MyTodoHead)组件。
  2. 在子组件中通过Props进行接收
  3. 最后再在子组件的input中定义一个回车事件,触发父组件中的增加方法,进行数据的更新。

App组件中修改:

<template>
  <div class="todo-container">
    <!-- header模块
    :addTodo 定义的是子组件接收的名称
    "addTodo" 指向的是此组件中所定义的方法
-->
    <TodoHeader :addTodo="addTodo" />
    <!-- main 模块 -->
    <TodoList :todos="todos"  />
    <!-- 主要的内容模块 -->
      
    <TodoFooter :todos="todos"/>
    <!-- footer模块 -->
  </div>
</template>
<script>
import TodoHeader from './components/MyTodoHeader'
import TodoList from './components/MyTodoList'
import TodoFooter from './components/MyTodoFooter'

export default {
  components: {
    TodoHeader,
    TodoList,
    TodoFooter
  },
  data () {
    return {
      message: 'hello',
      todos: [
        { id: '001', title: '吃饭', done: true },
        { id: '002', title: '睡觉', done: false },
        { id: '003', title: '敲代码', done: true }
      ]
    }
  },
  methods: {
    // 回车增加一个todo
    addTodo (todo) {
      this.todos.unshift(todo)
    }
  }
}
</script>

我们通过:addTodo="addTodo"传递给子组件一个方法,然后在子组件中我们用props来接收。

<template>
  <div class="todo-header-box">
    <input type="text" placeholder="请输入你的任务名称,按回车键确认"  v-model="title" @keyup.enter="add"/>
  </div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
export default {
  props: {
     // 通过props来接收
    addTodo: Function
  },
  data () {
    return {
      title: ''
    }
  },
  methods: {
    add () {
      // 1. 检查输入合法性
      const title = this.title.trim()
      if (!title) {
        alert('请输入内容')
        return
      }
      const id = uuidv4()
      // 2. 根据输入生成一个todo对象
      const todo = { id, title, done: false }
      // 3. 这里的this.addTodo 调用的实际上就是执行父组件中的addTodo函数 
      //添加到todos
      this.addTodo(todo)
      // 4. 清除输入
      this.title = ''
    }
  }
}
</script>

这里我使用到了uuid生成全局唯一id。

安装方式:

npm install uuid --save
# 引用的话直接
import { v4 as uuidv4 } from 'uuid';
# 用法: 直接调用这个函数即可
uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

三、完善 MyTodoList 组件 | 根据id删除一条todo&判断是否选中

先说说需求:

  1. 鼠标经过每个todo上显示删除按钮,可以点击删除
  2. 判断是否勾选,即判断是否已完成

其实还有第三个的哈,我没写了(懒了),第三个是编辑,大家可以试一试.

如下图:

mylist

思路其实蛮简单的哈:

  1. 点击删除,只要传个id即可,虽然是祖孙组件之间传值,但其实就是将方法传了两层,借助了List组件做个中介,接收完再传递给item组件而已
  2. 判断是否已完成也一样,都是借助了list组件传递,实现祖孙组件通信.

MyTodoList组件

<template>
<ul class="todo-main">
    <TodoItem v-for="(todo, index) in todos" :key="index" :todo="todo" :deleteTodo="deleteTodo" :checkTodo="checkTodo" />
  </ul>
</template>
<script>
import TodoItem from './MyTodoItem'
export default {
  components: {
    TodoItem
  },
  props: {
    todos: Array,
    deleteTodo: Function,
    checkTodo: Function
  },
  methods: {
  }
}
</script>

MyTodoItem组件

<template>
  <li @mouseenter="handleEnter(true)" @mouseleave="handleEnter(false)" :style="{background: bgColor}">
    <label>
      <input type="checkbox"  :checked="todo.done" @click="handlerCheck(todo.id)" />
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" style="display:none" v-show="isShow" @click="handlerDeleteItem(todo.id)">删除</button>
  </li>
</template>

<script>
export default {
  props: {
    todo: Object,
    checkTodo: Function,
    deleteTodo: Function
  },
  data () {
    return {
      bgColor: 'white',
      isShow: false
    }
  },
  methods: {
    handleEnter (isEnter) {
      if (isEnter) {
        this.bgColor = '#aaa'
        this.isShow = true
      } else {
        this.bgColor = 'white'
        this.isShow = false
      }
    },
    // 修改勾选状态
    handlerCheck (id) {
      console.log(id)
      this.checkTodo(id)
    },
      // 根据id删除
    handlerDeleteItem (id) {
      if (window.confirm('确定删除吗')) {
        this.deleteTodo(id)
      }
    }
  }
}
</script>

App组件见下文哈

四、完善尾部组件 | 判断是否全部勾选及清除全部已完成任务

照常先谈谈我们的需求:

1、判断是否全部勾选,修改数据状态。

2、清除选中的任务

3、当没有任何数据时,底部栏不展示

QQ录屏20211114234307


先讲讲第一个的思路:判断有没有全选,其实就是判断todos数组的长度是否等于已经选中的数量(另外就是注意就是数组长度必须要大于零)

第二个:清除选中的任务,其实就是根据id 删除掉 App父组件中 todos中我们选中的数据。

第三个:使用v-show指令即可,直接用todos数组的长度即可,当数组长度为0时,v-show自然为”false“,反之为true

理清楚,直接看代码哈

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <!--  // 第一种方式:通过dom元素来判断有没有进行勾选 不是最佳方式 -->
      <!-- <input type="checkbox" :checked="isAllCheck" @click="checkAll" /> -->
      <!-- 第二种方式: 通过绑定计算属性来进行展示 -->
      <input type="checkbox" v-model="isAllCheck" />
    </label>
    <span
      >已完成{{ doneTotal }}<span> / 全部{{ todos.length }} </span>
    </span>
    <button
      class="btn btn-danger"
      @click="deleteDoneAll"
    >
      清除已完成任务
    </button>
  </div>
</template>

<script>
export default {
  props: {
    todos: Array,
    clearDoneTodos: Function,
    checkAllTodos: Function
  },
  computed: {
    total () {
      return this.todos.length
    },
    doneTotal () {
      return this.todos.reduce((preTotal, todo) => preTotal + (todo.done ? 1 : 0), 0)
    },
    isAllCheck: {
      get () {
        return this.doneTotal === this.todos.length && this.doneTotal > 0
      },
      // 通过计算属性来判断是否全选或全不选
      set (checked) {
        this.checkAllTodos(checked)
      }
    }
  },
  methods: {
    deleteDoneAll () {
      this.clearDoneTodos()
    }
    // 通过dom元素来判断有没有进行勾选 不是最佳方式
    // checkAll (e) {
    //   console.log(e.target.checked)
    //   this.checkAllTodos(e.target.checked)
    // }
  }
}
</script>

为什么不选择通过dom元素来判断有没有进行勾选呢?

Vue框架中并不建议我们直接操作Dom元素,更多的是希望我们通过vue框架自带的方式来实现.

App组件:

<template>
  <div class="todo-container">
    <!-- :message 对应的是子组件 prop 中接收变量的名称
         "message" 对应的父组件中data中定义的数据
       -->
    <!-- header模块 -->
    <TodoHeader :addTodo="addTodo" />
    <!-- main 模块 -->
    <TodoList :todos="todos" :deleteTodo="deleteTodo" :checkTodo="checkTodo" />
    <!-- 主要的内容模块 -->
    <TodoFooter
      :todos="todos"
      :clearDoneTodos="clearDoneTodos"
      :checkAllTodos="checkAllTodos"
    />
    <!-- footer模块 -->
  </div>
</template>
<script>
import TodoHeader from './components/MyTodoHeader'
import TodoList from './components/MyTodoList'
import TodoFooter from './components/MyTodoFooter'

export default {
  components: {
    TodoHeader,
    TodoList,
    TodoFooter
  },
  data () {
    return {
      message: 'hello',
      todos: [
        { id: '001', title: '吃饭', done: true },
        { id: '002', title: '睡觉', done: false },
        { id: '003', title: '敲代码', done: true }
      ]
    }
  },
  methods: {
    // 回车增加一个todo
    addTodo (todo) {
      this.todos.unshift(todo)
    },
    // 判断勾选不勾选
    checkTodo (id) {
      this.todos.forEach((todo) => {
        if (todo.id === id) todo.done = !todo.done
      })
    },
    // 删除一个todo
    deleteTodo (id) {
      this.todos = this.todos.filter(todo => todo.id !== id)
    },
    // 全选全不选
    checkAllTodos (done) {
      this.todos.forEach((todo) => { todo.done = done })
    },
    // 清除所有已完成的任务
    clearDoneTodos () {
      this.todos = this.todos.filter(todo => !todo.done)
    }

  }

}
</script>

五 小结

vue中组件通信的方式其实有很多种,就像我已经学过的就有props | emit | 全局事件主线 | 发布订阅模式

之后还有Vuex,另外我们还可以自己定制.还有蛮多我没学到的 捂脸

其实本质都是去做数据的共享,大都数情况都是根据实际情况来选择的,并非那一样就是最好的。

六 源码

gitee

github

后语

大家一起加油!!!如若文章中有不足之处,请大家及时指出,在此郑重感谢。

纸上得来终觉浅,绝知此事要躬行。

大家好,我是博主宁在春:[主页]https://segmentfault.com/u/wy...)

一名喜欢文艺却踏上编程这条道路的小青年。

希望:我们,待别日相见时,都已有所成。L


宁在春
164 声望17 粉丝