Vue TodoList案例

界面完成

在这里插入图片描述
components 文件夹下新建 Header.vue来完成头部组件

<template>
  <div class="todo-header">
    <input type="text" placeholder="请输入任务名,按回车确认">
  </div>
</template>

<script>
export default {
  name: "Header"
}
</script>

<style scoped>
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus{
  outline: none;
  border-color: rgba(82,168,236,0.8);
  box-shadow: inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.8);
}

</style>

Footer.vue 来完成底部组件

<template>
  <div class="todo-footer">
    <label>
      <input type="checkbox">
    </label>
    <span>
      <span>已完成0</span>/全部2
    </span>
    <button class="btn btn-danger">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: "Footer"
}
</script>

<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>

List.vue 来完成中间内容组件

<template>
  <div>
    <ul class="todo-main">
      <Item v-for="todoObj in todos" :key="todoObj.id" :todo="todoObj"/>
    </ul>
  </div>
</template>

<script>
import Item from "./Item"

export default {
  name: "List",
  components: {
    Item
  },
  data(){
    return{
      todos:[
        {id:'001',title:'抽烟',done:true},
        {id:'002',title:'喝酒',done:false},
        {id:'003',title:'开车',done:true},
      ]
    }
  }
}
</script>

<style scoped>
.todo-main {
  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;
}
</style>

Item.vue 来完成每一项的组件

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

<script>
export default {
  name: "Item",
  props:['todo']
}
</script>

<style scoped>
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>

App.vue 中引入并注册组件,同时写一些公共样式

<template>
  <div class="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <Header/>
        <List/>
        <Footer/>
      </div>
    </div>
  </div>
</template>

<script>
//引入组件
import Header from "./components/Header";
import Footer from "./components/Footer";
import List from "./components/List";

export default {
  name: 'App',
  components: {
    Header,
    Footer,
    List
  }
}
</script>

<style>
body {
  background: #ffffff;
}

.btn {
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertica1-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgb(255, 255, 255, 0.2), 0 1px 2px rgb(255, 255, 255, 0.5);
  border-radius: 4px;
}

.btn-danger {
  color: #ffffff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-danger:hover {
  color: #ffffff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}

.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>

main.js

//引入Vue
import Vue from 'vue';
//引入App
import App from './App';

//关闭vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
    el: "#app",
    render: h => h(App)
})

添加功能实现

在这里插入图片描述

我们需要在 Header.vue 编写代码,首先给 input 中增加键盘抬起时的事件 add,当按下回车时获取输入内容,然后包装成一个对象,也就是 List.vue 中相同的数据类型{id:'001',title:'抽烟',done:true}

其中 id 需要是唯一标识,可以使用 npm i nanoid 来安装 nanoid,用来生成唯一 id
其中 title 就是输入的内容,通 add(e) 中的参数 e.target.value 就可以拿到
最后 done 默认 false

<template>
  <div class="todo-header">
    <input type="text" placeholder="请输入任务名,按回车确认" @keyup.enter="add">
  </div>
</template>

<script>
import {nanoid} from 'nanoid'
export default {
  name: "Header",
  methods: {
    add(e) {
      const todoObj = {id: nanoid(), title: e.target.value, done: false}
      console.log(todoObj);
    }
    //或者在input中增加双向绑定v-model="title"
    /*add(){
      console.log(this.title);
    }*/
  }
}
</script>

<style scoped>
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.8);
}

</style>

在头部输入123,然后按回车,查看打印的内容
在这里插入图片描述
好了现在需要把这个对象添加给 List.vue 中的 todos 中。我们之前学过父组件向子组件中传值,但目前没有学兄弟组件之间的传值,所以,我们可以把 List.vue 中的数据 todos 剪切走放到 App.vue 中(后边学习其他方法)

App.vue 中

......
<List :todos="todos"/>
......  
     
<script>
//引入组件
......

export default {
  name: 'App',
  components: {
    ......
  },
  data(){
    return{
      todos:[
        {id:'001',title:'抽烟',done:true},
        {id:'002',title:'喝酒',done:false},
        {id:'003',title:'开车',done:true},
      ]
    }
  }
}
</script>

然后 List.vue 中接收一下就可以了

......
<script>
import Item from "./Item"

export default {
  name: "List",
  props:["todos"],
  components: {
    Item
  }
}
</script>

接着,当 Header.vue 中组装了一个对象后,传给 App.vue,需要这样做:首先 App.vue 中需要先传一个方法 addTodo 给 Header.vue,然后 Header.vue 接收到这个方法调用即可

修改 App.vue

<Header :addTodo="addTodo"/>

<script>
......
export default {
  ......

  methods:{
    addTodo(todoObj){
      this.todos.unshift(todoObj)
    }
  }
}
</script>

修改 Header.vue

......
<script>
......
export default {
  ......
  props:["addTodo"],
  methods: {
    add(e) {
      if(!e.target.value.trim()) return alert("输入不能为空")
      const todoObj = {id: nanoid(), title: e.target.value, done: false}
      this.addTodo(todoObj)
      e.target.value = ""
    }
    //或者在input中增加双向绑定v-model="title"
    /*add(){
      console.log(this.title);
    }*/
  }
}
</script>
......

勾选

实现方法一

思路:勾选后需要改变数据中的 done 的值,任务列表 todos 在 App.vue 中,所以在 App.vue 中增加 checkTodo 方法。由于 App.vue - List.vue - Item.vue 是嵌套关系,我们需要逐层传递,因此 先把这个方法传给 List.vue

App.vue

<List :todos="todos" :checkTodo="checkTodo"/>

<script>
......
export default {
  ......
  methods:{
    ......
    //勾选或取消勾选一个todo
    checkTodo(id){
      this.todos.forEach((todo)=>{
        if(todo.id === id) todo.done = !todo.done
      })
    }
  }
}
</script>

List.vue 中接收并再传递给 Item.vue


<Item v-for="todoObj in todos" 
            :key="todoObj.id" 
            :todo="todoObj" 
            :checkTodo="checkTodo"
      />

<script>
......

export default {
  ......
  props:["todos","checkTodo"],
  ......
}
</script>
......

Item.vue 中接收调用,在 checkbox 中增加 change 事件,获取选中或取消勾选的 id


<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)">
     
<script>
export default {
  name: "Item",
  props:['todo','checkTodo'],
  methods:{
    handleCheck(id){
      this.checkTodo(id)
    }
  }
}
</script>

到此为止我们就实现了勾选改变值的功能,运行看下效果:
在这里插入图片描述

实现方法二

我们可以换另一种写法,v-model对应的 data 属性是 Boolean 时,返回的是checkboxchecked的 boolean 值,所以第一种方法中的 传方法之类的全都不写了,直接修改 Item.vue

<input type="checkbox" v-model="todo.done">

虽然同样可以实现上边的效果,但是这种方法是不建议的,因为我们之前说过,传过来的 props 是不能修改的,为什么 vue 还没报错呢?

我们看着段代码

let obj = {a:1,b:2}
obj.a = 2//第一种修改方式
obj = {x:11,b:22}//第二种修改方式

vue 能检测到的是第二种修改方式

不建议这种写法哦

删除

在这里插入图片描述
先修改 App.vue,增加一个删除方法 deleteTodo,传给 List.vue

<template>
  <div class="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <Header :addTodo="addTodo"/>
        <List :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
        <Footer/>
      </div>
    </div>
  </div>
</template>

<script>
......

export default {
......
  methods: {
    ......
    //删除todo
    deleteTodo(id) {
      //过滤不改变原数组
      /*this.todos = this.todos.filter((todo)=>{
        return todo.id !== id
      })*/
      //可以精简为
      this.todos = this.todos.filter(todo => todo.id !== id)
    }
  }
}
</script>
......

List.vue 接收,并传给 Item.vue

<template>
  <div>
    <ul class="todo-main">
      <Item ......
            :deleteTodo="deleteTodo"
      />
    </ul>
  </div>
</template>

<script>
export default {
  ......
  props:["todos","checkTodo","deleteTodo"],
  ......
}
</script>

Item.vue 接收调用,并把要删除的 id 传过去

<template>
  <li>
    <label>
      <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)">
      <span>{{todo.title}}</span>
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
  </li>
</template>

<script>
export default {
  ......
  props:['todo','checkTodo','deleteTodo'],
  methods:{
    ......
    //删除
    handleDelete(id){
      if(confirm("确认删除吗?")){
        this.deleteTodo(id)
      }
    }
  }
}
</script>

<style scoped>
......

li:hover{
  background-color: #dddddd;
}

li:hover button{
  display: block;
}
</style>

底部统计功能实现

在这里插入图片描述

底部要实现统计功能,必须知道 todos 的长度和选中个数,所以需要将 todos 传过来

App.vue 将 todos 传给 Footer.vue

<Footer :todos="todos"/>

Footer.vue 接收并计算,使用 reduce 函数计算选中的个数,关于reduce的用法

如果选中个数和数组个数相等,那么前边的 checkbox 将被选中。使用计算属性返回选中个数和数组个数是否相等即可

如果把项目全部删除,那么底部将整体不展示,可以给 Footer.vue 增加一个 v-show 即可

点击 checkbox 将把项目全部选中,可以给 checkbox 增加 change 事件来全选或全部选,所以需要在 App.vue 中增加方法

App.vue

<Footer :todos="todos" :checkAllTodo="checkAllTodo"/>
<script>
......

export default {
  ......
  methods: {
    ......
    //全选 or 取消全选
    checkAllTodo(done){
      this.todos.forEach((todo)=>{
        todo.done = done
      })
    }
  }
}
</script>

Footer.vue

<template>
  <div class="todo-footer" v-show="total">
    <label>
      <input type="checkbox" :checked="isAll" @change="checkAll">
    </label>
    <span>
      <span>已完成{{ doneTotal }}</span>/全部{{ total }}
    </span>
    <button class="btn btn-danger">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: "Footer",
  props: ["todos","checkAllTodo"],
  computed: {
    total(){
      return this.todos.length
    },
    doneTotal() {
      /*return this.todos.reduce((pre, current) => {
        return pre + (current.done ? 1 : 0)
      }, 0)*/
      //精简为
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
    },
    isAll(){
      return this.doneTotal === this.total
    }
  },
  methods:{
    checkAll(e){
      this.checkAllTodo(e.target.checked)
    }
  }
}
</script>

<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>

我们还可以优化一点,其中 Footer.vue 的 checkbox 可以使用 v-model 来双向绑定数据

<input type="checkbox" v-model="isAll">

<script>
export default {
  name: "Footer",
  props: ["todos","checkAllTodo"],
  computed: {
   ......
    isAll:{
      get(){
        return this.doneTotal === this.total
      },
      set(value){
        this.checkAllTodo(value)
      }
    }
  }
}
</script>

底部清除已完成任务

在这里插入图片描述

App.vue中增加 clearAllTodo 方法,返回未被选中的项目,然后把这个方法传给 Footer.vue

App.vue中

<Footer :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>

<script>
......
export default {
	......
  methods: {
    ......
    //清除全部选中的任务
    clearAllTodo(){
      this.todos = this.todos.filter((todo)=>{
        return !todo.done
      })
    }
  }
}
</script>

Footer.vue接收并使用

<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>

<script>
export default {
	......
  methods:{
    clearAll(){
      this.clearAllTodo()
    }
  }
}
</script>

总结

1、组件化编码流程:
(1).拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突
(2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
------①一个组件在用:放在组件自身即可
------②一些组件在用:放在他们共同的父组件上(状态提升)
(3).实现交互:从绑定事件开始
2、props适用于:
(1).父组件=>子组件通信
(2).子组件=>父组件通信(要求父先给子一个函数)
3、使用v-model时要切记:v-model绑定的值不能是 props 传过来的值,因为 props 是不可以修改的!!
4、props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错。但不推荐这样做

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值