上节课我们学习了然后初始化加载展示一个动态的列表,在学习的过程中留下了一个问题不知道大家还记不记得,就是我们的子组件能修改父组件的state吗?因为上节课我们把数据都存储在了App组件的state中了。如果我们想通过Add组件来添加新的todo的话是不是就要把todo加到Appstate中去?那么我们能不能完成这步操作呢?这节课我们就来研究一下。

回顾

在开始研究如何在子组件中修改父组件的state之前,我们来对上节课的内容做一个简单的回顾。

  • 目前我们无法在平级组件之间相互传递数据
  • 在我们需要动态展示数据时一定要却定数据应该存储在哪个组件中
  • 父组件可以给任意子组件传递数据
  • checkbox如果写了checked属性,如果不写onChange事件就会变成只读状态
  • 遍历渲染一定要加唯一性的key

以上便是上节课的主要内容,接下来我们来研究能不能以及如何在子组件中修改父组件的state

添加新todo

我们先来看一下,我们既然要添加新todo,那么是一个什么流程?

  1. 输入todo
  2. 按下回车键
  3. 获取到输入的值
  4. 添加到App组件的state
  5. 重新加载页面

那么我们从简单的开始,首先第 5 步是不是根本不需要我来做?只要成功修改了App组件的state,是不是就自动重新加载页面了?第 1 ~ 2 步是不是也没有什么多难的地方?有手就会啊,那么问题重点是不是在第 3 ~ 4 步?

获取输入的值

先看第 3 步,获取输入的值,怎么获取?什么时候获取?我们说了是按下回车之后更新,那么是不是在按下回车之后我们去获取数据然后更新到App组件的state中?那么我们来想想在按下回车后取值,这是不是一个键盘事件?是不是用onKeyUp?为什么是onKeyUp?因为onKeyUP是按键按完在弹起,这代表正式按完了。

那么怎么获取呢?是不是下意识就想到了refs?但是我们这里是给谁绑定的事件?是不是<input>?而我们要获取谁的值?是不是也是<input>?那么我们还需要用refs吗?是不是就不用了直接用event.target就行了啊。那么好我们来编写一下代码:

export default class Add extends Component {
  render() {
    return (
      <div className="todo-add">
        <input 
          onKeyUp={this.handleKeyUp} type="text" 
          placeholder="请输入你的任务名称,按回车键确认" />
      </div>
    );
  }
  handleKeyUp = e => {
    console.log(e.target.value);
  }
}

我们来看一下,我们给<input>绑定了onKeyUp事件给了一个handleKeyUp方法做回调,用e来接收event,然后我们打印了e.target.value,为什么我们打印呢?因为什么现在还不知道怎么把获取到的值存到App组件的state中。我们先看一下效果。看看能不能拿到值:

image-20220107155838456

确实是拿到了值,但是好像我们输入的每一个字符都被获取到了。这并不是我们想要的功能,那么我们是不是要在回调方法中来做一个判断了?那么根据什么判断?是不是我们原生js里面就提到过得keyCode?那回车键的keyCode是多少?是不是13?那么我们来改一下代码

export default class Add extends Component {
  render() {...}

  handleKeyUp = e => {
    const {target,keyCode} = e;
    if (keyCode !== 13) return;
    console.log(target.value);
  }
}

现在我们做了判断是只当我们敲下回车之后才输出,那我们看一下效果吧:

image-20220107160358191

这次就只输出了我们敲下回车之后输入框中的值。

更新App组件的State

现在我们已经可以一按回车就获取到我们输入的值了,那么现在我们是不是就差一步,就是把值更新到App组件的state中就可以了?那么怎么更新进去呢?我们来分析一下:

我们的App组件中是不是有一个Add组件?这两个组件之间是什么关系?父子关系对不对?父组件给子组件传东西是不是很简单?直接传,然后子组件就会自动给收集到props中去,那么父组件怎么拿到子组件中的值呢?其实也很简单,来看看我们这段代码

export default class App extends Component {
  state = {...};

  add = data => {
    const {todos} = this.state;
    const todo = {id: todos.length + 1, name:data, done: false}
    this.setState({todos:[todo,...todos]});
  };

  render() {
    const {todos} = this.state;
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          <Add add={this.add} />
          <List todos={todos} />
          <Footer />
        </div>
      </div>
    )
  }
}

这是什么意思?我是不是定义了一个方法?然后把方法传给了Add组件?可能有人要说这个没明白,大家看,我是不是在App组件中写了一个方法?这个方法中的this指向是谁?是不是App组件的实例对象啊?那我把这个函数传给Add组件的换this指向会丢吗?当然不会。

那么我在Add组件中调用我们传进去的add方法,那是不是就直接操作了App组件中的state?那么我们再让add方法来接收一个参数,那么我们是不是就可以在Add组件中把值传给add方法,由add方法来修改Appstate了?

再来看看add方法中,我们添加新的todo,旧todo是不是还要留着啊?而且新todo要在最前面,那么我们就用展开运算符来拼接一下。接下来我们来修改一下Add组件

export default class Add extends Component {
  render() {...}
  handleKeyUp = e => {
    const {target,keyCode} = e;
    if (keyCode !== 13) return;
    this.props.add(target.value);
  }
}

现在我们就不需要打印target.value了,我们直接修改到了state中去,那么我们来看一下效果:

image-20220107162830281

这次我们成功添加了todo,而且正如需求中所说的那样,新todo在最上面显示。

但是我们这么写其实是不规范的,为什么呢?如果说我们的这个state中的todo对象结构变了的话我们是不是还要去改这个方法啊?那么我们怎么改一下?

export default class App extends Component {
  state = {...};

  add = dataObj => {
    const { todos } = this.state;
    this.setState({ todos: [dataObj, ...todos] });
  };

  render() {...}
}

我们这样,我们让add方法直接接收一个对象,我不管你结构变成什么样我就要求这是你在组件里面处理好的一个对象就行了。那么好,这里改了,我们是不是还要去改Add组件啊?那么我们又有一个问题,我们这里的id用什么呢?我们之前那种写法可以用state的长度,现在我虽然能改state但是我并不能拿到啊。这里我们来推荐一个库叫nanoid,这是一个很小而且能够生成全球唯一的id的一个库,直接在命令行中通过npm install nanoid就可以安装了,那么我们来改一下代码

import React, { Component } from 'react';
import { nanoid } from 'nanoid';
import './Add.css';

export default class Add extends Component {
  render() {...}
  handleKeyUp = e => {
    const { target, keyCode } = e;
    if (keyCode !== 13) return;
    const todo = { id: nanoid(), name: target.value, done: false };
    this.props.add(todo);
  }
}

首先我们来看,我们导入了nanoid然后在handleKeyUp方法中我们定义了一个新的todo对象,他的id直接调用了nanoid方法来生成,name就是我们的target.value,那么done呢?我们往todolist里面添加是不是肯定都还没完成?如果完成了也不会往里面添加啊。所以说这都是没完成的,所以说,那就默认false。然后我们再把这个对象传给add方法就可以完成state的更新了,我们再看一下效果:

image-20220107165235291

这次我也成功地更新了页面。但是我们有一个细节上的问题大家注意到没有?我们添加新todo之后输入框却没有清空,而且还有一个问题:

image-20220107165427547

像图上这样,我们什么都没用输入,但是也成功添加了,那么我们应该怎么处理?

export default class Add extends Component {
  render() {...}
  handleKeyUp = e => {
    const { target, keyCode } = e;
    if (keyCode !== 13) return;
    if (target.value.trim() ===""){
      alert("输入不能为空!");
      return;
    }
    const todo = { id: nanoid(), name: target.value, done: false };
    this.props.add(todo);
  }
}

我们先给handleKeyUp方法加了一个判断。这是什么意思?就是判断输入的值是不是为空,但是为了防止有人输入的就是一串空格,那么我们先给去一下空格在来检查输入是否为空,如果为空,我们先提示一下,然后千万不能忘了,一定要return来终止函数。那我们再来看一下效果:

image-20220107165934902

这一次我们这种不符合规则的内容将无法添加了。那么另一个问题,清空,那简单啊,最后我们让输入框的值为空字符串不就行了嘛

export default class Add extends Component {
  render() {...}
  handleKeyUp = e => {
    const { target, keyCode } = e;
    if (keyCode !== 13) return;
    if (target.value.trim() ===""){
      alert("输入不能为空!");
      return;
    }
    const todo = { id: nanoid(), name: target.value, done: false };
    this.props.add(todo);
    target.value = "";
  }
}

我们在最后把输入栏清空,我们看一下效果是不是有效:

image-20220107170222687

添加todo之后输入框也清空了,所以说这样是没有问题的,以上便是我们添加的功能

总结

  • 添加todo分为 5 步
  • 输入todo
  • 按下回车键
  • 获取到输入的值
  • 添加到App组件的state
  • 重新加载页面
  • 通过onKeyUp来监听键盘事件
  • 通过keyCode来判断是否是回车键
  • 通过父组件传入子组件一个函数来提供一个修改父组件state的入口给子组件

Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.

Link of this article:https://work.lynchow.com/article/lesson059_todolist_add_todo/