1
<meta name="referrer" content="no-referrer" />

一、案例效果及思路

  • 当输入待办事项,并Enter后可增加任务到待办事项列表中。
  • 点击对号表示已完成,左下角对应显示完成数量
  • 可以删除单个任务,可以清除已完成的任务
  • 当点击左下角单选框后,表示所有任务都已经完成

组件抽离 页面实现:

抽离成输入框所在Header组件,列表List组件,单个任务Item组件,以及底部Footer组件,按公司习惯开发(index.jsx index.css)

实现思想:

把初始数据放到哪个组件?

  • List组件,那么我在Header组件中新增任务后,怎么把状态交给List呢
  • 父子组件通信可以props,兄弟组件通信还没学过
  • 所以应该把初始状态放到App组件中,这样可以实现三个组件的数据传递
  • App传递给List通过props
  • Header组件怎么传递数据给App组件? — 下面会说

首先在App组件中定义数据并传递List组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default class App extends Component {
state = {
todos: [
{ id: '001', name: '吃饭', done: 'true' },
{ id: '002', name: '睡觉', done: 'true' },
{ id: '003', name: '打代码', done: 'false' },
],
};
render() {
const { todos } = this.state;
return (
<div className="todo-container">
<div className="todo-wrap">
<Header />
<List todos={todos} />
<Footer />
</div>
</div>
);
}
}

List组件通过props接收数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class List extends Component {
render() {
const { todos } = this.props;
return (
<ul className="todo-main">
{todos.map((todo) => {
return <Item key={todo.id} {...todo} />;
})}
</ul>
);
}
}

但是每个任务的具体信息要通过Item组件显示,所以传递数据给Item

1
2
3
{todos.map((todo) => {
return <Item key={todo.id} {...todo} />;
})}

主要{…todo}是什么意思。提示:组件三大属性其中一个的知识点

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
export default class Item extends Component {
state = { mouse: false };
handleMouse = (flag) => {
return () => {
this.setState({ mouse: flag });
};
};
render() {
const { name, done } = this.props;
const { mouse } = this.state;
return (
<ul>
<li>
<label>
<input type="checkbox" defaultChecked={done} />
<span>{name}</span>
</label>
<button className="btn btn-danger">
删除
</button>
</li>
</ul>
);
}
}

defaultChecked:默认选中,可以通过点击更改选中状态(会有bug,以后改正)

这样数据的初始渲染就完成了。

新增待办事项

之前有提到过,把初始状态放到App组件中,那么Header怎么提交数据给App组件呢?

App.jsx

1
2
3
4
5
6
//新增todo事项
addTodo = (todoObj) => {
const newTodos = [todoObj, ...this.state.todos];
this.setState({ todos: newTodos });
};
<Header addTodo={this.addTodo} />

也是通过props,但是传递的是一个函数,Header通过props接收,将数据作为参数返回,就实现了传递数据到App

Header.jsx

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
add = (e) => {
//解构赋值
const { target, keyCode } = e;
//判断是否按下enter键
if (keyCode !== 13) return;
if(target.value.trim() === ''){
alert('输入不能为空')
return
}
//准备一个todo对象传给App
const todoObj = { id: nanoid(), name: target.value, done: false };
//通过props回传给App
this.props.addTodo(todoObj);
//清空输入框内容
target.value = ''
};

render() {
return (
<div className="todo-header">
<input
onKeyUp={this.add}
type="text"
placeholder="请输入你的任务名称,按回车键确认"
/>
</div>
);
}

注意这个子传父数据的实现

nanoid是一个第三方库,可以实现生成不重复唯一标识,适用于id

实现鼠标移入高亮 显示删除按钮

两个事件:

onMouseEnter:鼠标移入

onMouseLeave:鼠标移出

思路:

  • 给每个li添加事件,事件共用一回调函数,通过flag判断,true为移入,false为移出,回调再次返回函数进行修改初始状态
  • 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
state = { mouse: false };
handleMouse = (flag) => {
return () => {
this.setState({ mouse: flag });
};
};

<ul>
<li
onMouseEnter={this.handleMouse(true)}
onMouseLeave={this.handleMouse(false)}
style={{ backgroundColor: mouse ? '#ddd' : 'white' }}
>
<label>
<input type="checkbox" defaultChecked={done} />
<span>{name}</span>
</label>
<button
className="btn btn-danger"
style={{ display: mouse ? 'block' : 'none' }}
>
删除
</button>
</li>
</ul>

思考:为什么handleMouse返回函数,不能直接返回值?

点击单选框更新state(点击完成,将state对应的done改为false)

Item组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
//勾选已完成
handleCheck = (id) => {
return (e) => {
this.props.updateTodo(id, e.target.checked);
};
};

<input
type="checkbox"
这里要修改为checked 不能用defaultChecked 因为defaultChecked初始化一次,以后不能修改
checked={done}
onChange={this.handleCheck(id)}
/>

App组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//updateTodo用于更新一个todo
updateTodo = (id, done) => {
const newTodos = this.state.todos.map((todo) => {
if (todo.id === id) {
return { ...todo, done: done };
} else return todo;
});
this.setState({ todos: newTodos });
};

<List
todos={todos}
updateTodo={this.updateTodo}
deleteTodo={this.deleteTodo}
/>

对props进行限制

npm i prop-types

1
2
3
4
5
6
import PropTypes from 'prop-types';

//对props进行限制
static propTypes = {
addTodo: PropTypes.func.isRequired,
};

删除一个todo

Item组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//删除todo
handleDelete = (id) => {
this.props.deleteTodo(id);
};

<button
className="btn btn-danger"
style={{ display: mouse ? 'block' : 'none' }}
onClick={() => {
this.handleDelete(id);
}}
>
删除
</button>

App组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
//deleteTodo用于删除一个todo
deleteTodo = (id) => {
const newTodos = this.state.todos.filter((todo) => {
return todo.id !== id;
});
this.setState({ todos: newTodos });
};

<List
todos={todos}
updateTodo={this.updateTodo}
deleteTodo={this.deleteTodo}
/>

Footer组件:

1
2
3
4
5
6
7
const { todos } = this.props;
//已完成总数
const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0);
//todo总数
const total = todos.length;

<span>已完成{doneCount}</span> / 全部{total}

Footer全选后 todo为全选择 实现

1
2
3
4
5
6
7
8
9
10
11
//全选
handleCheckAll = (e) => {
this.props.checkAllTodo(e.target.checked);
};

<input
type="checkbox"
这里当todo为空时,底部已完成不应为选中,所以加个total!==0判断
checked={doneCount === total && total !== 0 ? true : false}
onChange={this.handleCheckAll}
/>

App组件

1
2
3
4
5
//全选todo为已完成
checkAllTodo = (done) => {
const newTodos = this.state.todos.map((todo) => {
return { ...todo, done };
});

Footer

1
2
3
4
5
6
7
8
//清除所有已完成
clearAll = () => {
this.props.clearAllDone();
};

<button className="btn btn-danger" onClick={this.clearAll}>
清除已完成任务
</button>

App

1
2
3
4
5
6
7
//清除所有已完成任务
clearAllDone = () => {
const newTodos = this.state.todos.filter((todo) => {
return !todo.done;
});
this.setState({ todos: newTodos });
};

二、总结

拆分组件、实现静态组件,注意:className、style的写法

动态初始化列表,如何确定将数据放在哪个组件的state中?
某个组件使用:放在其自身的state中
某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)

关于父子之间通信:
【父组件】给【子组件】传递数据:通过props传递
【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数

注意defaultChecked 和 checked的区别,类似的还有:defaultValue 和 value

状态在哪里,操作状态的方法就在哪里