上节课我们学习了如何去删除一个todo
,现在我们再回头看看我们整个案例,是不是就只剩下底部的Footer
没有实现了?那么这节课我们就来讲一下这个功能的实现。
回顾
在开始讲解底部功能之前我们来先对上节课的内容来做一个简单的回顾。
- 数组的
filter
方法用来根据某些条件完成过滤效果 delete
是一个关键字,不能直接拿来做方法名confirm
再使用时要指定window.confirm
以上便是上节课我们的主要知识点。接下来我们来开始这节课的新内容
需求分析
首先我们先来分析一下底部这一块的元素:
第一,有一个checkbox
,这个checkbox
有全选所有todo
的功能,比如我们todolist
中的所有事情都已经完成了,但是我们之前一直没有事件去勾选完成,那么现在我们一个个去勾选太烦人了,那么我就可以通过这个checkbox
来一件完成。
第二,我们有一个区域来统计我们今天总共有多少事儿要做,而且截止到目前为止,我们完成了多少。这个就不多解释什么作用了。
第三,一个清除所有已完成任务的按钮,这个也不难理解如果说我们有太多事情要做,完成了一部分了,但是没件事情也不可能就完全按照todolist
中的顺序来完成的,而且边有完成的事情,一边可能还有新的事情添加进来,那么整个list
看着就会很乱。那么我们就把已完成的全部删掉这样就可以算是我们所谓的专注模式嘛。
以上便是我们整个底部区域的需求。
功能分析
我们来逐一分析一下我们的这三个功能。
首先从最简单的,我们所有todo
是不是都存放在App
组件的state
里面?那么我们总共有多少事情是不是就是这个state
中todos
数组的长度?已完成的数量是不是state
中todos
数组里done
属性为true
的数量?
第二,一键完成的checkbox
也不难啊,我们是不是在App
组件中提供一个入口函数供Footer
组件调用,然后直接将state
中todos
里的所有todo
对象的done
属性都改成true
就可以了?
第三个,删除所有已完成任务,既然是个按钮,那么就要用onClick
事件,绑定一个回调,因为这个事件是在Item
组件中触发的,那么在回调里就要调用App
组件提供的入口函数来将state
中todos
数组里所有done
属性为true
的todo
对象都过滤掉。
以上便是我们功能实现的思路
功能实现
既然我们已经分析了思路了,那么我们来开始逐一做功能实现吧:
首先我们来实现统计功能:
// 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
组件中的代码没有什么可说的,既然我们要知道state
中todos
数组的长度已经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
方法感觉陌生的,赶紧回去复习数组的常用方法。我们那么我们来看一下结果:
我们总共有 5 个todo
,完成了 2 个,那么在底部是不是完完整整得统计好展示了出来?这就是我们的第一个功能点。但是还有一点看图
当我们全部勾选之后,那是不是代表我们所有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
是不是不能写死?我们要已完成数量和总数相等才能让checked
为true
。但是我们要注意一点,当我们整个列表中没有任何todo
的时候我们应该勾选吗?是不是觉得勾不勾选没有什么意义?那么我们默认不勾选,因为没有需要完成的事情,那么我们来看一下结果:
首先,列表里面什么都没有,不勾选然后我们添加事件再全部勾选完成:
现在看到我们将todo
全部完成之后Footer
的checkbox
也自动勾选了,那么我再取消掉一个看看:
取消掉任意一个,底部的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
的更新。那么我们来看一下效果:
有意思的问题出现了。我们勾选了底部的checkbox
是不是一键完成了所有todo
?那么我们每件todo
独立对应的checkbox
为什么没有被勾选?而且state
中每个todo
的done
属性是不是也正常完成了更新?按照逻辑来说页面上应该是勾选的才对啊。这是为什么呢?
这就是因为我们在Item
组件中,将checkbox
的<input>
用了defaultChecked
,因为defaultChecked
只在第一次加载才会起作用,那么我们给改一下用checked
吧,可能有朋友要说,不行啊,用checked
不就成只读的了吗?我们说过如果用checked
而且没有绑定onChange
事件的情况下,会使得checkbox
变成只读的状态,而且我们之前使用defaultChecked
也确实是为了避免这个问题。但是我们Item
组件中现在是不是已经给checkbox
加上了onChange
事件了啊?那还怕什么呢,改成checked
我们再来看看效果:
勾选底部checkbox
之后全部勾选。
取消勾选底部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
属性为true
的todo
对象,只留下done
属性为false
的todo
对象。然修改state
,从而使得我们的todos
里面只剩下未完成的todo
。
紧接着把这个方法传给Footer
组件,然后我们来看一下Footer
组件,我们给按钮绑定了onClick
事件,用handleDelAllFinished
方法作为onClick
事件的回调。在这个回调中直接调用props
中接收到的App
组件传过来的deleteFinished
函数,从而来完成对App
组件中state
的修改。那么我们来看具体的效果是什么样子:
我们最初始有 7 个todo
,完成了 4 个,现在我们清除已完成任务看一下:
现在就只剩先 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/