【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;

  

 

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

 

posted @ 2021-04-27 13:20  芝麻小仙女  阅读(630)  评论(0编辑  收藏  举报