热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【react+ts+antd】开发一个单行编辑气泡组件的血泪史

首先接到的任务是这样的:

首先接到的任务是这样的:

【react+ts+antd】开发一个单行编辑气泡组件的血泪史 - 文章图片

 

 【react+ts+antd】开发一个单行编辑气泡组件的血泪史 - 文章图片

 

 那么打开参考对象看一眼:

【react+ts+antd】开发一个单行编辑气泡组件的血泪史 - 文章图片

 

 总结一下组件的内容和功能点:

1.一个输入框,两个按钮(确定,取消)

2.点击文本,弹出气泡,进行编辑,提交/取消,关闭气泡,更新数据(数据不变则不更新)

而原本的组件,则是直接点击编辑按钮,变为编辑模式:

【react+ts+antd】开发一个单行编辑气泡组件的血泪史 - 文章图片

 

 

因此,我选择了antd提供的Popover组件,稍微封装一下功能,做成一个独立的小小组件,代码是这样的:

import React, { useState, useEffect, useRef, useImperativeHandle } from 'react';
import { Input, Button, Popover } from 'antd';
import { CloseCircleOutlined } from '@ant-design/icons';
// 工具函数
import { trimAllBlank } from '@/utils/tools';
// 样式文件
import styles from './style.less';
// 属性定义文件
import { Props } from './index.type';
/**
* Single line edit bubble component【单行编辑气泡组件】
* author: wun
*/
const TheEditCellBubble: React.FC = (props) => {
const {
inputType,
initValue,
record,
dataIndex,
placeholder,
verify,
className,
request,
update,
cRef,
} = props;
// 输入框ref
const inputRef = useRef(null);
// 输入框的值
const [inputValue, setInputValue] = useState('');
// 单行展示的值
const [showValue, setShowValue] = useState('');
// 错误提示文案
const [errorText, setErrorText] = useState('');
// 错误提示文案展示状态控制
const [errorVisible, setErrorVisible] = useState(false);
// 确认按钮loading状态控制
const [submitLoading, setSubmitLoading] = useState(false);
// 气泡展示状态控制
const [visible, setVisible] = useState(false);
// 校验函数
const verifyInput = (val: any) => {
if (verify && verify.rules && verify.rules.length > 0) {
const error = verify.rules.find((el: any) => {
// 空验证
if (el.required) {
return !val;
}
// 正则验证
if (el.pattern) {
return !el.pattern.test(val);
}
// 自定义验证
if (el.validator) {
return !el.validator(val);
}
return false;
});
if (error) {
setErrorVisible(true);
setErrorText(error.message);
return false;
}
}
return true;
};
// 监听输入框实时内容
const handleChange = (e: { target: { value: string } }) => {
const val = e.target.value;
setInputValue(trimAllBlank(val));
// 重置错误提示
if (errorVisible && verifyInput(val)) {
setErrorVisible(false);
setErrorText('');
}
};
// 确定-回调
const handleOk = async (e: React.MouseEvent | React.KeyboardEvent) => {
e.stopPropagation();
// 如输入框内容未修改,直接return
if (inputValue === showValue) {
return;
}
// 验证输入内容
if (!verifyInput(inputValue)) return;
// 创建参数对象
const params = dataIndex ? { [dataIndex]: inputValue } : {};
// 如需发送请求
if (request) {
try {
// 确认按钮loading状态开启
setSubmitLoading(true);
// 发起请求
const res: any = await request({ ...record, ...params });
if (res && res.code === 0 ) {
setShowValue(inputValue);
if (update) update(params, res);
setVisible(false);
}
// 默认值不存在时一般是做为新建功能使用此组件, 默认会在成功后清空输入项
if (!initValue) setInputValue('');
setSubmitLoading(false);
} catch (error) {
setSubmitLoading(false);
} finally {
//
}
} else if (update) {
// 无需发送请求,则直接修改数据并返回
setShowValue(inputValue);
update(params, {});
setVisible(false);
setSubmitLoading(false);
}
}
// 取消-回调
const handleCancel =(e: React.MouseEvent)=>{
e.stopPropagation();
setVisible(false);
}
// 点击打开气泡
const handleVisibleChange = () => {
setVisible(true)
};
// 暴露给父级的方法
useImperativeHandle(cRef, () => ({
// 获取当前输入框值
value: inputValue,
// 可编辑状态时手动插入值
insert: (value: string) => {
// 在当前光标位置插入内容
if (typeof inputValue === 'string') {
const { input } = inputRef.current;
const { selectionStart, selectionEnd } = input;
// 优先插入当前光标所在位置, 如无法确定当前光标所在位置则插入当前值末尾
setInputValue(
inputValue.substring(0, selectionStart) +
value +
inputValue.substring(selectionEnd, inputValue.length),
);
// 重置光标位置
input.focus();
}
// 重置错误提示
if (errorVisible && verifyInput(value)) {
setErrorVisible(false);
setErrorText('');
}
},
}));
// 气泡展示时输入框自动聚焦
useEffect(() => {
let timer: any = null;
if (visible) {
timer = setTimeout(() => {
inputRef.current.focus();
}, 0);
}
return function cleanUp() {
if (timer) clearTimeout(timer);
};
}, [visible]);
// 内容初始化赋值
useEffect(() => {
if (initValue) {
setShowValue(initValue);
setInputValue(initValue);
}
}, []);
return (


placement="bottom"
cOntent={


/>



{errorVisible &&
{errorText}
}

}
trigger="click"
visible={visible}
OnVisibleChange={handleVisibleChange}
getPopupCOntainer={(triggerNode) => triggerNode} // 改变浮层渲染父节点
>



);
};
export default TheEditCellBubble;

  

属性定义的文件是这样的:

export interface Props {
inputType?: string; // input类型
initValue?: string; // 单元格初使值
record?: any; // 行数据
dataIndex?: string; // 单元格数据在行数据中对应的路径
cRef?: any;
placeholder?: string;
verify?: {
rules?: any; // 规则
maxLength?: number; // 最大程度
}; // 单元格输入相关规则
className?: string; // 自定义文本状态 class
request?: (params?: any) => Promise; // 更新单元格数据接口
update?: (params?: object, result?: any) => void; // 更新回调, 回传请求参数和后台返回数据
}

  

css样式是这样的:

.c-edit_cell-bubble {
.c-edit_cell-bubble-content{
width: 500px;
display: flex;
min-height: 32px;
align-items: center;
padding: 4px;
box-sizing: border-box;
white-space: nowrap;
transition: linear 2s;
input{
width: 70%;
}
button {
margin-left: 8px;
}
.c-edit_cell-bubble-input-error{
border-color: red;
}
}
.c-edit_cell-bubble-error-tips{
min-height: 20px;
line-height: 1.5;
color: red;
.c-edit_cell-bubble-error-icon{
color: red;
margin: 0 4px;
}
}
}

  

使用方式是这样的:

# Single line edit bubble component【单行编辑气泡组件】
## 引用
import { BasisTheEditCellBubble } from '@/components/index';
## 调用
``

``
## 属性参考
index.type.ts文件
########### 示例参考
[可替换掉项目管理的BasisEditTableCell组件用以体验]
``
initValue={text}
record={record}
dataIndex="appName"
verify={{
...rulesData.appName,
rules: [
{pattern: /\S+/,message: `请输入${ tableHeaderList.filter((el: any) => el.dataIndex === 'appName')[0].title}`,
},
],
}}
request={modifyProject}
update={() => initTableList()}
/>
``

  

我觉得很ok,于是提交了代码,跟大佬表示做完了!

然而大佬看过之后,却表示:代码跟之前那个组件冗余了,要不考虑放到一起吧,减少代码的重复。

我:好的!

于是第二个版本,我的思路是,在原本行内编辑的组件里实现2种模式,在index文件增加一个isBubble(是否气泡模式)的属性,传给这个单行编辑组件进行区分。思路有了,快速进行开发。

开发完成之后,再给大佬看,大佬沉默了。

大佬表示,她想要的不是在最低层去封装,最底层最好不动。

ok!于是第三个版本,我的思路就是在组件的index进行封装,方法都提取出来,底层的组件不再需要进行请求之类的操作,直接在index管理,类似这样:

import React, { useState, useEffect } from 'react';
import { Button } from 'antd';
import { FormOutlined } from '@ant-design/icons';
import { trimAllBlank } from '@/utils/tools';
// 业务组件
import EditableCellForm from './EditableCellForm';
import TheEditCellBubble from './TheEditCellBubble';
// css
import styles from './style.less';
// 类型定义
import { Props } from './index.type';
/**
* @description 可编辑单元格
* @param {object} props - 父级数据
* @returns {component}
*/
const TheEditTableCell: React.FC = (props) => {
const {
initValue,
record,
dataIndex,
placeholder,
verify,
ellipsis,
disabled,
textClassName,
inputType,
request,
update,
onEdit,
onCancel,
onTextClick,
isBubble,
} = props;
// 文本状态时显示的值
const [textValue, setTextValue] = useState(initValue);
// 可编辑状态
const [editable, setEditable] = useState(false);
// 输入框的值
const [inputValue, setInputValue] = useState('');
// 错误提示文案
const [errorText, setErrorText] = useState('');
// 错误提示文案展示状态控制
const [errorVisible, setErrorVisible] = useState(false);
// 确认按钮loading状态控制
const [loading, setLoading] = useState(false);
// 气泡展示状态控制
const [visible, setVisible] = useState(false);
const handleOk = async (value?: string) => {
if (value) {
// 输入内容校验不通过,直接return
if (!verifyInput(value)) return;
// 内容不变,直接return
if (inputValue === textValue) return;
// 保存展示内容
setTextValue(value);
// 如果是编辑状态,则关闭
if (editable) {
setEditable(false);
}
// 创建参数对象
const dataParams = dataIndex ? { [dataIndex]: inputValue } : {};
// 如需发送请求
if (request) {
try {
// 确认按钮loading状态开启
setLoading(true);
// 发起请求
const res: any = await request({ ...record, ...dataParams });
if (res && res.code === 0 ) {
// 保存展示内容
setTextValue(value);
setInputValue(value);
// 如需更新
if (update) update({ ...record, ...dataParams }, res.result);
// 关闭编辑框
if(visible) setVisible(false);
}
// 默认值不存在时一般是做为新建功能使用此组件, 默认会在成功后清空输入项
if (!initValue) setInputValue('');
setLoading(false);
} catch (error) {
setLoading(false);
} finally {
//
}
} else if (update) {
// 无需发送请求,则直接修改数据并返回
setTextValue(inputValue);
setInputValue(inputValue);
setVisible(false);
setLoading(false);
}
}
}
// 文本点击回调
const handleTextClick = () => {
if (onTextClick) onTextClick();
}
// 校验函数
const verifyInput = (val: any) => {
if (verify && verify.rules && verify.rules.length > 0) {
const error = verify.rules.find((el: any) => {
// 空验证
if (el.required) {
return !val;
}
// 正则验证
if (el.pattern) {
return !el.pattern.test(val);
}
// 自定义验证
if (el.validator) {
return !el.validator(val);
}
return false;
});
if (error) {
setErrorVisible(true);
setErrorText(error.message);
return false;
}
}
return true;
};
// 监听输入框实时内容
const handleChange = (e: { target: { value: string } }) => {
const val = e.target.value;
setInputValue(trimAllBlank(val));
verifyInput(val);
// 重置错误提示
if (errorVisible && verifyInput(val)) {
setErrorVisible(false);
setErrorText('');
}
};
// 取消-回调
const handleCancel =(e: React.MouseEvent)=>{
e.stopPropagation();
if(visible) setVisible(false);
if(editable) setEditable(false);
setInputValue(textValue);
}
// 点击打开气泡
const handleVisibleChange = () => {
setVisible(true);
};
// 监听初使值的变化
useEffect(() => {
if (initValue) {
setTextValue(initValue);
setInputValue(initValue);
}
}, [initValue]);
// 监听编辑状态的变化
useEffect(() => {
// 激活编辑回调
if (editable && onEdit) {
onEdit();
}
// 取消编辑回调
else if (onCancel) {
onCancel();
}
}, [editable]);
return (
<>
{!isBubble && !editable &&


{ellipsis ? (
title={textValue}
className="ads-single-ellipsis"
| &#39;-&#39;}

) : (

)}
{!disabled && (

}
{!isBubble && editable && !disabled &&
defaultValue={textValue}
inputValue={inputValue}
placeholder={placeholder}
verify={verify}
errorText={errorText}
errorVisible={errorVisible}
loading={loading}
isFocus={editable}
inputType={inputType}
handleOk={handleOk}
handleCancel={handleCancel}
handleChange={handleChange}
/>
}
{ isBubble &&
inputValue={inputValue}
showValue={textValue}
errorText={errorText}
errorVisible={errorVisible}
loading={loading}
visible={visible}
verify={verify}
handleChange={handleChange}
handleVisibleChange={handleVisibleChange}
handleOk={handleOk}
handleCancel={handleCancel}
/>
}

);
};
TheEditTableCell.defaultProps = {
ellipsis: false,
inputType: &#39;text&#39;,
};
export default TheEditTableCell;

  

ok实现!

于是再次提交代码,给大佬过目,然而大佬又一次沉默了。

这次沉默的原因是:大可以和index做成并列关系的组件,只是内部的输入框之类,可以直接调用之前已有的,用气泡包裹起来就好了。

我:……

我:好的,我相信这次一定没问题。

这次的思路就是,单独,与index并列,引用已有的底层组件,包一层popover。于是第四个版本诞生了:

&#160;

import React, { useState, useEffect } from &#39;react&#39;;
import { Popover, Button } from &#39;antd&#39;;
// 业务组件
import EditableCellForm from &#39;./EditableCellForm&#39;;
// 编辑icon
import { FormOutlined } from &#39;@ant-design/icons&#39;;
// 样式文件
import styles from &#39;./style.less&#39;;
// 类型定义
import { Props } from &#39;./index.type&#39;;
/**
* Single line edit bubble component【单行编辑气泡组件】
* author: wun
*/
const EditCellBubble: React.FC = (props) => {
const {
initValue,
record,
dataIndex,
placeholder,
verify,
ellipsis,
disabled,
textClassName,
inputType,
request,
update,
onEdit,
onCancel,
cRef,
} = props;

// 文本状态时显示的值
const [textValue, setTextValue] = useState(initValue);
// 编辑状态
const [editable, setEditable] = useState(false);
// 确定-回调
const handleOk = async (value?: string, params?: object, result?: any) => {
if (value) {
setTextValue(value);

// 更新父级数据
if (update) {
update(params, result);
}
}
if (editable) {
setEditable(false);
}
}
const handleVisibleChange = () => {
setEditable(!editable);
};
// 监听初使值的变化
useEffect(() => {
if (initValue) setTextValue(initValue);
}, [initValue]);
// 监听编辑状态的变化
useEffect(() => {
// 激活编辑回调
if (editable && onEdit) { onEdit(); }
// 取消编辑回调
else if (onCancel) { onCancel(); }
}, [editable]);
return (


{!disabled &&
}trigger="click"visible={editable}OnVisibleChange={handleVisibleChange}>
{textValue || &#39;&#39;}{!disabled && (

}
{disabled &&
{textValue || &#39;&#39;}
}

);
};
EditCellBubble.defaultProps = {
ellipsis: false,
inputType: &#39;text&#39;,
};
export default EditCellBubble;

  

&#160;

原来扩展个小破组件,这么难,暴风落泪。

&#160;


推荐阅读
author-avatar
mobiledu2502931957
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有