H5的Form表单项不够灵活怎么办?来看看这篇通用组件封装思路分享

前言

公司用的 react + antd 的技术栈。因为antd自带的组件并不是很灵活。所以团队封装了了类似FormItem表单项的组件,方便使用。

比如,公司有下面的交互

这个页面,有一个渠道表单项,点击它,弹出下拉列表

并支持搜索

选择以及后续打开回填

渠道筛选项TXChannelPickerItem

首先该组件的使用直接套用在 Form.Item 里面即可,值直接由Form表单记录,使用的时候可以抛去心智负担

tsx 复制代码
    <Form.Item
      name="channelId"
      className="required_form_item"
      label="渠道"
      rules={[{ required: true, message: "请选择渠道" }]}
      clickable
    >
      <TXChannelPickerItem />
    </Form.Item>
  );

TXChannelPickerItem 里面分为两部分,一部分就是表单项的形式,一部分就是底部弹出层

tsx 复制代码
export interface ITXChannelPickerItemProps {
  value?: ITreeItem[];
  onChange?: (v?: ITreeItem[]) => void;
  placeholder?: string;
  disabled?: boolean;
  className?: string;
  transformData?: (value: TRecord[]) => IOption[];
  /** @param 额外参数 */
  extraReq?: Partial<IReqBusinessV1ChannelGetPage>;
  /**@param 初始化时,是否需要回填任意一条数据 */
  needBackFill?: boolean;
  }

export const TXChannelPickerItem = function TXChannelPickerItem_(props: ITXChannelPickerItemProps) {
  return (
    <>
      {/* 表单项内容  */}
      <div></div>
      {/* 底部弹出层内容 */}
      <ChannelPicker/>
    </>
  );
};

先看表单项内容

tsx 复制代码
<>
  {/* 上 */}
  <div
    className=" absolute top-0 left-0 w-full h-full opacity-0"
    onClick={() => {
      if (disabled) {
        return;
      }
      channelRef.current?.openModal({
        initValue: value,
      });
    }}
  ></div>
  {/* 下 */}
  <div className={className}>
    <div className="w-max max-w-full overflow-hidden flex">
      <span
        className={classNames({
          "text-[#ccccd6]": true,
          hidden: !!text,
        })}
      >
        {placeholder}
      </span>
      <div
        className={classNames({
          "flex-1 wes": true,
          hidden: !text,
        })}
      >
        {text}
      </div>
    </div>
  </div>
</>;

上部分是用绝对定位盖在了筛选项上,同时满足一些情况下禁用的业务逻辑

下部分就是placeHolder和选择的值

然后再来看 弹出层内容

tsx 复制代码
const channelRef = useRef<IChannelPickerRef>(null);
<ChannelPicker
  {...rest}
  ref={channelRef}
  onSelect={(_, full) => {
    onChange?.(full);
  }}
  extraReq={{
    enableFlag: true,
    ...extraReq,
  }}
/>;

弹出层用的是antd的PickerView或者CascaderView实现的,弹出层组件内部会维护状态和相关业务逻辑。然后直接通过props将状态向父组件暴露

继续看ChannelPicker内部是怎么划分的

tsx 复制代码
// 选广告渠道
import { observer, useSyncProps, useWhen } from "@quarkunlimit/qu-mobx";
import { Popup } from "antd-mobile";
import { forwardRef, useImperativeHandle } from "react";
import { IChannelPickerProps, IChannelPickerRef } from "./interface";
import { PickerContent } from "./modules/PickerContent";
import { SearchRow } from "./modules/SearchRow";
import { TopOption } from "./modules/TopOption";
import { Provider, useStore } from "./store/RootStore";

const ChannelPicker = observer(
  forwardRef<IChannelPickerRef, IChannelPickerProps>(
    function ChannelPicker_(props, ref) {
      const root = useStore();
      useSyncProps(root, Object.keys(props), props);
      const { logic } = root;

      useImperativeHandle(ref, () => {
        return {
          openModal: logic.openModal,
          closeModal: logic.closeModal,
        };
      });

      useWhen(
        () => true,
        () => {
          logic.init();
        }
      );

      return (
        <Popup
          visible={logic.open}
          bodyStyle={{
            height: "70vh",
            display: "flex",
            flexDirection: "column",
          }}
          onMaskClick={logic.closeModal}
        >
          <TopOption />
          <SearchRow />
          <PickerContent />
        </Popup>
      );
    }
  )
);

export default observer(
  forwardRef<IChannelPickerRef, IChannelPickerProps>(
    function ChannelPickerPage(props, ref) {
      return (
        <Provider>
          <ChannelPicker {...props} ref={ref} />
        </Provider>
      );
    }
  )
);
  • 初始化时 init 调接口
  • DOM结构分为顶部操作按钮、搜索框、数据源

其中 PickerContent 是通过CascaderView实现, 当然你也可以使用 PickerView 实现。PickerView还可以自定义渲染每一行的内容。而CascaderView只能通过 options 配置项形式渲染

tsx 复制代码
import { observer } from "@quarkunlimit/qu-mobx";
import { useStore } from "../store/RootStore";
import { CascaderView } from "antd-mobile";

export const PickerContent = observer(function PickerContent_() {
  const root = useStore();
  const { logic, computed } = root;
  return (
    <CascaderView
      style={{
        "--height": "calc(70vh - 130px)",
      }}
      className="flex-1"
      value={logic.value}
      options={logic.dataSource}
      loading={computed.loading}
      onChange={logic.onChangeValue}
    />
  );
});

至此,一个类似于Select交互操作的H5表单项就封装完成了。需要使用时,直接套用在Form.Item里面即可。

公司其余的组件也是这样的方式实现的

整体代码

tsx 复制代码
import ChannelPicker from "@/pages/Picker/ChannelPicker";
import { IChannelPickerRef } from "@/pages/Picker/ChannelPicker/interface";
import { IReqBusinessV1ChannelGetPage } from "@/service/business/v1/channel/get-page";
import { IOption, ITreeItem, TRecord } from "@/utils/interface";
import { classNames } from "@/utils/tools";
import { useRef } from "react";

export interface ITXChannelPickerItemProps {
  value?: ITreeItem[];
  onChange?: (v?: ITreeItem[]) => void;
  placeholder?: string;
  disabled?: boolean;
  className?: string;
  transformData?: (value: TRecord[]) => IOption[];
  /** @param 额外参数 */
  extraReq?: Partial<IReqBusinessV1ChannelGetPage>;
  /**@param 是否需要回填任意一条数据 */
  needBackFill?: boolean;
}

export const TXChannelPickerItem = function TXChannelPickerItem_(
  props: ITXChannelPickerItemProps
) {
  const {
    value = [],
    onChange,
    placeholder = "请选择渠道",
    disabled,
    className,
    extraReq,
    ...rest
  } = props;
  const channelRef = useRef<IChannelPickerRef>(null);

  let text = "";
  if (value.length > 0) {
    text = value[value.length - 1].label;
  }

  return (
    <>
      <div
        className=" absolute top-0 left-0 w-full h-full opacity-0"
        onClick={() => {
          if (disabled) {
            return;
          }
          channelRef.current?.openModal({
            initValue: value,
          });
        }}
      ></div>
      <div className={className}>
        <div className="w-max max-w-full overflow-hidden flex">
          <span
            className={classNames({
              "text-[#ccccd6]": true,
              hidden: !!text,
            })}
          >
            {placeholder}
          </span>
          <div
            className={classNames({
              "flex-1 wes": true,
              hidden: !text,
            })}
          >
            {text}
          </div>
        </div>
      </div>
      <ChannelPicker
        {...rest}
        ref={channelRef}
        onSelect={(_, full) => {
          onChange?.(full);
        }}
        extraReq={{
          enableFlag: true,
          ...extraReq,
        }}
      />
    </>
  );
};
tsx 复制代码
// 选广告渠道
import { observer, useSyncProps, useWhen } from "@quarkunlimit/qu-mobx";
import { Popup } from "antd-mobile";
import { forwardRef, useImperativeHandle } from "react";
import { IChannelPickerProps, IChannelPickerRef } from "./interface";
import { PickerContent } from "./modules/PickerContent";
import { SearchRow } from "./modules/SearchRow";
import { TopOption } from "./modules/TopOption";
import { Provider, useStore } from "./store/RootStore";

const ChannelPicker = observer(
  forwardRef<IChannelPickerRef, IChannelPickerProps>(
    function ChannelPicker_(props, ref) {
      const root = useStore();
      useSyncProps(root, Object.keys(props), props);
      const { logic } = root;

      useImperativeHandle(ref, () => {
        return {
          openModal: logic.openModal,
          closeModal: logic.closeModal,
        };
      });

      useWhen(
        () => true,
        () => {
          logic.init();
        }
      );

      return (
        <Popup
          visible={logic.open}
          bodyStyle={{
            height: "70vh",
            display: "flex",
            flexDirection: "column",
          }}
          onMaskClick={logic.closeModal}
        >
          <TopOption />
          <SearchRow />
          <PickerContent />
        </Popup>
      );
    }
  )
);

export default observer(
  forwardRef<IChannelPickerRef, IChannelPickerProps>(
    function ChannelPickerPage(props, ref) {
      return (
        <Provider>
          <ChannelPicker {...props} ref={ref} />
        </Provider>
      );
    }
  )
);
相关推荐
文心快码BaiduComate15 小时前
5句话让文心快码实现一个大模型MBTI测试器
前端·后端·llm
橙某人15 小时前
💫分享一个CSS技巧:用径向渐变实现弯曲框缺口效果
前端·css
颜酱15 小时前
基于 Ant Design 的配置化表单开发指南
前端·javascript·react.js
anyup15 小时前
uni-app 项目创建方式有哪些,看这一篇就够了!
前端·vue.js·uni-app
大猫会长15 小时前
react用useImages读取图片,方便backgroundImage
开发语言·前端·javascript
柯南二号15 小时前
【大前端】React 父子组件通信、子父通信、以及兄弟(同级)组件通信
前端·javascript·react.js
pepedd86415 小时前
突破 JS 单线程限制:Web Worker 实战指南
前端·javascript
加个鸡腿儿15 小时前
异步函数中return与catch的错误处理
前端·javascript
黑狼传说15 小时前
深入探索V8引擎的编译机制:从JavaScript到机器码的完整之旅
前端·javascript·v8