发布于 

JSX实现todoList案例

在vue2中使用JSX实现todoList案例,通过这个案例,可以了解template的v-if、v-for、v-on、v-model、为组件添加样式、传递自定义事件及数据,在JSX中如何实现。

注意:JSX是在render函数写的,由于使用了render函数,template就不用写了,因为template和render同时存在,会以template为准。

拆分组件

Header:添加todo

List:展示全部数据的区域

Item:展示每条todo信息、修改和删除操作

Footer:统计已完成的todo、清除已完成

目录结构如下:

1
2
3
4
5
6
7
├─ src
│ ├─ App.vue
│ ├─ components
│ │ ├─ Footer.vue
│ │ ├─ Header.vue
│ │ ├─ Item.vue
│ │ ├─ List.vue

页面效果如下:

todoList

数据存储

将数据放在App.vue下

因为header为增加数据,List也要获取数据,对数据进行删改查操作,Footer也要用数据进行统计、删除,App作为它们的父组件,父子通信比非父子通信更简单、快捷

完成添加todo

涉及两个组件App和Header

在Header组件中添加todo,将todo数据整理好,携带数据并通知App(父组件)添加数据,进行父子通信,用到自定义事件

JSX:

  1. 自定义事件在JSX中的写法是这样的:vOn:getToDo={this.getToDo},vOn表示v-on,getToDo表示自定义事件名,在{}里写JS代码

  2. v-model需要写成:vModel={this.task},驼峰式写法

  3. 绑定的事件需要添加修饰符如keyup.enter,需要写成vOn:keyup_enter={this.submit}

App.vue组件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Header from "@/components/Header.vue";
export default {
name: "App",
render() {
return (
<div class="app">
<Header class="distance" vOn:getToDo={this.getToDo} />
</div>
);
},
data() {
return {
val: "value",
//数据保存在list中
todoList: [],
};
},
methods: {//只展示与Header有关的方法
//添加todo
getToDo(data) {
this.todoList.push(data);
},
},
};

Header.vue代码

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
export default {
render() {
return <div>{this.renderInput()}</div>;
},
data() {
return {
task: "",
id: 0,
};
},
methods: {
renderInput() {
const inputAttrs = {
type: "text",
placeholder: "请输入您的任务,按回车键确认",
};
return (
<input
{...{ attrs: inputAttrs }}
vModel={this.task}
vOn:keyup_enter={this.submit}
class="form-control"
/>
);
},
submit() {
//通知App组件添加todo
//整理数据,为每个todo添加唯一id
//Item组件进行删除操作,vue的虚拟Dom会复用相同的组件,会导致虚拟DOM不能正确追踪每一个Item组件(每个todo),确保每个todo项(每一个Item组件)唯一,
//需要在List组件进行v-for遍历todoList生成Item组件时,为每个Item绑定唯一值:key=id
this.$emit("getToDo", { task: this.task, id: this.id++, checked: false });
this.task = "";
},
},
};

完成todoList展示、选中、修改、删除

涉及三个组件:App、List、Item

其中删除、修改在Item组件中,需要到App组件去操作数据,而Item和App是爷孙关系,可以使用**$attr和$listeners**进行爷孙通信,v-on=”$listeners”可以让子组件继承所有我们依赖的组件的事件。

在点击修改按钮,显示输入框时,单纯使用v-show或v-if并不能使显示的输入框自动聚焦,所以我使用自定义指令和v-if组合实现显示输入框自动聚焦。

之所以不使用自定义+v-show,是因为使用自定义完成input框聚焦时,使用的inserted钩子只在节点插入时生效,v-show的效果相当于display为none或block,并不像v-if那样实现对节点的增删。

JSX:

  1. 在JSX中组件需要传递多个事件,可以使用on={{事件1,事件2...}}
  2. 父组件传递数据给子组件:todoList={this.todoList}
  3. v-on=”$listeners”,需要写成:on={this.$listeners}
  4. v-show和自定义指令v-focus,需要写成驼峰式写法:vShow、vFocus
  5. 添加多个样式:class={[cls,’btn’]},其中cls为变量传入,并且可以写JS表达式(如三元运算符)

App(爷)组件代码

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
import Header from "@/components/Header.vue";
export default {
name: "App",
render() {
return (
<div class="app">
<Header class="distance" vOn:getToDo={this.getToDo} />
<List
class="distance"
todoList={this.todoList}
on={{
checkedTodo: this.checkedTodo,
delToDo: this.delToDo,
updateTodo: this.updateTodo,
}}
/>
</div>
);
},
data() {
return {
val: "value",
//数据保存在list中
todoList: [],
};
},
methods: {//展示与Item有关的方法
//修改item的checked值
checkedTodo(id) {
this.todoList.forEach((item) => {
//将当前的todoItem的checked值修改
if (item.id == id) {
item.checked = !item.checked;
}
});
},
//删除某个todo
delToDo(id) {
//利用filter将id不符合的数据返回即可
this.todoList = this.todoList.filter((item) => item.id != id);
},
//更新某个todo
updateTodo(task, id) {
this.todoList.forEach((item) => {
if (item.id == id) {
item.task = task;
}
});
},
},
};

List(父)组件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Item from "@/components/Item.vue";
export default {
props: ["todoList"],
render() {
return (
<div>
{this.todoList.map((todoItem) => {
return (
//Item组件进行删除操作,vue的虚拟Dom会复用相同的组件,会导致虚拟DOM不能正确追踪每一个Item组件(每个todo),确保每个todo项(每一个Item组件)唯一,
//需要在List组件进行v-for遍历todoList生成Item组件时,为每个Item绑定唯一值:key=id
<Item todoItem={todoItem} on={this.$listeners} key={todoItem.id} />
);
})}
</div>
);
},
};

Item(孙)组件代码

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
export default {
props:['todoItem'],
render(){
return <div class="checkbox" vOn:mouseenter={this.showBtn} vOn:mouseleave={this.closeBtn}>
{this.renderCheckBox()}
{this.renderModifyInput()}
{this.renderText()}
{this.renderBtn('btn-info',this.showInput,'修改')}
{this.renderBtn('btn-danger',this.delItem,'删除')}
</div>
},
data(){
return {
isShowBtn:false, //控制修改、删除按钮显示与隐藏
text: this.todoItem.task, //修改task的文本
isshowInput:false //控制修改输入框显示与隐藏
}
},
methods:{
//渲染修改和删除按钮
renderBtn(cls,method,type){
return <button type="button" class={[cls,'btn']} vShow={this.isShowBtn} vOn:click={method}>{type}</button>
},
//渲染todo的文本
renderText(){
return <span vShow={!this.isshowInput}>{this.todoItem.task}</span>
},
//渲染修改框
renderModifyInput(){
if(this.isshowInput){
return <input type="text" vModel={this.text} vFocus vOn:blur={this.handleBlur} />
}
},
//渲染勾选框
renderCheckBox(){
return <label><input type="checkbox" checked={this.todoItem.checked} vOn:click={this.handleSelected} /></label>
},
//展示修改、删除按钮
showBtn(){
this.isShowBtn = true
},
//隐藏修改、删除按钮
closeBtn(){
this.isShowBtn = false
},
//checked改变通知爷组件修改checked的值
handleSelected(){
//通知App组件(爷组件),谁被勾选或取消
this.$emit('checkedTodo',this.todoItem.id)
},
//删除item
delItem(){
//通知App组件(爷组件),谁被勾选或取消
this.$emit('delToDo',this.todoItem.id)
},
//显示修改输入框
showInput(){
this.isshowInput = true
},
//失去焦点后,完成修改
handleBlur(){
//隐藏修改框
this.isshowInput = false
//判断taks是否改变,task改变才通知爷组件修改数据
if(this.text!=this.todoItem.task){
this.submit(this.text,this.todoItem.id)
}
},
//完成修改
submit(text,id){
//通知App组件(爷组件),更新todo数据
this.$emit('updateTodo',text,id)
}


},
directives:{
//自定义v-foucs,当input显示自动聚焦
focus:{
inserted(el){
el.focus()
}
}
}
};

统计todoList和清除已完成的todo

涉及两个组件:App和Footer组件

footer在清除已完成的todo和全选或取消全选时,需要通知App去修改数据,父子通信使用自定义事件

App组件代码

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
import Header from "@/components/Header.vue";
import List from "@/components/List.vue";
import Footer from "@/components/Footer.vue";
export default {
name: "App",
render() {
return (
<div class="app">
<Header class="distance" vOn:getToDo={this.getToDo} />
<List
class="distance"
todoList={this.todoList}
on={{
checkedTodo: this.checkedTodo,
delToDo: this.delToDo,
updateTodo: this.updateTodo,
}}
/>
<Footer class="distance" vOn:checkedAll={this.checkedAll} vOn:delChecked={this.delChecked} todoList={this.todoList} />
</div>
);
},
data() {
return {
val: "value",
//数据保存在list中
todoList: [],
};
},
methods: {//只展示与Footer有关的方法
//全部选中或取消选中
checkedAll(flag) {
if (flag) {
this.cancel();
} else {
this.todoList.forEach((item) => {
item.checked = true;
});
}
},
//取消全选
cancel() {
this.todoList.forEach((item) => {
item.checked = false;
});
},
//清除已完成的todo
delChecked() {
this.todoList = this.todoList.filter((item) => {
return item.checked == false;
});
},
},
};

Footer组件代码

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
export default {
props: ["todoList"],
render() {
return (
<div class="checkbox">
{this.renderCheckBox()}
{this.renderSpan()}
{this.renderBtn()}
</div>
);
},
methods: {
//渲染清除按钮
renderBtn() {
return (
<button
type="button"
class="btn btn-danger"
disabled={this.Done == 0}
vOn:click={this.handleDelChecked}
>
清除已完成按钮
</button>
);
},
//渲染统计信息文本
renderSpan() {
return (
<span>
已完成 {this.Done}/ 全部{this.total}
</span>
);
},
//渲染全选框
renderCheckBox() {
return (
<label>
<input
type="checkbox" //如果每一个todo都勾选那么也应该全选
checked={this.Done == this.todoList.length && this.todoList.length}
vOn:click={this.handleCheckedAll}
/>
</label>
);
},
handleCheckedAll() {
// console.log()
//如果Done=总数,则表明取消全选,反之全选
this.$emit("checkedAll", this.Done === this.todoList.length);
},
//删除已完成(即删除勾选的)
handleDelChecked() {
//通知App(父组件)清除已完成的todo
this.$emit("delChecked");
},
},
computed: {
//计算总数
total() {
return this.todoList.length;
},
//计算有多少已完成
Done() {
return this.todoList.reduce((prev, curr) => {
return prev + (curr.checked ? 1 : 0);
}, 0);
},
},
};