我们掌握了react的基本语法,我们尝试实现一个todos的案例.
素材
- 官网: https://todomvc.com
- 模板下载: https://github.com/tastejs/todomvc-app-template
- 文档可能和代码不一致,以随堂代码为准:
- 随堂代码: https://gitee.com/bufanxy/react-todo-mvc
功能实现
修改解构
- 实现新增
- 实现列表展示
- 实现是否完成状态切换
- 实现item left
- 全选/取消全选
- 注意setState异步问题
- 双击编辑
- 注意取消还原的问题
- 删除
- 状态过滤
- 清除完成
代码部分:
class Todos extends React.Component {
constructor(props){
super(props);
this.state = {
newTodoValue : '',
todoList: [],
itemLeft: 0 , // 未完成剩余数量
allCheckState: false, // 全选状态
preEditValue: '',// 备份编辑前的内容
fitlerState: 'all', //all, active, completed
}
this.handleNewTodo = this.handleNewTodo.bind(this);
this.handleNewTodoEnter = this.handleNewTodoEnter.bind(this);
this.regetItemLeft = this.regetItemLeft.bind(this);
this.handleCheckAll = this.handleCheckAll.bind(this);
this.handleClearCompleted = this.handleClearCompleted.bind(this);
}
// 处理新增表单value
handleNewTodo(e){
this.setState({
newTodoValue: e.target.value
})
}
// 处理新增事件
handleNewTodoEnter(e){
// 内容为空 不处理
if(!e.target.value) return;
if(e.keyCode === 13){
// 复制todoList临时变量
var todoList = [...this.state.todoList];
// 构建todo对象
var todo = {
id: new Date().getTime(),
isEdit: false,
text: e.target.value,
isDone: false,
}
todoList.push(todo);
//更新到todoLis
this.setState({
todoList
});
// 重新计算剩余数量
this.regetItemLeft();
// 清除新增输入框内容
this.setState({
newTodoValue: ''
})
}
// esc 取消
if(e.keyCode === 27){
this.setState({
newTodoValue: ''
})
}
}
// 处理全选
handleCheckAll(){
if(this.state.todoList.length == 0) return;
// 注意: setState是异步的,必须在第二个回调才能获取最新状态
// # https://react.docschina.org/docs/state-and-lifecycle.html
this.setState(state=>({
allCheckState: !state.allCheckState
}),()=>{
// 更改每一项
var todoList = [...this.state.todoList];
todoList.map(item=>{
item.isDone = this.state.allCheckState;
return item;
})
console.log('todoList',todoList)
//
this.setState(state=>({
todoList
}))
// // 更新剩余数量
this.regetItemLeft();
})
}
// 某一行的checkbox被修改
handleCheckOne(id,e){
var todoList = [...this.state.todoList];
// 找到目标元素下标
var index = todoList.findIndex(item=>item.id == id);
todoList[index].isDone = !todoList[index].isDone;
// 更新
this.setState({
todoList
})
// 计算剩余未完成数量
this.regetItemLeft();
}
// 编辑行数据变化
handleEditChange(id,e){
var todoList = [...this.state.todoList];
var index = todoList.findIndex(item=>item.id == id);
todoList[index].text = e.target.value;
this.setState({
todoList
})
}
//双击编辑
handleEdit(id,e){
var todoList = [...this.state.todoList];
// 排他 只能有一个处于编辑状态
var beforeEditIndex = todoList.findIndex(item=>item.isEdit);
if(beforeEditIndex>-1){
todoList[beforeEditIndex].isEdit = false;
}
// 找到目标元素下标
var index = todoList.findIndex(item=>item.id == id);
todoList[index].isEdit = true;
this.setState({
todoList
},()=>{
// 通过dom关系找到目标元素
e.target.parentNode.nextElementSibling.focus();
})
// 备份旧内容 用于esc取消还原
this.setState({
preEditValue: todoList[index].text
})
}
// 编辑行确定
handleOkOrCancel(id,e){
var todoList = [...this.state.todoList];
// 找到目标元素下标
var index = todoList.findIndex(item=>item.id == id);
// 确定是esc还是enter
if(e.keyCode === 13){
todoList[index].isEdit = false;
this.setState({
todoList,
preEditValue: ''
})
}else if(e.keyCode === 27){
// 还原
todoList[index].text = this.state.preEditValue;
todoList[index].isEdit = false;
this.setState({
todoList,
preEditValue: ''
})
}
}
// 计算剩余
regetItemLeft(){
// 如果没有数据
if(this.state.todoList.length == 0){
// 处理全选
this.setState({
allCheckState: false
})
return;
}
// 设置剩余长度
var itemLefts = this.state.todoList.filter(item=>!item.isDone);
this.setState({
itemLeft: itemLefts.length
})
// 响应全选
this.setState({
allCheckState: itemLefts.length==0
})
}
// 删除
removeItem(id){
var todoList = [...this.state.todoList];
// 找到目标元素下标
var index = todoList.findIndex(item=>item.id == id);
todoList.splice(index,1);
this.setState({
todoList
})
}
// 改变filterState
changeFilterState(state){
this.setState({
fitlerState: state
})
}
// 清除所有完成的
handleClearCompleted(){
var todoList = [...this.state.todoList];
todoList = todoList.filter(item=>!item.isDone);
this.setState({
todoList
})
}
render() {
return (
<section className="todoapp">
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
value = {this.state.newTodoValue}
onChange = {this.handleNewTodo}
onKeyUp= {this.handleNewTodoEnter}
/>
</header>
{/* This section should be hidden by default and shown when there are todos */}
<section className="main">
<input id="toggle-all" className="toggle-all" type="checkbox" checked={this.state.allCheckState} onChange={this.handleCheckAll}/>
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
{/* These are here just to show the structure of the list items */}
{/* List items should get the class `editing` when editing and `completed` when marked as completed */}
{
this.state.todoList.map(item=>{
// 待办
if(this.state.fitlerState == 'active'){
if(item.isDone) return null;
}
// 已完成
if(this.state.fitlerState == 'completed'){
if(!item.isDone) return null;
}
return(
<li className={item.isDone?'completed':''} onDoubleClick={e=>this.handleEdit(item.id,e)} key={item.id}>
<div className="view" style={{display: item.isEdit?'none':'block'}}>
<input className="toggle" type="checkbox" checked={item.isDone} onChange={e=>this.handleCheckOne(item.id,e)}/>
<label>{item.text}</label>
<button className="destroy" onClick={e=>this.removeItem(item.id)}></button>
</div>
<input className="edit" onKeyUp={e=>this.handleOkOrCancel(item.id,e)} style={{display: !item.isEdit?'none':'block'}} value={item.text} onChange={e=>this.handleEditChange(item.id,e)} />
</li>
)
})
}
</ul>
</section>
{/* This footer should be hidden by default and shown when there are todos */}
<footer className="footer">
{/* This should be `0 items left` by default */}
<span className="todo-count">
<strong>{this.state.itemLeft}</strong> item left
</span>
{/* Remove this if you don't implement routing */}
<ul className="filters">
<li onClick={e=>this.changeFilterState('all')}>
<a className={this.state.fitlerState=='all'?'selected':''} href="#/">
All
</a>
</li>
<li onClick={e=>this.changeFilterState('active')}>
<a className={this.state.fitlerState=='active'?'selected':''} href="#/active">Active</a>
</li>
<li onClick={e=>this.changeFilterState('completed')}>
<a className={this.state.fitlerState=='completed'?'selected':''} href="#/completed">Completed</a>
</li>
</ul>
{/* Hidden if no completed items are left ↓ */}
<button className="clear-completed" onClick={this.handleClearCompleted}>Clear completed</button>
</footer>
</section>
);
}
}
ReactDOM.render(<Todos/>,document.getElementById('app'));
封装组件
拆分组件
把功能拆分为 <Todos />
<TodoItem />
<FooterBar />
三个组件.
其实todos 这个例子并不适合我们这样组件化,因为组件和组件之间存在大量的数据联动,我们在处理某个组件的变量的同时需要同时考虑其他组件的变量,这个不仅增加了代码量,而且提高了代码逻辑的复杂度.那这里为什么要封装组件呢?
但通过这个案例,可充分体现react的设计思路:
- 提高代码复用性
- 体现react组件开发的优势
- 体会并理解react 单向数据流 , 自上而下 的state 思想
- 关于state:
状态提升:
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。让我们看看它是如何运作的。
任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。
// 单个todo组件
class TodoItem extends React.Component {
constructor(props){
super(props);
}
// 处理一个变化
handleCheckOne(id){
this.props.handleCheckOne(id)
}
// 双击编辑
handleEdit(id,e){
// 通过dom关系找到目标元素
var target = e.target.parentNode.nextElementSibling;
this.props.handleEdit(id,target);
}
handleEditChange(id,e){
var value = e.target.value;
this.props.handleEditInputChange(id,value);
}
handleOkOrCancel(id,e){
this.props.handleEditOkOrCancel(id,e);
}
render(){
return(
<li className={this.props.isDone?'completed':''}>
<div className="view" style={{display: this.props.isEdit?'none':'block'}}>
<input className="toggle" type="checkbox" checked={this.props.isDone} onChange={e=>this.handleCheckOne(this.props.id,e)}/>
<label onDoubleClick={e=>this.handleEdit(this.props.id,e)}>{this.props.text}</label>
<button className="destroy" onClick={e=>this.props.removeItem(this.props.id)}></button>
</div>
<input className="edit" onKeyUp={e=>this.handleOkOrCancel(this.props.id,e)} style={{display: !this.props.isEdit?'none':'block'}} value={this.props.text} onChange={e=>this.handleEditChange(this.props.id,e)} />
</li>
)
}
}
class FooterBar extends React.Component {
constructor(props){
super(props);
this.changeFilterState=this.changeFilterState.bind(this);
this.handleClearCompleted=this.handleClearCompleted.bind(this);
}
changeFilterState(state){
this.props.changeFilterState(state);
}
handleClearCompleted(){
this.props.handleClearCompleted();
}
render(){
return(
<footer className="footer">
{/* This should be `0 items left` by default */}
<span className="todo-count">
<strong>{this.props.itemLeft||0}</strong> item left
</span>
{/* Remove this if you don't implement routing */}
<ul className="filters">
<li onClick={e=>this.changeFilterState('all')}>
<a className={this.props.fitlerState=='all'?'selected':''} href="#/">
All
</a>
</li>
<li onClick={e=>this.changeFilterState('active')}>
<a className={this.props.fitlerState=='active'?'selected':''} href="#/active">Active</a>
</li>
<li onClick={e=>this.changeFilterState('completed')}>
<a className={this.props.fitlerState=='completed'?'selected':''} href="#/completed">Completed</a>
</li>
</ul>
{/* Hidden if no completed items are left ↓ */}
<button className="clear-completed" onClick={this.handleClearCompleted}>Clear completed</button>
</footer>
)
}
}
class Todos extends React.Component {
constructor(props){
super(props);
this.state = {
newTodoValue : '',
todoList: [],
itemLeft: 0 , // 未完成剩余数量
allCheckState: false, // 全选状态
preEditValue: '',// 备份编辑前的内容
fitlerState: 'all', //all, active, completed
}
this.handleNewTodo = this.handleNewTodo.bind(this);
this.handleNewTodoEnter = this.handleNewTodoEnter.bind(this);
this.handleCheckOne = this.handleCheckOne.bind(this);
this.handleCheckAll = this.handleCheckAll.bind(this);
this.handleEdit = this.handleEdit.bind(this);
this.handleEditChange = this.handleEditChange.bind(this);
this.handleOkOrCancel = this.handleOkOrCancel.bind(this);
this.removeItem = this.removeItem.bind(this);
this.changeFilterState = this.changeFilterState.bind(this);
this.handleClearCompleted = this.handleClearCompleted.bind(this);
}
// 响应新增
handleNewTodo(e){
this.setState({
newTodoValue: e.target.value
})
}
// 处理新增
handleNewTodoEnter(e){
// enter or esc
if(e.keyCode === 13){
var todo = {
id: new Date().getTime(),
text: e.target.value,
isEdit: false,
isDone: false
}
var todoList = [...this.state.todoList];
todoList.push(todo);
// 添加新增 并修改表单内容
// 因为setState是异步的
this.setState({
todoList,
newTodoValue: ''
},()=>{
this.regetItemLeft();
})
}else if(e.keyCode === 27){
this.setState({
newTodoValue: ''
})
}
}
// 当修改是否选中状态
handleCheckOne(id){
console.log('id',id)
var todoList = [...this.state.todoList];
var todo = todoList.find(item=>item.id == id);
todo.isDone = !todo.isDone;
// 修改state
this.setState({
...todoList
})
this.regetItemLeft();
}
// 全选
handleCheckAll(){
if(this.state.todoList.length == 0) return;
// 注意: setState是异步的,必须在第二个回调才能获取最新状态
// # https://react.docschina.org/docs/state-and-lifecycle.html
this.setState(state=>({
allCheckState: !state.allCheckState
}),()=>{
// 更改每一项
var todoList = [...this.state.todoList];
todoList.map(item=>{
item.isDone = this.state.allCheckState;
return item;
})
//
this.setState(state=>({
todoList
}),()=>{
// 更新剩余数量
this.regetItemLeft();
})
})
}
// 计算剩余
regetItemLeft(){
// 如果没有数据
if(this.state.todoList.length == 0){
// 处理全选
this.setState({
allCheckState: false
})
return;
}
// 设置剩余长度
var itemLefts = this.state.todoList.filter(item=>!item.isDone);
this.setState({
itemLeft: itemLefts.length
})
console.log(itemLefts.length)
// 响应全选
this.setState({
allCheckState: itemLefts.length==0
})
}
//双击编辑
handleEdit(id,target){
var todoList = [...this.state.todoList];
// 排他 只能有一个处于编辑状态
var beforeEditIndex = todoList.findIndex(item=>item.isEdit);
if(beforeEditIndex>-1){
todoList[beforeEditIndex].isEdit = false;
}
// 找到目标元素下标
var index = todoList.findIndex(item=>item.id == id);
todoList[index].isEdit = true;
this.setState({
todoList
},()=>{
target.focus();
})
// 备份旧内容 用于esc取消还原
this.setState({
preEditValue: todoList[index].text
})
}
// 编辑行数据变化
handleEditChange(id,value){
var todoList = [...this.state.todoList];
var index = todoList.findIndex(item=>item.id == id);
todoList[index].text = value;
this.setState({
todoList
})
}
// 编辑行确定
handleOkOrCancel(id,e){
var todoList = [...this.state.todoList];
// 找到目标元素下标
var index = todoList.findIndex(item=>item.id == id);
// 确定是esc还是enter
if(e.keyCode === 13){
todoList[index].isEdit = false;
this.setState({
todoList,
preEditValue: ''
})
}else if(e.keyCode === 27){
// 还原
todoList[index].text = this.state.preEditValue;
todoList[index].isEdit = false;
this.setState({
todoList,
preEditValue: ''
})
}
}
removeItem(id){
var todoList = [...this.state.todoList];
// 找到目标元素下标
var index = todoList.findIndex(item=>item.id == id);
todoList.splice(index,1);
this.setState({
todoList
})
}
// 改变filterState
changeFilterState(state){
this.setState({
fitlerState: state
})
}
// 清除所有完成的
handleClearCompleted(){
var todoList = [...this.state.todoList];
todoList = todoList.filter(item=>!item.isDone);
this.setState({
todoList
})
}
render() {
return (
<section className="todoapp">
<header className="header">
<h1>todos</h1>
<input
className="new-todo"
placeholder="What needs to be done?"
autoFocus
value = {this.state.newTodoValue}
onChange = {this.handleNewTodo}
onKeyUp= {this.handleNewTodoEnter}
/>
</header>
{/* This section should be hidden by default and shown when there are todos */}
<section className="main">
<input id="toggle-all" className="toggle-all" type="checkbox" checked={this.state.allCheckState} onChange={this.handleCheckAll}/>
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
{
this.state.todoList.map(item=>{
// 待办
if(this.state.fitlerState == 'active'){
if(item.isDone) return null;
}
// 已完成
if(this.state.fitlerState == 'completed'){
if(!item.isDone) return null;
}
return(
/*把整个item对象传入组件*/
<TodoItem key={item.id} {...item}
handleEdit={this.handleEdit}
handleCheckOne={this.handleCheckOne}
handleEditInputChange={this.handleEditChange}
handleEditOkOrCancel={this.handleOkOrCancel}
removeItem={this.removeItem}/>
)
})
}
</ul>
</section>
<FooterBar fitlerState={this.state.fitlerState} itemLeft={this.state.itemLeft} changeFilterState={this.changeFilterState} handleClearCompleted={this.handleClearCompleted}/>
</section>
);
}
}
ReactDOM.render(<Todos/>,document.getElementById('app'));
什么时候封装组件?
至于什么时候需要组件封装,这个需要我们在以后的开发过程中根据实际情况来体会.这里我个人总结的两个原则: