网站建设价格槽闸阀,手机网站的内容模块,西安优秀的集团门户网站建设公司,Wordpress 101React学习笔记#xff08;番外二#xff09;——列表多选批量操作复合组件前言〇、Show you the code一、 任务分析及拆解表头行的Checkbox——总开关记录行的Checkbox——行级开关二、 基础实现表头行的文件——header-row.js记录行的文件——record-row.js页面的文件App.js…
React学习笔记番外二——列表多选批量操作复合组件前言〇、Show you the code一、 任务分析及拆解表头行的Checkbox——总开关记录行的Checkbox——行级开关二、 基础实现表头行的文件——header-row.js记录行的文件——record-row.js页面的文件App.js阶段效果三、 完善控制逻辑custom-hooks.js——自定义React Hook4处useState4处useCallback1处useMemoApp.js——部分改造header-row.js——部分改造record-row.js——部分改造阶段效果四、 最后一步后记前言
近期有需求需要实现如题的列表多选批量处理复合组件。因为是两部分组件互相控制对于初次实现的我来说还是有一定困难的。经过几天的实现、Bugfix现对这种比较常见的复合组件的实现作以记录。 〇、Show you the code
成熟的程序猿讲究【Talk is cheap, show me the code】如果你只想看代码
本篇文章中最终的完整代码已托管在CodeSandBox点击前往查看代码及编译、渲染结果 一、 任务分析及拆解
先来看一下上面说的到底是什么。如下图当我们有一个比较长的列表时有时希望一次性多选几行进行批量操作。
我们可以很轻松地将所需的复合组件拆分为以下几部分子组件
表头行的Checkbox——总开关记录行的Checkbox——行级开关右上方的操作按钮
表头行的Checkbox——总开关
这个Checkbox比较特殊有3个状态如下图
未选中状态 点击后变为全部选中状态。此状态下列表中的记录都是未选中状态。表头行Checkbox由于点击达到该状态时所有记录行Checkbox都变为未选中状态所有记录行Checkbox由于点击都达到未选中状态时表头行Checkbox也受控变为未选中状态。部分选中状态 点击后变为全部选中状态。此状态下列表中有部分记录[1, n-1]为选中状态。该状态只能由部分记录行Checkbox受到点击变为选中状态而达到。全部选中状态 点击后变为未选中状态。此状态下列表中的记录都是选中状态。表头行Checkbox由于点击达到该状态时所有记录行Checkbox都变为选中状态所有记录行Checkbox由于点击都达到选中状态时表头行Checkbox也受控变为全部选中状态。
记录行的Checkbox——行级开关
记录行前的就是常规的Checkbox只有两种状态如下图
未选中状态 点击后变为选中状态。此状态下当前行的记录是未选中状态。所有记录行Checkbox都为该状态时表头行Checkbox受控变为未选中状态。选中状态 点击后变为未选中状态。此状态下当前行的记录是选中状态。部分记录行Checkbox为该状态时表头行Checkbox受控变为部分选中状态。所有记录行Checkbox都为该状态时表头行Checkbox受控变为全部选中状态。 二、 基础实现
我们将一个包含列表的页面分为三个部分
表头行的文件——header-row.js记录行的文件——record-row.js页面的文件——App.js
接下来分别实现三个部分
表头行的文件——header-row.js
import ./index.css;
import React from react;
import { Checkbox } from antd;const HeaderRow () {return (div classNameheader-rowdiv classNamecolumn0Checkbox //divdiv classNamecolumn1Id/divdiv classNamecolumn2Title/divdiv classNamecolumn3Desc/div/div);
};export default HeaderRow;记录行的文件——record-row.js
import ./index.css;
import React from react;
import { Checkbox } from antd;const RecordRow (props) {let { record } props;let { id, title, desc } record;return (div classNamerecord-rowdiv classNamecolumn0Checkbox //divdiv classNamecolumn1{id}/divdiv classNamecolumn2{title}/divdiv classNamecolumn3{desc}/div/div);
};export default RecordRow;页面的文件App.js
import ./styles.css;
import React, { useMemo } from react;
import HeaderRow from ./components/header-row;
import RecordRow from ./components/record-row;const MainPage (props) {// 生成要显示的记录数据现实中应该通过后台请求获得let records useMemo(() {let _records [];for (let i 0; i 10; i) {_records.push({id: i 1,title: 这是测试标题${i 1},desc: 这是测试描述${i 1}});}return _records;}, []);// 记录行的组件列表let recordRowViews useMemo(() {return records.map((item) {return RecordRow key{record-${item.id}} record{item} /;});}, [records]);return (divHeaderRow /{recordRowViews}/div);
};export default MainPage;阶段效果
基础实现比较简单当然功能也不完善。我们还没有写Checkbox之间的控制逻辑先看看目前的效果 三、 完善控制逻辑
因为实现中涉及到多个页面具有同样的控件和逻辑需要因此实现中尽可能地考虑代码的复用性。这里的代码实现拆分为以下几个部分
custom-hooks.js——包含几个自定义React Hook最主要的Checkbox互相控制算法逻辑部分App.js——前文中已有的文件做部分改造header-row.js——前文中已有的文件做部分改造record-row.js——前文中已有的文件做部分改造
custom-hooks.js——自定义React Hook
import { useState, useMemo, useCallback } from react;/*** 列表多选批量处理的逻辑Hook* param showedRecords 经过过滤后页面需要显示的列表项* returns {[[string],boolean,boolean,boolean,function,function]}*/
const useBatchOperation (allRecords) {// 表示部分[1, n-1]行被勾选的状态仅控制UIlet [partialChecked, setPartialChecked] useState(false);// 表示是否所有的条目当前都被选中即UI表现为表头的checkbox是否勾选let [headerChecked, setHeaderChecked] useState(false);// 表示当前用户是否主动点击了表头的那一个checkbox点击后无论状态如何其他checkbox都要跟随该状态同headerChecked配合控制使用let [operateAll, setOperateAll] useState(false);// 当前勾选的行所代表的数据单元组成的列表let [checkedItemList, setCheckedItemList] useState([]);// 选中某行记录let checkItem useCallback((item) {let newCheckedItemList [...checkedItemList];if (!Array.isArray(item)) {item [item];}for (let i 0; i item.length; i) {let index checkedItemList.indexOf(item[i]);if (index -1) {newCheckedItemList.push(item[i]);}}setCheckedItemList(newCheckedItemList);return newCheckedItemList;},[checkedItemList]);// 取消选中某行记录let uncheckItem useCallback((item, clear false) {let newCheckedItemList [];if (!clear) {if (!Array.isArray(item)) {item [item];}for (let i 0; i checkedItemList.length; i) {if (item.indexOf(checkedItemList[i]) -1) {newCheckedItemList.push(checkedItemList[i]);}}}setCheckedItemList(newCheckedItemList);return newCheckedItemList;},[checkedItemList]);// 记录行checkbox的onChange事件let onSingleCheckBoxChange useCallback((checked, record) {// 按照判断逻辑必须放在第一行setOperateAll(false);let newCheckedItemList;if (checked) {newCheckedItemList checkItem(record);} else {newCheckedItemList uncheckItem(record);}setPartialChecked(newCheckedItemList.length 0 newCheckedItemList.length allRecords.length);setHeaderChecked(newCheckedItemList.length allRecords.length);},[checkItem, uncheckItem, allRecords]);// 表头行checkbox的onChange事件let onBatchCheckBoxChange useCallback(() {if (!headerChecked) {checkItem(allRecords);} else {uncheckItem(null, true);}setPartialChecked(false);setHeaderChecked(!headerChecked);setOperateAll(true);}, [headerChecked, checkItem, uncheckItem, allRecords]);// 选中的记录其Id组成的列表let checkedIdList useMemo(() {if (checkedItemList.length 0) {return checkedItemList.map((item) {return item.id;});}return [];}, [checkedItemList]);return [checkedIdList,partialChecked,operateAll,headerChecked,onSingleCheckBoxChange,onBatchCheckBoxChange];
};export { useBatchOperation };这里代码量有110行还是比较多的我们拆开来讲一下逻辑。一共用了3种React Hook分别是useState、useCallback和useMemo。其中useState4处useCallback4处useMemo1处。
4处useState // 表示部分[1, n-1]行被勾选的状态仅控制UIlet [partialChecked, setPartialChecked] useState(false);// 表示是否所有的条目当前都被选中即UI表现为表头的checkbox是否勾选let [headerChecked, setHeaderChecked] useState(false);// 表示当前用户是否主动点击了表头的那一个checkbox点击后无论状态如何其他checkbox都要跟随该状态同headerChecked配合控制使用let [operateAll, setOperateAll] useState(false);// 当前勾选的行所代表的数据单元组成的列表let [checkedItemList, setCheckedItemList] useState([]);partialChecked 标识表头行Checkbox当前是否处于部分选中状态仅用于控制UI样式初始值为falseheaderChecked 标识表头行Checkbox当前是否处于全部选中状态即是否所有记录行Checkbox都为选中状态初始值为falseoperateAll 标识当前用户是否主动点击了表头行Checkbox点击后所有记录行Checkbox必须跟随改变为同样的状态初始值为falsecheckedItemList 当前勾选的记录行所代表的数据组成的列表前文段落页面的文件App.js中records数组的子集初始值为空数组[]
4处useCallback
代码较长不再复制徒增篇幅。
checkItem 勾选某行记录行Checkbox后执行的事件方法。实际代码逻辑是给checkedItemList列表中新增一个记录数据然后通过setCheckedItemList更新最新的checkedItemList之所以使用useCallback封装一层是为了其访问到的checkedItemList是当前最新的。uncheckItem 和checkItem作用相反取消勾选某行记录行Checkbox后执行的事件方法。实际代码逻辑是在checkedItemList列表中找到并删除一个记录数据…onSingleCheckBoxChange 当记录行Checkbox被点击而发生状态变化时执行的事件方法。代码逻辑包括根据当前状态是选中还是未选中调用checkItem或uncheckItem更新checkedItemList根据checkedItemList的length判断表头行Checkbox是否应该改为部分选中、全部选中或未选中状态等。onBatchCheckBoxChange 当表头行Checkbox被点击而发生状态变化时执行的事件方法。代码逻辑包括根据当前状态是全部选中还是未选中调用checkItem或uncheckItem更新checkedItemList等。
1处useMemo
checkedIdList 根据checkedItemList实时计算表示当前被选中的记录的id组成的列表暴露出去向后台发请求时用。
App.js——部分改造
......
const MainPage (props) {......// 使用自定义hook-useBatchOperation传入records列表let [checkedIdList,partialChecked,operateAll,headerChecked,onSingleCheckBoxChange,onBatchCheckBoxChange] useBatchOperation(records);let recordRowViews useMemo(() {return records.map((item) {return (RecordRowkey{record-${item.id}}record{item}operateAll{operateAll}headerChecked{headerChecked}onSingleCheckBoxChange{onSingleCheckBoxChange}/);});}, [records, operateAll, headerChecked, onSingleCheckBoxChange]);return (divHeaderRowpartialChecked{partialChecked}headerChecked{headerChecked}onBatchCheckBoxChange{onBatchCheckBoxChange}/{recordRowViews}/div);
};export default MainPage;为了不占过多篇幅用......代替未发生改变的部分。
主要修改3个部分
创建一个自定义Hook-useBatchOperation的实例获得暴露出来的6个变量。RecrodRow传入operateAll、headerChecked和onSingleCheckBoxChangeHeaderRow传入partialChecked、headerChecked、onBatchCheckBoxChange 剩下暂时未用到的checkedIdList留给后面的batch-operation-buttons.js header-row.js——部分改造
......
const HeaderRow (props) {let { partialChecked, headerChecked, onBatchCheckBoxChange } props;......Checkboxindeterminate{partialChecked}checked{headerChecked}onChange{onBatchCheckBoxChange}/......);
};export default HeaderRow;主要修改2个部分
引入父组件App.js传入的partialChecked、headerChecked、onBatchCheckBoxChange 将上述值和方法传入Checkbox控件
record-row.js——部分改造
......
const RecordRow (props) {......let { record, operateAll, headerChecked, onSingleCheckBoxChange } props;let [checked, setChecked] useState(false);useEffect(() {if (operateAll) {setChecked(headerChecked);}}, [operateAll, headerChecked, setChecked]);let wrappedOnChange useCallback(() {setChecked(!checked);onSingleCheckBoxChange(!checked, record);}, [checked, setChecked, record, onSingleCheckBoxChange]);return (......Checkboxchecked{checked}onChange{wrappedOnChange}onClick{wrappedOnChange}/......);
};export default RecordRow;主要修改4个部分
引入父组件App.js传入的operateAll、headerChecked、onSingleCheckBoxChange 添加一个useEffect Hook如果表头行Checkbox被点击跟随其改变状态给onSingleCheckBoxChange封装一层被点击时不仅要更新自身状态还要触发自定义Hook的数据更新将上述值和方法传入Checkbox控件
阶段效果
全选 / 取消全选 部分选中 四、 最后一步
经过前文中的努力UI上表现达到预期了所需的数据checkedIdList也拿到了距离实现文章开头的效果还差右上角的操作按钮。这里我们新建一个batch-operation-buttons.js
import ./index.css;
import React, { useCallback } from react;const BatchOperationButtons (props) {let { checkedIdList } props;let handleClick useCallback((func) {switch (func) {case 0:alert(批量删除记录IdList${JSON.stringify(checkedIdList)});break;case 1:alert(批量下载记录IdList${JSON.stringify(checkedIdList)});break;default:throw new TypeError(操作码[${func}]未实现);}},[checkedIdList]);if (checkedIdList?.length 1) {return null;}return (div classNamebatch-operation-buttonsspan classNamedelete-button onClick{() handleClick(0)}删除/spanspan//spanspan classNamedownload-button onClick{() handleClick(1)}下载/span/div);
};export default BatchOperationButtons;注意 这里的alert是为了演示方便现实情况下应该改为向后台发请求按照checkedIdList和操作的func对选中的记录进行批量操作. App.js中仅需改动以下代码即可 ......return (divdiv classNamebuttonsBatchOperationButtons checkedIdList{checkedIdList} //div....../div);后记
这次的实现中首次学习并使用了自定义Hook。实现之后发现了它的重要意义即我们可以通过自定义Hook真正地将HTML模板和JS数据逻辑拆分为两个独立的文件一个只管模板内容一个只管数据的更新。官方一点的说法即实现了Modelcustom-hooks.js和ViewApp.js的分离意义重大。
为了便于理解你可以想象在学会使用自定义Hook之前App.js和custom-hooks.js里的代码统统写在App.js里这样一个控件里的代码量将非常大维护起来比较麻烦。拆分为两个文件后模板有Bug我们就只改View文件数据有Bug我们就只改Model文件。