React实现Todos

我们掌握了react的基本语法,我们尝试实现一个todos的案例.

素材

功能实现

修改解构

  • 需要动态渲染的内容复制到JSX中
  • 修改JSX中的注释为 {/* */} 形式
  • 修改原生HTML属性为驼峰类型

开发顺序

  1. 实现新增
  2. 实现列表展示
  3. 实现是否完成状态切换
  4. 实现item left
  5. 全选/取消全选
    1. 注意setState异步问题
  6. 双击编辑
    1. 注意取消还原的问题
  7. 删除
  8. 状态过滤
  9. 清除完成

代码部分:

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'));

什么时候封装组件?

至于什么时候需要组件封装,这个需要我们在以后的开发过程中根据实际情况来体会.这里我个人总结的两个原则:

  • 考虑功能模块的复用性
  • 提高代码的可读性,组件的逻辑性

总结

我们到目前为止,react的基本语法已经学完了.如果大家有vue基础,相信能很快理解并掌握react. 当然react还有路由管理,状态管理等相关知识点,我们会在接下来的项目开发中,带大家直接上手,在开发中理解并学会其应用.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值