手把手教你实现一个数据驱动的表单引擎

我正在参加「掘金·启航计划」

一、背景

表单在前端开发场景中是非常常见的一个功能,在很多需要用户输入的地方都要用到,而通常大多数简单的表单手写时又有较多重复的工作,因此这里我们写一个简单好用体积小的表单引擎,用来快速配置出简单的表单,提高表单开发效率。

在之前的图形编辑器系列文章svg实现图形编辑器系列十:工具栏&配置面板里,我们在最后也提到了为了扩展图形的属性配置项,需要使用到表单配置面板,那么表单引擎就可以用来实现这样的配置面板。

首先我们看一下效果图:

手把手教你实现一个数据驱动的表单引擎

可以看到配置数据中每个表单项都被抽象成了一个描述对象,渲染时就会变成输入框、下拉框等表单项,使用非常简单,仅需要关注必要的配置属性即可。

名词解释

名词 含义
setter 设置器,也即输入组件,就是用来生产数据的,可以是Input、Select、Checkbox等
field 字段

二、实现原理

1. 输入组件(Setter)的抽象

想要实现用数据来生成表单,用逆向思维来考虑,首先我们需要将各种输入方式抽象成为数据

以最典型的输入框为例,我们先分析下输入框通常都有哪些用户常用的配置:

  • 输入框在表单里对应的属性名是什么
  • 标题
  • 输入框默认值
  • 占位提示符
  • 是否必填
  • 是否禁用
  • 提示信息
  • 最大输入长度
  • 其他…

再以下拉框为例和输入框进行对比,下拉框用户常用的配置有:

  • 输入框在表单里对应的属性名是什么
  • 标题
  • 输入框默认值
  • 占位提示符
  • 是否必填
  • 是否禁用
  • 提示信息
  • 选项列表
  • 其他…

对比可以很容易的发现有些属性配置是相同的,有些属性配置是不同的,例如输入框需要最大输入长度,而下拉框不需要;下拉框需要选项列表,而输入框则不需要。

因此我们将通用的属性配置抽象在首层,每个设置器(Setter)独有的属性放在setterProps中,因此输入框和下拉框的描述数据分别为:

// 输入框配置
const inputConfig = {
  name: 'username',
  setter: 'input',
  title: '用户名',
  placeholder: '请输入',
  tips: '这是输入框',
  defaultValue: '张三',
  setterProps: {
    maxLength: 20,
  },
};

// 下拉框配置
const selectConfig = {
  name: 'role',
  setter: 'select',
  title: '角色',
  placeholder: '请选择',
  tips: '这是下拉框',
  defaultValue: 'citizen',
  setterProps: {
    options: [
      { label: '普通用户', value: 'citizen' },
      { label: '管理员', value: 'admin' },
      { label: '会员', value: 'member' },
      { label: '内部用户', value: 'internal_user' },
    ],
  },
};

按照上面的规则,我们就可以很方便的继续写出别的类型的输入组件了,如:

  • 数字输入框(InputNumber)
  • 文本域(TextArea)
  • 单选框(Radio)
  • 多选框(Checkbox)
  • 开关(Switch)
  • 开关(Switch)
  • 日期(Date)
  • 时间(Time)
  • 其他…

最后我们给出描述的接口interface

/**
 * 字段配置接口描述
 */
interface IFieldSetting {
  /**
   * 字段属性
   */
  name: string;
  /**
   * 字段标题
   */
  title: string;
  /**
   * 输入组件类型,可以是名字,也可以直接是一个组件
   */
  setter: string | ISetterComp;
  /**
   * 输入组件属性,可以是名字,也可以直接是一个组件
   */
  setterProps?: Record<string, any> & IFieldSetterProps;
  /**
   * 默认值
   */
  defaultValue?: any;
  /**
   * 是否禁用
   */
  disabled?: boolean;
  /**
   * 是否只读
   */
  readOnly?: boolean;
  /**
   * 提示文案
   */
  tips?: string;
  /**
   * 是否必填
   */
  required?: boolean;
  /**
   * 是否显示
   */
  condition?: boolean;
  /**
   * 布局模式,水平或者垂直
   */
  layout?: 'horizontal' | 'vertical';
}

将输入组件进行抽象后,我们就可以很容易写出表单的字段配置,以按钮的属性配置表单为例,配置项如下:

const buttonFieldConfig = [
  {
    name: 'content',
    title: '文字',
    setter: 'input',
    defaultValue: '按钮',
    required: true,
  },
  {
    name: 'size',
    title: '尺寸',
    setter: 'radio',
    defaultValue: 'middle',
    setterProps: {
      options: [
        { label: '大', value: 'large' },
        { label: '中', value: 'middle' },
        { label: '小', value: 'small' },
      ],
    },
  },
  {
    name: 'type',
    title: '类型',
    setter: 'select',
    setterProps: {
      options: [
        { label: '默认', value: 'default' },
        { label: '主要', value: 'primary' },
        { label: '背景透明', value: 'ghost' },
        { label: '虚线', value: 'dashed' },
        { label: '链接', value: 'link' },
        { label: '文本', value: 'text' },
      ],
    },
  },
    htmlType: {
      type: 'string',
      title: '原生按钮类型',
      tips: '设置 button 原生的 type 值',
      enum: [
        { label: '默认', value: 'button' },
        { label: '提交', value: 'submit' },
        { label: '重置', value: 'reset' },
      ],
    },
  {
    name: 'disabled',
    title: '禁用',
    setter: 'switch',
  },
  {
    name: 'loading',
    title: '加载中',
    setter: 'switch',
  },
]

手把手教你实现一个数据驱动的表单引擎

2. 配置数据渲染为表单项

首先是表单字段循环渲染:

  • 主要职责就是把每个字段的配置进行拆分,分别分配到表单项(FormItem)输入组件(Setter)的属性中去
import { Form } from 'antd';

export const FormEngine = ({
  fieldSettings = [],
  onFieldChange,
  onValuesChange,
  setterConfigMap,
}: IFormEngineProps) => {
  const [formValues, setFormValues] = useState<Record<string, any>>(
    collectDefaultValues(fieldSettings),
  );
  const handleChange = (value: any, name: string) => {
    const newValues = { ...formValues, [name]: value };
    setFormValues(newValues);
    onFieldChange?.(value, name);
    onValuesChange?.(newValues);
  };
  return (
    <Form>
      {/** 循环渲染字段 */}
      {fieldSettings.map(fieldSetting => {
        const {
          name,
          title,
          tips,
          layout,
          setter,
          setterProps,
          required,
          disabled,
          readOnly,
        } = fieldSetting;
        const value = formValues[name];
        return (
          {/** 表单项 */}
          <Form.Item
            key={name}
            name={name}
            layout={layout}
            label={title}
            required={required}
            tooltip={tips}>
            {/** 输入组件渲染器 */}
            <SetterRender
              setter={setter}
              setterProps={setterProps}
              setterConfigMap={setterConfigMap}
              disabled={disabled}
              readOnly={readOnly}
              value={value}
              onChange={val => handleChange(val, name)}
            />
          </Form.Item>
        );
      }}
    </Form>
  );
};


其中,输入组件(Setter)的渲染器如下:

  • 主要职责就是把根据setter名字找到setter的组件渲染出来
export const SetterRender = ({
  type,
  setter,
  setterProps = {},
  setterConfigMap,
  readOnly,
  disabled,
  value,
  onChange,
}: IProps) => {
  const SetterComp = React.useMemo(
    () => getSetterComp({ type, setter, setterConfigMap, readOnly }),
    [type, readOnly, setter, setterConfigMap],
  );
  return (
    <SetterComp
      {...setterProps}
      disabled={disabled}
      value={value}
      onChange={onChange}
    />
  );
};

3. 扩展输入组件(Setter),自定义输入组件

输入组件(Setter)就相当于是表单引擎的原材料,最好是支持扩展的,这样才能实现五花八门的丰富功能,因此这里我们介绍下如何写Setter

对setter输入组件的最基本要求只有2个,就是接受valueonChange 两个参数。

首先看一下简单的输入组件(Setter)如何写:

3.1 最基本的setter

import { Input, Select } from 'antd';

interface ISetterProps {
  value: any;
  onChange: (value: any) => void;
}

// 输入框
const InputSetterConfig: ISetterConfig = {
  name: 'input',
  setter: Input,
}

// 下拉框
const SelectSetterConfig: ISetterConfig = {
  name: 'select',
  setter: Select,
}

是不是非常简单。

3.2 定制setter值变化时机

如果你希望在失去焦点、按下回车或者其他时机触发值变化,那么你可以这样写:

// 输入框
const InputSetterConfig: ISetterConfig = {
  name: 'input',
  setter: ({ value, onChange, ...rest }: ISetterProps) => {
    const [val, setVal] = useState(value);
    useEffect(() => setVal(value), [value]);
    return (
      <Input
        {...rest}
        value={val}
        onChange={setVal}
        onBlur={(e) => onChange(e.target.value)}
        onPressEnter={(e) => onChange(e.target.value)}
      />
    );
  },
}

3.3 定制其他业务setter

其他定制的setter输入组件如:

  • 颜色输入
  • 邮箱输入
  • 搜索框
  • 手机号输入

这里我们实现一个颜色输入框,输入框可以显示颜色矩形,也可以使用输入框编辑16进制颜色值。

// 字段配置
const inputFieldSetting: IFieldSetting = {
  name: 'color',
  setter: 'color',
  title: '颜色',
}

// 颜色输入框setter
const InputSetterConfig: ISetterConfig = {
  name: 'color',
  setter: ({ value, onChange, ...rest }: ISetterProps) => {
    return (
      <Input
        {...rest}
        value={value}
        onChange={onChange}
        prefix={<ColorBlock color={value} />}
      />
    );
  },
}

// 矩形色块组件
const ColorBlock = ({ color }: { color: string }) => (
  <div
    style={{
      width: 16,
      height: 16,
      backgroundColor: color,
    }}
  />
);

点击矩形色块时可以打开弹框显示颜色选择器,颜色选择器的实现见我的另外一篇文章实现超好用的React颜色选择器组件

颜色输入框的效果图如下:

手把手教你实现一个数据驱动的表单引擎

4. 一些工具函数:

/**
 * 根据字段中的setter配置获取setter输入组件
 */
export const getSetterComp = ({
  setter,
  setterConfigMap,
}: {
  type: string | ValueType;
  setter?: string | React.FC<any>;
  setterConfigMap: Record<string, ISetterConfig>;
}): React.FC<ISetterProps> => {
  if (typeof setter === 'function') {
    return setter;
  } else if (typeof setter === 'string' && setterConfigMap[setter]) {
    return setterConfigMap[setter].setter;
  } else {
    return () => (
      <div style={{ backgroundColor: 'red', color: '#fff', padding: '0 8px' }}>
        Setter「{setter || type}」不存在
      </div>
    );
  }
};

/**
 * 从字段配置中收集表单默认值
 */
export const collectDefaultValues = (fieldSettings: IFieldSetting[]) => {
  const values: Record<string, any> = {};
  fieldSettings.forEach(field => {
    if (field.hasOwnProperty('defaultValue')) {
      values[field.name] = field.defaultValue;
    }
  });
  return values;
};

这样一来我们就得到了一个简单的表单渲染引擎,虽然功能较为简单,但包含了表单渲染的核心流程, 麻雀虽小五脏俱全

三、总结

本文介绍了如何使用配置数据的方式来快速生成一个表单,使用时只需要关注必要的配置字段,不用重复书写代码,可以在简单表单的场景下大大提高开发效率。

同时,为了方便理解,本文介绍的是一个简单的渲染主流程,表单还有校验联动等等相对复杂一些的表单功能,我们会在后续的文章中继续介绍。

其他能力规划:

  • 表单校验
  • 表单联动
  • 表单布局(垂直、水平、行内等)
  • 表单额外操作(Action)
  • 自定义表单内容渲染

最后,我们如果不想手写配置数据,彻底解放双手,或者向让不动写代码的运营、产品、设计等角色使用的话,可以做一个表单编辑器,用拖拽配置的方式实现一个表单编辑器,生产出上面的字段配置数据,丢给表单渲染引擎即可完成渲染。

原文链接:https://juejin.cn/post/7248982532728963129 作者:前端君

(0)
上一篇 2023年6月27日 上午10:27
下一篇 2023年6月27日 上午10:37

相关推荐

发表回复

登录后才能评论