Vue-TodoList案例剖析

目录

一、TodoList案例介绍:

二、案例全部完整代码:

2.1 Mylist.vue

2.2 Myitem.vue

2.3 MyHeader.vue

2.4 MyFooter.vue

2.5 App.vue

三、配置项props:

四、部分功能分析:

4.1 list列表显示

4.2 添加功能


一、TodoList案例介绍:

        TodoList案例有点类似于我们平时使用的“备忘录”的功能。在这次案例中主要是为了体验“组件化编程的流程”,组件化编程的流程有以下三个:

  1. 拆分静态组件;组件要按照功能点拆分,命名不要与html元素冲突

  2. 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用;

  3. 实现交互:从绑定事件开始

其中实现组件化又分为了两种:

  • 一个组件在用:放在组件自身即可.

  • 一些组件在用:放在他们共同的父组件上(状态提升)

项目拆分组件及功能如图所示: 

 二、案例全部完整代码:

        下面是项目实现的完整代码,可以参考学习

2.1 Mylist.vue

        Mylist.vue是列表显示组件,它有一个子组件item,也是每个“要做的事儿~”

<template>
    <ul class="todo-main">
        <Item v-for="todoobj in todos" 
        :key="todoobj.id" 
        :todo="todoobj" 
        :checkTodo='checkTodo'
        :deleteTodo='deleteTodo'/>
    </ul>
</template>

<script>
import Item from "./Myitem.vue";

export default {
    name:"List",
    components:{Item},
    props:[
        'todos',
        'checkTodo',
        'deleteTodo'
    ]
}
</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>

2.2 Myitem.vue

        Myitem组件是每一个具体要做的事情`

<template>
  <li>
    <label>
      <!-- checked = true表示勾选   :checked="todo.done动态绑定是否勾选-->
      <!-- 可以綁定@change事件或者是@click事件 -->
      <input type="checkbox" :checked="todo.done" @change="handlecheck(todo.id)" />
      <!-- 以下代碼也可以完成功能,但是不建議使用,只是转了v-model空子 -->
      <!-- <input type="checkbox" v-model="todo.done"/> -->

      <!-- 父传子,拿到title数据 -->
      <span>{{ todo.title }}</span>
    </label>
    <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
  </li>
</template>

<script>
export default {
  name: "Item",
  data() {
    return {

    }
  },
  // 声明接收todo对象(父传子)
  props: ["todo", "checkTodo","deleteTodo"],
  methods: {
    handlecheck(id) {
      console.log(id)
      this.checkTodo(id);
    },
    handleDelete(id) {
      if (confirm("确定删除吗?")) {
        this.deleteTodo(id);
      }
    }

  },
}
</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;
}

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

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

 2.3 MyHeader.vue

         MyHeader组件是头部添加部分

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

<script>
import {nanoid} from "nanoid"
export default {
    name:"MyHeader",
    data() {
      return {
        title:'',
      }
    },methods: {
      add(){
        // 校验输入的数据不能为空
        if(!this.title.trim()) return alert("输入不能为空")
        // event.target.value获取发生事件的元素的值,这里使用的是双向数据绑定,两种都可以
        // 将用户的输出包装成todo对象   uuid用于生成全球唯一字符编码id   nanoid是在uuid的基础上做的精简
        const todoobj ={id:nanoid(),title:this.title,done:false}
        this.addTodo(todoobj)
        // 清空
        this.title=''

      }
    },props:[
      'addTodo'
    ]
}
</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.6);
  }
</style>

2.4 MyFooter.vue

        MyFooter是底部“完成件儿数和事件的总数以及清空已完成部分”这一部分的内容

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

<script>
export default {
  name: "Footer",
  data() {
    return {

    }
  },
  props: [
    'todos','checkAlltodo'
  ],
  methods: {
    checkAll(e) {
this.checkAlltodo(e.target.checked)
    }
  },
  computed: {
    total() {
      return this.todos.length
    },
    doneTotal() {
      // reduce做条件查询的 第一个参数是函数(调用的次数是数组的长度),第二个参数是初始符合条件的个数
      // pre第一次调用是0,第二次pre是第一次pre的返回值
      // current是当前todo对象
      return this.todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
    },
    isAll:{
      get(){
         return this.doneTotal === this.total && this.total > 0
      },
      set(value){
        this.checkAlltodo(value)
      }
    }
  }
}
</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>

2.5 App.vue

        App组件是前面所有组件的父组件以及“爷爷”组件。

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

<script>
import Header from './components/MyHeader.vue';
import List from "./components/Mylist.vue";
import Footer from "./components/MyFooter.vue";

export default {
  name: 'App',
  components: {
    Header,
    List,
    Footer
  },
  data() {
    return {
      todos: [
        // id:唯一标识
        // title:需要做的事
        //done:是否完成 true完成  FALSE失败
        {
          id: '001',
          title: '吃饭',
          done: true
        }, {
          id: '002',
          title: '喝酒',
          done: false
        }, {
          id: '003',
          title: '吃饭',
          done: true
        }
      ]
    }
  },
  methods: {
    // 添加
    addTodo(x) {
      console.log("我收到了数据", x);
      // 对数组进行修改
      this.todos.unshift(x)
    },
    // 勾選or或取消勾選
    checkTodo(id) {
      this.todos.forEach((todo) => {
        if (todo.id == id) todo.done = !todo.done;
        console.log(todo.done)
      })
    },
    // 删除
    deleteTodo(id) {
     this.todos= this.todos.filter((todo) => {
        return todo.id !== id
      })
    },
    // 全选、全不选
    checkAlltodo(done){
      this.todos.forEach((todo)=>{
        todo.done=done
      })
    }
  },
}
</script>

<style>
body {
  background: #fff;
}

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

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

.btn-danger:hover {
  color: #fff;
  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>

三、配置项props:

        在剖析todolist案例前呢,我们一定要先熟练地使用props(父传子传值),因为在这次案例中使用较多。

功能:让组件接收外部传过来的数据

使用 :

  (1)、父组件传递数据:

<Student name="李四" :age="11"></Student>

(2)、子组件使用props接收数据,接收的同时对数据类型进行限制+默认值的制定+必要性限制

props:
        {
            name: {
                type: String,
                required: true
            },
            age: {
                type: Number,
                default:99
            }
        }

备注:props是只读的。vue底层会检监测你对props的修改,如果修改了,就会发出警告,

    若业务需求需要修改,那么复制props的内容到data中一份,然后去修改data中的数据

四:部分功能分析:

4.1 list列表显示

        由于todos是有很多个组件进行操作,所以在定义的时候为了方便操作,我们会把它方法App.vue(公用父组件)中,由于列表的显示是在item组件中,在我们没有学会消息订阅与发布以及全局事件总线前,我们只能先将todos通过props先传递给mylist组件、然后使用v-for进行数据的遍历这样就可以得到list列表显示的效果了

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

<script>
import Item from "./Myitem.vue";

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

4.2 添加功能

        由于添加功能的时候,所处的组件关系不是父子关系,所以添加功能相对于其他的功能会复杂一些,在我们没有学过全局事件总线以及消息的订阅与发布之前,我们可以使用组件自定义事件进行myheader组件对vue.app组件 的传值(子传父),而再将拿到的数据通过父传子传给mylist子组件即可。子组件向父组件传值怎么传递呢?方法如下:

 子传父的方式:

        方式一:通过父组件给子组件擦魂帝函数类型的props实现;子给父传递函数

        方式二:通过父组件给子组件绑定一个自定义事件实现:子传父传递数据

        方式三:通过ref ,灵活性更强(方式三是方式二的另一种实现方式)

假定有两个组件,app.vue为父组件,school,student为子组件

(1)、通过父组件给子组件擦魂帝函数类型的props实现;子给父传递函数

 app组件:  <School :getSchoolName="getSchoolName"></School>
                     
                   getSchoolName(name) {
                        this.msg = name
                   }
                          
 School组件:<button @click="sendSchoolName">点我将学校名传给APP组件</button>

                    methods: {
                        sendSchoolName(){
                            this.getSchoolName(this.name)
                        }
                        },
                    props:[
                        'getSchoolName'
                        ]

(2)、通过父组件给子组件绑定一个自定义事件实现:子传父传递数据

 app组件 : <Student v-on:atguigu="getStudentName"></Student> 

                getStudentName(name) {
                    console.log("收到了"+name)
                    this.msg = name
                }
 Student组件:
                <button @click="sendStudentName">点我将学生名传给APP</button>
                methods: {
                    sendStudentName(){
                        // 触发Student组件实例身上的atguigu事件
                        this.$emit('atguigu',this.name)
                    }
                },

(3)、通过ref ,灵活性更强(方式三是方式二的另一种实现方式)

  APP组件: 
                <Student ref="student"></Student> 

                 getStudentName(name) {
                    console.log("收到了"+name)
                    this.msg = name
                 },
                
                mounted() {
                    setTimeout(() => {
                    // 绑定自定义事件
                    // this.$refs.student.$on('atguigu', this.getStudentName)
                    // 绑定自定义事件,只执行一次
                        this.$refs.student.$once('atguigu', this.getStudentName)
                    }, 5000)

                },
 Student组件:
             methods: {
                    sendStudentName(){
                        // 触发Student组件实例身上的atguigu事件
                        this.$emit('atguigu',this.name)
                    }
                },

按照这三种方式,也可以完成子组件向父组件间的传值功能,可以体会一下~后期学了“消息的订阅与发布”和“全局事件总线”后,会有更加便捷的方法~

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暇光曙墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值