【react+ts+antd】开发一个单行编辑气泡组件的血泪史
作者:互联网
首先接到的任务是这样的:
那么打开参考对象看一眼:
总结一下组件的内容和功能点:
1.一个输入框,两个按钮(确定,取消)
2.点击文本,弹出气泡,进行编辑,提交/取消,关闭气泡,更新数据(数据不变则不更新)
而原本的组件,则是直接点击编辑按钮,变为编辑模式:
因此,我选择了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> = (props) => { const { inputType, initValue, record, dataIndex, placeholder, verify, className, request, update, cRef, } = props; // 输入框ref const inputRef = useRef<any>(null); // 输入框的值 const [inputValue, setInputValue] = useState<string>(''); // 单行展示的值 const [showValue, setShowValue] = useState<string>(''); // 错误提示文案 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 ( <div className={`${styles['c-edit_cell-bubble']}${className ? ` ${className}` : ''}`}> <Popover placement="bottom" content={ <div> <div className={`${styles['c-edit_cell-bubble-content']}`}> <Input ref={inputRef} value={inputValue} placeholder={placeholder} maxLength={(verify && verify.maxLength) || 50} onChange={handleChange} onPressEnter={handleOk} type={inputType} className={`${errorVisible && styles['c-edit_cell-bubble-input-error']}`} /> <Button type="primary" onClick={handleOk} loading={submitLoading}>确定</Button> <Button onClick={handleCancel}>取消</Button> </div> {errorVisible && <div className={`${styles['c-edit_cell-bubble-error-tips']}`}><CloseCircleOutlined className={`${styles['c-edit_cell-bubble-error-icon']}`}/>{errorText}</div>} </div> } trigger="click" visible={visible} onVisibleChange={handleVisibleChange} getPopupContainer={(triggerNode) => triggerNode} // 改变浮层渲染父节点 > <Button type="text">{showValue}</Button> </Popover> </div> ); }; 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<any>; // 更新单元格数据接口 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'; ## 调用 `` <BasisTheEditCellBubble /> `` ## 属性参考 index.type.ts文件 ########### 示例参考 [可替换掉项目管理的BasisEditTableCell组件用以体验] `` <BasisTheEditCellBubble 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> = (props) => { const { initValue, record, dataIndex, placeholder, verify, ellipsis, disabled, textClassName, inputType, request, update, onEdit, onCancel, onTextClick, isBubble, } = props; // 文本状态时显示的值 const [textValue, setTextValue] = useState<string | undefined>(initValue); // 可编辑状态 const [editable, setEditable] = useState(false); // 输入框的值 const [inputValue, setInputValue] = useState<string | undefined>(''); // 错误提示文案 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 && <div className={`${styles['c-editcell-text']}${textClassName ? ` ${textClassName}` : ''}`}> {ellipsis ? ( <div title={textValue} className="ads-single-ellipsis" style={onTextClick ? { cursor: 'pointer' } : { width: '100%' }} onClick={handleTextClick} > {textValue || '-'} </div> ) : ( <span style={onTextClick ? { cursor: 'pointer' } : undefined} onClick={handleTextClick}> {textValue || '-'} </span> )} {!disabled && ( <Button type="link" icon={<FormOutlined />} onClick={() => setEditable(true)} /> )} </div> } {!isBubble && editable && !disabled && <EditableCellForm 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 && <TheEditCellBubble 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: 'text', }; export default TheEditTableCell;
ok实现!
于是再次提交代码,给大佬过目,然而大佬又一次沉默了。
这次沉默的原因是:大可以和index做成并列关系的组件,只是内部的输入框之类,可以直接调用之前已有的,用气泡包裹起来就好了。
我:……
我:好的,我相信这次一定没问题。
这次的思路就是,单独,与index并列,引用已有的底层组件,包一层popover。于是第四个版本诞生了:
import React, { useState, useEffect } from 'react'; import { Popover, Button } from 'antd'; // 业务组件 import EditableCellForm from './EditableCellForm'; // 编辑icon import { FormOutlined } from '@ant-design/icons'; // 样式文件 import styles from './style.less'; // 类型定义 import { Props } from './index.type'; /** * Single line edit bubble component【单行编辑气泡组件】 * author: wun */ const EditCellBubble: React.FC<Props> = (props) => { const { initValue, record, dataIndex, placeholder, verify, ellipsis, disabled, textClassName, inputType, request, update, onEdit, onCancel, cRef, } = props; // 文本状态时显示的值 const [textValue, setTextValue] = useState<string | undefined>(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 ( <div className={`${styles['c-edit_cell-bubble']}}`}> {!disabled && <Popover placement="bottom" content={ <div className={`${styles['c-edit_cell-bubble-content']}${inputType === 'number' ? ` ${styles['c-edit_cell-bubble-content-number']}` : ''}`}> <EditableCellForm cRef={cRef} defaultValue={textValue} placeholder={placeholder} verify={verify} serverOptions={{ params: record, dataIndex, onRequest: request }} isFocus={editable} inputType={inputType} onOk={handleOk} onCancel={handleVisibleChange} /> </div> } trigger="click" visible={editable} onVisibleChange={handleVisibleChange} > <div className={`${styles['c-edit_cell-bubble-value']}${textClassName ? ` ${textClassName}` : ''}${ellipsis ? ` c-edit_cell-bubble-ellipsis` : ''}`} > {textValue || ''}{!disabled && ( <Button type="link" icon={<FormOutlined />} onClick={() => setEditable(true)} /> )} </div> </Popover>} {disabled && <div className={`${styles['c-edit_cell-bubble-value c-edit_cell-bubble-value-disabled']}${textClassName ? ` ${textClassName}` : ''}${ellipsis ? ` c-edit_cell-bubble-ellipsis` : ''}`}>{textValue || ''}</div>} </div> ); }; EditCellBubble.defaultProps = { ellipsis: false, inputType: 'text', }; export default EditCellBubble;
原来扩展个小破组件,这么难,暴风落泪。
标签:initValue,false,ts,血泪史,react,&&,import,return,const 来源: https://www.cnblogs.com/nangras/p/14708049.html