上节课我们学习了如何去删除一个todo,现在我们再回头看看我们整个案例,是不是就只剩下底部的Footer没有实现了?那么这节课我们就来讲一下这个功能的实现。

回顾

在开始讲解底部功能之前我们来先对上节课的内容来做一个简单的回顾。

  • 数组的filter方法用来根据某些条件完成过滤效果
  • delete是一个关键字,不能直接拿来做方法名
  • confirm再使用时要指定window.confirm

以上便是上节课我们的主要知识点。接下来我们来开始这节课的新内容

需求分析

image-20220110144306981

首先我们先来分析一下底部这一块的元素:

第一,有一个checkbox,这个checkbox有全选所有todo的功能,比如我们todolist中的所有事情都已经完成了,但是我们之前一直没有事件去勾选完成,那么现在我们一个个去勾选太烦人了,那么我就可以通过这个checkbox来一件完成。

第二,我们有一个区域来统计我们今天总共有多少事儿要做,而且截止到目前为止,我们完成了多少。这个就不多解释什么作用了。

第三,一个清除所有已完成任务的按钮,这个也不难理解如果说我们有太多事情要做,完成了一部分了,但是没件事情也不可能就完全按照todolist中的顺序来完成的,而且边有完成的事情,一边可能还有新的事情添加进来,那么整个list看着就会很乱。那么我们就把已完成的全部删掉这样就可以算是我们所谓的专注模式嘛。

以上便是我们整个底部区域的需求。

功能分析

我们来逐一分析一下我们的这三个功能。

首先从最简单的,我们所有todo是不是都存放在App组件的state里面?那么我们总共有多少事情是不是就是这个statetodos数组的长度?已完成的数量是不是statetodos数组里done属性为true的数量?

第二,一键完成的checkbox也不难啊,我们是不是在App组件中提供一个入口函数供Footer组件调用,然后直接将statetodos里的所有todo对象的done属性都改成true就可以了?

第三个,删除所有已完成任务,既然是个按钮,那么就要用onClick事件,绑定一个回调,因为这个事件是在Item组件中触发的,那么在回调里就要调用App组件提供的入口函数来将statetodos数组里所有done属性为truetodo对象都过滤掉。

以上便是我们功能实现的思路

功能实现

既然我们已经分析了思路了,那么我们来开始逐一做功能实现吧:

首先我们来实现统计功能:

// App 组件
export default class App extends Component {

  state = { todos: [] };

  add = dataObj => {...};
  update = (id, done) => {...};
  deleteTodo = id => {...};

  render() {
    const { todos } = this.state;
    return (
      ...
          <Footer todos={todos} />
      ...
    );
  }
}
// Footer 组件
  export default class Footer extends Component {
  render() {
    const { todos } =this.props;
    return (
      <div className="footer">
        ...
        <span>
          <span>
            已完成{todos.reduce((pre, todo) => todo.done ? pre + 1 : pre, 0)}
          </span> / 全部{todos.length}
        </span>
        ...
      </div>
    )
  }
}

我们来看代码,App组件中的代码没有什么可说的,既然我们要知道statetodos数组的长度已经todos数组中done属性为true的数量那么肯定就要把state通过props来传给Footer组件。

我们主要要在乎的是Footer组件。暂时多余的代码就先省略了。既然App传了todos了,那么Footer组件是便是就要接啊?我们看,在Footer组件中先是解构赋值拿到todos

然后我们来看这一块,我们在一个<span>中又套了一个<span>,我们先来看外层的<span>,这个结构是不是就是一个<span>后面跟了一串文字,这串文字我们是看得明白的,这是我们全部的事情的总数。就是todos数组的长度啊。

那么这串文字前面的<span>里面是什么?我们是不是对todos数组做了条件统计啊?我们的条件什么什么?是不是todo对象的done属性?只要done属性为true,那么我们就在原基础上加 1,当然你也可以用很多其他方法,比如filter之后取length,或者直接for循环,但是reduce方法一定得会,这儿个方法实在太重要也太常用了。如果大家对reduce方法感觉陌生的,赶紧回去复习数组的常用方法。我们那么我们来看一下结果:

image-20220110151526597

我们总共有 5 个todo,完成了 2 个,那么在底部是不是完完整整得统计好展示了出来?这就是我们的第一个功能点。但是还有一点看图

image-20220110154935531

当我们全部勾选之后,那是不是代表我们所有todo都已经完成了啊?可是这里我们的checkbox并没有被勾选,我们是不是应该要求当我们已完成的数量等于全部数量就自动勾选这个checkbox?那么是不是就要用到我们之前提到过的checked属性?之前我们没有这个属性,是不是因为这个属性一但写了就要绑定onChange事件?不然这个checkox就变成只读的了?我们来看代码:

export default class Footer extends Component {
  render() {
    const { todos } = this.props;
    const total = todos.length;
    const doneCount = todos.reduce((pre, todo) => todo.done ? pre + 1 : pre, 0);
    return (
      <div className="footer">
        <label>
          <input checked={doneCount === total && total !== 0} type="checkbox" />
        </label>
        <span>
          <span>已完成{doneCount}</span> / 全部{total}
        </span>
        <button className="btn btn-danger">清除已完成任务</button>
      </div>
    );
  }
}

首先,因为我们要做比较,多次使用某个值,为了方便,那就定义一个变量吧。然后我们来看这个checkbox,我们写checked是不是不能写死?我们要已完成数量和总数相等才能让checkedtrue。但是我们要注意一点,当我们整个列表中没有任何todo的时候我们应该勾选吗?是不是觉得勾不勾选没有什么意义?那么我们默认不勾选,因为没有需要完成的事情,那么我们来看一下结果:

image-20220110160032901

首先,列表里面什么都没有,不勾选然后我们添加事件再全部勾选完成:

image-20220110160142619

现在看到我们将todo全部完成之后Footercheckbox也自动勾选了,那么我再取消掉一个看看:

image-20220110160257783

取消掉任意一个,底部的checkbox都会自动取消勾选。但是我们这里可以用defaultChecked吗?还记不记得我们之前怎么介绍defaultChecked的?这个玩意儿只在第一次加载有效,之后无论如何都不会再起作用的,所以说不可以使用defaultChecked

那么接下来我们来看第二个,实现一键完成功能:

// App 组件
export default class App extends Component {
  state = { todos: [] };
  ...
  finishTodos = done => {
    const { todos } = this.state;
    const newTodos = todos.map(todo => ({...todo, done}));
    this.setState({ todos: newTodos });
  };

  render() {
    const { todos } = this.state;
    return (
      ...
          <Footer todos={todos} finishTodos={this.finishTodos} />
      ...
    );
  }
}
// Footer 组件
export default class Footer extends Component {
  render() {
    ...
    return (
      <div className="footer">
        <label>
          <input onChange={this.handleFinish}... />
        </label>
        ...
      </div>
    );
  }
  handleFinish = e => {
    const { finishTodos } = this.props;
    finishTodos(e.target.checked);
  }
}

我吧目前不需要的代码先隐藏掉,我们先来看App组件中,先定义一个finishTodos方法,方法中我们先获取到原有的todos,然后将数组加工一下,用map遍历数组复制数组中每个对象并将done属性修改掉,大家看我们这里还接收了参数,为啥接收参数呢?因为我们既然要全选,自然要有取消全选啊。

我们有没有想过这个删除全部已完成事件还有一个应用场景是什么?如果我们准备旅游,规划了一堆事情,然后因为疫情原因,去不了了,那么我们这些事情一条条删是不是很累,那么我们一键完成,然后删除已完成任务,是不是很方便?

但是如果在你全选了即将删除的时候,发现,这个消息是朋友骗你的,其实你想去的城市没有被疫情影响,那么你是表示要取消全选,所以说我们要用event.target.checked来更改这离的done属性。

那么好,我们吧这个done属性确定好了之后是不是就要修改state了?然后把这个方法传给Footer组件,我们来看一下Footer组件,我们给checkbox对应的<input>绑定了onChange事件,之前我们加了checked,现在绑定了onChange是不是就使得我们的checkbox不再是只读的了?我们给onChange事件设置的回调函数是handleFinish方法,在这里接收event并将event.target.checked传给finishTodos函数来完成App组件state的更新。那么我们来看一下效果:

image-20220110163236746

有意思的问题出现了。我们勾选了底部的checkbox是不是一键完成了所有todo?那么我们每件todo独立对应的checkbox为什么没有被勾选?而且state中每个tododone属性是不是也正常完成了更新?按照逻辑来说页面上应该是勾选的才对啊。这是为什么呢?

这就是因为我们在Item组件中,将checkbox<input>用了defaultChecked,因为defaultChecked只在第一次加载才会起作用,那么我们给改一下用checked吧,可能有朋友要说,不行啊,用checked不就成只读的了吗?我们说过如果用checked而且没有绑定onChange事件的情况下,会使得checkbox变成只读的状态,而且我们之前使用defaultChecked也确实是为了避免这个问题。但是我们Item组件中现在是不是已经给checkbox加上了onChange事件了啊?那还怕什么呢,改成checked我们再来看看效果:

image-20220110164248133

勾选底部checkbox之后全部勾选。

image-20220110164324398

取消勾选底部checkbox之后又全部取消勾选。

到此我们的全选功能也写完了,还剩最后一个,就是我们的删除已完成任务:

// App 组件
export default class App extends Component {
  state = { todos: [] };
  ...
  deleteFinished = () => {
    const { todos } = this.state;
    const newTodos = todos.filter(todo => todo.done !== true);
    this.setState({ todos: newTodos });
  }

  render() {
    const { todos } = this.state;
    return (
      ...
          <Footer ... deleteFinished={this.deleteFinished}/>
      ...
    )
  }
}
// Footer 组件
export default class Footer extends Component {
  render() {
    ...
    return (
      ...
        <button onClick={this.handleDelAllFinished} className="btn btn-danger">
          清除已完成任务
        </button>
      ...
    );
  }
  ...
  handleDelAllFinished = ()=>{
    this.props.deleteFinished();
  }
}

我们来看一下代码,我们首先在App组件中定义了一个deleteFinished方法,这个方法中就是获取当前的todos,然后用数组的filter方法来过滤掉done属性为truetodo对象,只留下done属性为falsetodo对象。然修改state,从而使得我们的todos里面只剩下未完成的todo

紧接着把这个方法传给Footer组件,然后我们来看一下Footer组件,我们给按钮绑定了onClick事件,用handleDelAllFinished方法作为onClick事件的回调。在这个回调中直接调用props中接收到的App组件传过来的deleteFinished函数,从而来完成对App组件中state的修改。那么我们来看具体的效果是什么样子:

image-20220110165544583

我们最初始有 7 个todo,完成了 4 个,现在我们清除已完成任务看一下:

image-20220110165634259

现在就只剩先 3 条未完成的todo了。但是我们现在也还是直接删除也没有确认,那么就像上节课一样加一个confirm吧,这里我就不再赘述了。

至此我们边完成了我们整个案例。

总结

  • reduce方法来着条件统计
  • defaultChecked只在第一次加载起作用
  • 要学会需求分析和功能实现思路分析

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/todolist_footer/