likes
comments
collection
share

使用react开发一个功能完备的Pop组件,支持命名空间

作者站长头像
站长
· 阅读数 14

大家好,我是苏先生,一名热爱钻研、乐于分享的前端工程师,跟大家分享一句我很喜欢的话:人活着,其实就是一种心态,你若觉得快乐,幸福无处不在

前言

本文是屎山代码优化系列的第二篇文章,本文将为大家提供开发并实现一个Pop组件的核心思路

头脑风暴 🤔

  • 首先,弹窗的功能antd已经为我们提供了,因此即使是定式化的ui,我个人也不建议从0到1开发,而是应该使用gloal去覆盖重写,故我们这里借助antd的Modal来实现功能

使用react开发一个功能完备的Pop组件,支持命名空间

  • 从布局上来讲一个弹窗可以被分为头部、主体、底部三部分

1.头部一般由标题和关闭按钮组成,而标题部分应该是可变的

2.主体一般都是一个或者多个form表单用于收集或者展示用户信息,因此,form表单应该被内置,而form表单的每一项则应该作为动态的值由外部控制

3.底部一般只会包括一个或多个操作按钮,这一部分ant的Modal已经给我们提供了定制化属性我们只需要进行透传就可以了

  • 至于命名空间,其实就是一个一个的表单,它们相互独立互不干扰,使用map或者数组进行管理即可

代码实现

props定义

结合实际开发业务需求最终设计出的props如下

interface Ipop {
  visible: boolean;
  title: string;
  columns?: TformItem[];
  initialValue?: Record<string, any> | Record<string, any>[];
  width: number;
  height?: number;
  labelCol?: {
    span?: number;
    [key: string]: any;
  };
  prefixCls?: string;
  modalConfig?: any;
  multiTitle?: string;
  restComponent?: React.ReactNode;
  outComponet?: (
    formIndex: number,
    initialValue: any | any[]
  ) => React.ReactNode;
  onOk?: (values: any) => boolean | Promise<any>;
  onCancel?: (
    clickType?: boolean,
    values?: Record<string, any> | any[]
  ) => void;
}
  • visible

用于控制Pop组件的显示或隐藏

  • title

表单标题

  • columns

具体的表单项数组,考虑到不同命名空间下的key理论上不可能重复,我们这里设计的是一个一维数组

type TformItem = {
  label: string;
  name: string;
  component?: React.ReactNode;
  rules?: any[];
};
  • initialValue

考虑到编辑表单初始值回填和命名空间的问题,我们这里将其设计为数组对象或纯对象

  • multiTitle

在我们的业务中,每一个命名空间下的表单都有一个小标题,它们拥有着共同的前缀,但是使用数字1,2,3进行递增

  • width、height

用于控制表单的宽高

  • prefixCls

为客制化样式提供类名标记

  • modalConfig

其他的antd的Modal的支持属性

  • restComponent

在表单项之外的任意项

  • outComponet

表单项的具体渲染内容

  • onOk和onCancel

确认和取消按钮的回调

代码实现

第一步就是根据initialValue参数的类型进行对初始值设置,包括弹窗关闭的重置处理,如下,分别从表单对应调用resetFields或setFieldsValue即可

 useEffect(() => {
    if (!visible) {
      if (Array.isArray(formRef.current)) {
        formRef.current.forEach((v) => {
          v?.resetFields();
        });
      }
    } else {
      if (initialValue) {
        if (Array.isArray(initialValue)) {
          initialValue.forEach((v, i) => {
            if (
              Object.prototype.toString.call(v) === "[object Object]" &&
              Object.keys(v).length
            ) {
              queueMicrotask(() => {
                formRef.current[i]?.setFieldsValue(v);
              });
            }
          });
        } else {
          if (
            Object.prototype.toString.call(initialValue) ===
              "[object Object]" &&
            Object.keys(initialValue).length
          ) {
            queueMicrotask(() => {
              formRef.current[0]?.setFieldsValue(initialValue);
            });
          }
        }
      }
    }
}, [visible, initialValue]);

第二步是处理表单的渲染

由于有命名空间的概念,而initialValue为数组时恰好与命名空间一一对应,故我们对initialValue进行遍历即可

const renderForm = () => {
    if (Array.isArray(initialValue)) {
      return initialValue.map((_, i) => {
        return (
          <>
            <div className={styles.pop_multiTitle}>
              {multiTitle}
              {i + 1}
            </div>
            {_renderForm(i)}
            {i != initialValue.length - 1 ? (
              <div className={styles.pop_line} />
            ) : null}
          </>
        );
      });
    }
    return _renderForm(0);
};

而_renderForm即是简单的生成一个antd的Form组件

const _renderForm = (i: number) => {
    return (
      <Form
        labelCol={labelCol}
        onFinish={handleOk}
        labelAlign="left"
        form={form[i]}
        layout="horizontal"
        colon={false}
        ref={(form) => (formRef.current[i] = form)}
      >
        {renderItem(columns!, false, i)}
      </Form>
    );
};

最后renderItem则处理每一个表单项的创建,主要是对不同分支的判断处理,由于我们支持一行一个或者一个多个的情况,所以还会有分组的能力,比如isGroup则是为此单独打的类名标签

const renderItem = (arr: any[], isGroup?: boolean, formIndex?: number) => {
    return arr.map((v, i) => {
      if (Array.isArray(v)) {
        return (
          <div className={styles.pop_group} key={i}>
            {renderItem(v, true, formIndex)}
          </div>
        );
      }
      const klass = classNames({
        [styles.pop_group_item]: isGroup,
        [`${prefixCls ? prefixCls : ""}`]: true,
        [`${prefixCls ? prefixCls + "_" : ""}pop_group_item_${i}`]: isGroup,
      });
      return (
        <Form.Item
          labelCol={v.labelCol}
          rules={v.rules}
          label={v.label === "$null$" ? " " : v.label}
          name={v.name}
          key={v.name}
          className={klass}
        >
          {typeof outComponet === "function" && v.name === "$useOutComponent$"
            ? outComponet(formIndex!, initialValue)
            : v.component}
        </Form.Item>
      );
    });
};

最后处理下新增和取消按钮,它们很简单,就是获取到外部传入的接口进行调用即可

const handleCancel = (e?: any) => {
    const isClickBtn = e?.pageY > 500;
    if (typeof onCancel === "function") {
      exexLoading();
      onCancel(isClickBtn, getFormValues());
    }
};

使用示例

引入并按照相关的参数进行设置

<Pop
    ...
/>

比如当需要进行编辑预览时,可以使用useState来进行状态管理

const [initialValue, setInitialValue] = useState<any>({});

然后在获取到对应的预览内容后进行设置即可

selectPermit(params).then((res: any) => {
      ...
      setInitialValue({
        ...da,
        auditId: list[0]?.id,
      });
      ...
});

总结

接下来,笔者会继续分享关于多级Table的封装与实现思路,还有利用webpack-loader对面包屑进行调用层面的优化,另外,笔者已经很久没有主力开发过react了,因此对于react的技术储备更新不及时,接下来打算对react的新的api或能力进行学习。感兴趣的可以关注笔者,我们一起学习一起进步🌹


如果本文对您有用,希望能得到您的点赞和收藏

订阅专栏,每周更新2-3篇类型体操,每月1-3篇vue3源码解析,等你哟😎


文章推荐