10-父传子与todolist案例

6/3/2022

# 一,组件通信

在一个Vue项目中,组件之间的通信是非常重要的环节

# 1.1, 父子传递数据给子组件

父子组件之间通信

  • 父组件传递给子组件:通过props属性
  • 子组件传递给父组件:通过$emit触发事件

父组件传递给子组件

  • 父组件有一些数据,需要子组件来进行展示,可以通过props来完成组件之间的通信
  • Props是你可以在组件上注册一些自定义的attribute
  • 父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值

Props有两种常见的用法

  • 方式一:字符串数组,数组中的字符串就是attribute的名称
  • 方式二:对象类型,对象类型我们可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等
// ======== main.js

// 引入vue核心库
import Vue from 'vue'
// 引入App组件
import App from './App.vue'

Vue.config.productionTip = false

// 初始化项目唯一的vm实例
// 把App组件渲染到容器中
new Vue({
    render: h => h(App),
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ======== App.vue
<template>
  <div id="app">
    <h1>父组件 --- 王健林</h1>
    <hr>
    <!-- car="二八大杠" 叫自定义属性,父传子就是靠自定义属性 -->
    <!-- :money="money" 也是自定义属性,值不是字符串,值是number -->
    <!-- :changeAppMoney="changeAppMoney" 也是自定义属性  值是一个方法  -->
    <MyComponent1 
      :car="car" 
      :money="money" 
      :changeAppMoney="changeAppMoney"
      :changeAppCar="changeAppCar"
    ></MyComponent1>
    <hr>
    <MyComponent2 :msg="msg"></MyComponent2>
  </div>
</template>

<script>
import MyComponent1 from "./components/MyComponent1.vue"
import MyComponent2 from "./components/MyComponent2.vue"
export default {
  name: 'App',
  components: {
    MyComponent1,
    MyComponent2
  },
  data(){
    return{
      money:10000000,
      car:"二八大杠",
      msg:"hello vue"
    }
  },
  methods:{
    // 定义修改状态的方法
    changeAppMoney(){
      this.money = "1个小目标"
    },
    changeAppCar(){
      this.car = "鬼火"
    }
  }
}
</script>

<style lang="less">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ======== MyComponent1.vue
<template>
    <div>
        <h1>王思聪组件</h1>
        <p>收到父的money ---- {{money}}</p>
        <p>收到父的car ---- {{car}}</p>
        <!-- <button @click="changeAppMoney">修改钱</button> -->
        <button @click="updateMoney">修改钱</button>
        
        <button @click="changeAppCar">修改车</button>
    </div>
</template>

<script>

export default{
    name:"MyComponent1",
    // props代表子组件接收父组件的数据
    // props有多种写法,第一种写法,props后面写一个数组
    // props中写自定义属性的名字
    props:["car","money","changeAppMoney","changeAppCar"],
    mounted(){
        // console.log(typeof this.car);
        // console.log(typeof this.money);

        // console.log(this.changeAppMoney);
    },
    data(){ return {

    } },
    methods:{
        updateMoney(){
            // 调用父传递过来的方法
            this.changeAppMoney();
        }
    }
}

</script>

<style lang="less" scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

type的类型都可以是哪些

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
// ======== MyComponent2.vue

<template>
  <!-- 在vue2中,需要有一个唯一的根标签 -->
  <div>
    <h1>我是子组件MyComponent2</h1>
    <p>{{ msg }}</p>
  </div>
</template>

<script>
export default {
  name: "MyComponent2",
  // props:["msg"],
  // props的第二种写法:对象的写法
  props: {
    // msg后面也可以再配置一个对象
    //   msg:{
    //       type:String, // 期望父传递的是字符串类型,如果不是,会发出警告
    //       default:"haha", // 如果父没有传递msg,默认值是haha
    //   }

    // String  Number  Boolean  Array  Object  Date  Function  Symbol
    msg: {
      type: String, 
      required:true, // msg数据必须要传,如果不传就发出警告
    },
  },
  data() {
    return {};
  },
  methods: {},
};
</script>

<style lang="less" scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

Prop 的大小写命名

  • HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符
  • 这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名;

非Prop的Attribute

  • 当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的Attribute
  • 常见的包括class、style、id属性等
  • 当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中

禁用Attribute继承

  • 不希望组件的根元素继承attribute,可以在组件中设置 inheritAttrs: false
  • 禁用attribute继承的常见情况是需要将attribute应用于根元素之外的其他元素
  • 可以通过 $attrs来访问所有的 非props的attribute

# 二,TodoList

/*index.css*/
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;
}

/*header*/
.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);
}

/*main*/
.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;
}

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

/*footer*/
.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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<!-- todo.html -->
<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>React App</title>

    <link rel="stylesheet" href="index.css">
</head>

<body>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <div class="todo-header">
                    <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
                </div>
                <ul class="todo-main">
                    <li>
                        <label>
                            <input type="checkbox" />
                            <span>xxxxx</span>
                        </label>
                        <button class="btn btn-danger" style="display:none">删除</button>
                    </li>
                    <li>
                        <label>
                            <input type="checkbox" />
                            <span>yyyy</span>
                        </label>
                        <button class="btn btn-danger" style="display:none">删除</button>
                    </li>
                </ul>
                <div class="todo-footer">
                    <label>
                        <input type="checkbox" />
                    </label>
                    <span>
                        <span>已完成0</span> / 全部2
                    </span>
                    <button class="btn btn-danger">清除已完成任务</button>
                </div>
            </div>
        </div>
    </div>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// App.vue
<template>
  <div class="todo-container">
    <h4>{{todos}}</h4>
    <h1>{{isAllDone}}</h1>
    <div class="todo-wrap">
      <TodoHeader :addTodo="addTodo"></TodoHeader>
      <TodoMain :todos="todos" :updateDone="updateDone" :deleteTodo="deleteTodo"></TodoMain>
      <TodoFooter 
        :todos="todos" 
        :todoIsDone="todoIsDone" 
        :isAllDone="isAllDone"
        :updateAllDone="updateAllDone"
        :deleteDoneTodo="deleteDoneTodo"
      ></TodoFooter>
    </div>
  </div>
</template>

<script>
import TodoHeader from "./components/TodoHeader.vue";
import TodoMain from "./components/TodoMain.vue";
import TodoFooter from "./components/TodoFooter.vue";
export default {
  name: "App",
  components: {
    TodoHeader,
    TodoMain,
    TodoFooter,
  },
  data() {
    return {
      // todos:[
      //   {id:"01",title:"学习vue",done:true},
      //   {id:"02",title:"学习react",done:false},
      //   {id:"03",title:"学习小程序",done:false},
      // ]
      todos:JSON.parse(localStorage.getItem("TODO")) || []
    };
  },
  methods: {
    // 更新todo的Done
    updateDone(id,done){
      // 01 false
      // 03 true
      // console.log(id,done);
      // for循环遍历
      // for(let i=0; i<this.todos.length; i++){
      //   if(this.todos[i].id == id){
      //     this.todos[i].done = done
      //   }
      // }

      // forEach
      // this.todos.forEach(todo=>{
      //   if(todo.id == id){
      //     todo.done = done;
      //   }
      // })

      // 使用map
      // this.todos = this.todos.map(item=>{
      //   if(item.id == id){
      //     return { ...item, done}
      //   }
      //   return item;
      // })

      // 简化后的
      this.todos = this.todos.map(item=>item.id==id?{...item,done}:item)
    },
    // 删除单个todo
    deleteTodo(id){
      // this.todos = this.todos.filter(item=>{
      //   if(item.id==id){
      //     return false;
      //   }else{
      //     return true
      //   }
      // })

      this.todos = this.todos.filter(item=>item.id==id?false:true)
    },
    // 添加todo
    addTodo(todo){

      // find:如果找到了,返回对应的todo 如果没有找到,返回und
      let repeat = this.todos.find(item=>item.title === todo.title);
      console.log(repeat);

      // if(!repeat){
      //   this.todos.push(todo)
      // }else{
      //   alert(`【${todo.title}】任务已存在`)
      // }

      // !repeat && this.todos.push(todo);

      !repeat ? this.todos.push(todo) : alert(`${todo.title}】任务已存在`)
    },
    // 全选和反选
    updateAllDone(done){
      this.todos = this.todos.map(item=>({...item,done}))
    },
    // 清除已完成
    deleteDoneTodo(){
      this.todos = this.todos.filter(item=>!item.done)
    }
  },
  computed:{
    // 统计已完成的数据
    todoIsDone(){
      // 过滤出done为true的todo
      return this.todos.filter(item=>item.done)
    },
    // 统计是否全部已完成
    isAllDone(){
      return this.todos.every(item=>item.done)
    }
  },
  watch:{
    todos(){
      // 侦听todos,todos数据一旦变化,就会被侦听到
      // 需要把数据持久化存储  localStorage
      localStorage.setItem("TODO",JSON.stringify(this.todos))
    }
  }
};
</script>

<style lang="less">
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// TodoHeader.vue
<template>
  <div class="todo-header">
    <input 
      type="text" 
      placeholder="请输入你的任务名称,按回车键确认" 
      @keyup.enter="enterHandler"
    />
  </div>
</template>

<script>
export default {
  name: "TodoHeader",
  props: ["addTodo"],
  data() {
    return {};
  },
  methods: {
    enterHandler(e){
      if(e.target.value.trim() === ""){
        alert("输入内容不能为空~")
        return;
      }

      // 拼装一个todo
      let newTodo = {id:Date.now(), title:e.target.value, done:false};
      // console.log(newTodo);
      this.addTodo(newTodo)

      e.target.value = ""; // 清空输入框
    }
  },
};
</script>

<style lang="less" scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// TodoMain.vue
<template>
  <ul class="todo-main">
    <li 
      v-for="(todo,index) in todos" 
      :key="todo.id"
      @mouseenter="enterHander(index)"
      :class="{active:currentIndex === index}"
    >
      <label>
        <input type="checkbox" :checked="todo.done" @change="handler(todo,$event)" />
        <span>{{todo.title}}</span>
      </label>
      <button class="btn btn-danger" v-show="todo.done" @click="clickHandler(todo.id)">删除</button>
    </li>
  </ul>
</template>

<script>
export default {
  name: "TodoMain",
  props: ["todos","updateDone","deleteTodo"],
  data() {
    return {
      currentIndex:-1
    };
  },
  methods: {
    // 鼠标移入li
    enterHander(index){
      // console.log(index);
      this.currentIndex = index;
    },
    // 点击单选框
    handler({id},e){
      // console.log(id);
      // console.log(e.target.checked);
      this.updateDone(id,e.target.checked)
    },
    // 点击删除
    clickHandler(id){
      // 子调用父传递的方法
      this.deleteTodo(id)
    }
  },
};
</script>

<style lang="less" scoped>
.active{
  color: yellowgreen;
  background-color: #eee;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// TodoFooter.vue
<template>
  <div class="todo-footer">
    <label>
      <!-- <input type="checkbox" :checked="isAllDone" @change="changeHandler" /> -->
      <input type="checkbox" :checked="todoIsDone.length === todos.length && todos.length>0" @change="changeHandler" />
    </label>
    <span> <span>已完成{{todoIsDone.length}}</span> / 全部{{todos.length}} </span>
    <button class="btn btn-danger" @click="clickHandler">清除已完成任务</button>
  </div>
</template>

<script>
export default {
  name: "TodoFooter",
  props: ["todos","todoIsDone","isAllDone","updateAllDone","deleteDoneTodo"],
  data() {
    return {};
  },
  methods: {
    changeHandler(e){
      this.updateAllDone(e.target.checked)
    },
    // 点击清除已完成
    clickHandler(){
      this.deleteDoneTodo();
    }
  },
};
</script>

<style lang="less" scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 三,TodoMVC

# 1,分析

参考:https://todomvc.com/examples/vue/

需要准备两个状态:

  • todos:[{text:"学习vuex, So easy~", done:false}, {text:"学习React, So easy~", done:false}]
  • visibilty:,筛选的条件 :all completed active

功能

  • 显示todos数据
  • 添加一条todos数据
  • 删除todos数据
  • 修改某一项的状态:完成,没有完成之间切换
  • 批量修改状态: 全部完成,全部未完成
  • 统计没有完成的数量
  • 批量删除已经完成的
  • 三种状态的筛选
  • 编辑todos的内容
  • 本地存储

组件拆分:

静态资源如下:

# 2,创建项目,显示todo

创建项目完毕,如下:

运行之,如下:

浏览器运行之,如下:

把静态资源放到项目中,如下:

把todo.html的代码copy到App.vue组件中,如下:

浏览器访问之,如下:

在App.vue中导入样式,如下:

再次测试之,如下:

拆组件,如下:

创建组件,如下:

在App组件中导入,注册,使用之,如下:

浏览器测试之,如下:

在App组件中准备状态,如下:

有三个数据(todo),就需要循环出三个ListTodo组件,如下:

浏览器测试之,如下:

把todo传给子,如下:

子进行接收,使用之,如下:

浏览器效果如下:

到此,显示todo就OK了。

# 3,添加todo

拆组件,如下:

创建AddTodo.vue组件,如下:

在App.vue中引入,注册,并使用之,如下:

浏览器效果如下:

现在todos状态是在App组件中,需要把输入框中输入的todo添加到todos,就是子传父,先收集输入框中的数据,如下:

需要把数据传递给父,在父中定义一个方法,把方法先传给子,如下:

儿子接收之,并调用之,如下:

父得到数据,把数据放到todos中,如下:

测试之,OK,如下:

# 4,删除todo

需求:

小x在ListTodo组件中,如下:

说白了,就是点击小x,把父中的数据删除掉,子不能直接删除父中的数据,套路和之前一样,在父定义一个方法,子中调用之。在父中定义方法,传给子,子中是有完整的todo,如下:

儿子接口,并调用之,如下:

测试之,完美,如下:

# 5,切换单个todo的状态

把HelloWorld组件删除了,并把App组件中之前注释掉的li删除。

需求:

需要在定义中定义一个修改状态的方法,然后把方法传递给子,子去调用之,先在父中定义方法,如下:

子组件接收之,如下:

给子组件中的父选框绑定一个change事件,如下:

在调用父的方法时,把todo传递给了父,父就可以得到子传递的数据,数据是todo,在父中就可以修改todo的状态,如下:

浏览器测试之,如下:

到此,第5个功能,也实现了。

# 6,全选和反选

全选按钮是在App组件中,如下:

当下面每一个todo都选中了,全选按钮也需要选中,如下:

使用计算属性,如下:

浏览器中测试之,如下:

然后我们就需要实现全选和反选了,给全选输入框上绑定chagne事件,如下:

实现对应的方法,如下:

测试之,如下:

到此,就实现了全选和反选。

# 7,统计没有完成的数量

需求:

统计没有完成的任务的数量,说白了,就是数一数,todos中的任务为false的数量。使用计算属性,代码如下:

浏览器测试之,如下:

统计没有完成的数量,还有其它的写法,如下:

测试,OK。

# 8,删除已完成的todo

需求:

如果所有的todo都没有完成,清除的按钮是不能显示的,如下:

浏览器测试之,如下:

然后,给按钮绑定一个点击事件,当点击时,就需要清除已的,如下:

实现上面的方法,如下:

测试之,完美。

# 9,三种状态的切换

需求:

定义一个状态,如下:

点击下面的三个按钮,就需要改变状态:

给三个按钮绑定点击事件,如下:

实现对应的方法,如下:

浏览器测试之,看一下状态是否改变,如下:

改变对应的样式,如下:

测试之,如下:

现在页面上显示什么todo,就需要根据这个visibility来确定了,如下:

在页面上,使用filterTodos,如下:

浏览器测试之,如下:

# 10,编辑todo(有难度)

双击某个todo时,就需要实现编辑功能,如下:

给ListTodo这个组件加一个isEdit数据项,用它来表示当前是否处于编辑状态,如下:

当双击todo时,就需要改变状态,如下:

浏览器,测试之,如下:

根据isEdit状态,给li身上添加一个类,叫editing,如下:

浏览器测试之,如下:

要显示一个输入框,用于编辑,如下:

浏览器测试之,如下:

输入框,还需要有之前todo的内容,如下:

浏览器测试之,如下:

在输入框中,可以编辑内容,当按了回车,或失去焦点时,完成编辑,绑定两个事件,如下:

然后实现FinishEdit方法,如下:

浏览器测试之,如下:

你的目的是改变App组件中的状态,所以还需要把数据传递父,在父中定义方法如下:

把方法,传递给子,如下:

子接收之,如下:

子去调用之,调用时,把数据再传递给父,如下:

父可以接收到数据,如下:

浏览器,测试之,如下:

在父中实现编辑,如下 :

测试之,如下:

# 11,数据持久化

定义一个工具函数,如下:

当todos中的数据变化了,就需要存储了,设置一个监听器,如下:

浏览器测试之,如下:

当刷新时,发现没有刚才添加的数据,如下:

取数据,也需要从localStorage中取数据,如下:

浏览器测试之,如下:

到此,数据的持久化就实现了。

Last Updated: 12/25/2022, 10:02:14 PM