如何封装一个配置式的表单

334 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情

前言

在常见的业务里,表单往往是复杂且繁琐的功能,通常代码里会充斥着大量的冗余模板。我们可以运用抽离、封装的思想,尝试对冗余的模板进行封装,尝试用配置式的手法去驱动表单模板,实现配置式表单的封装。

以下示例基于antd封装

痛点

常见业务表单的痛点往往是冗余的模板代码,一块小小的功能可能涉及到大量的代码处理,在视觉上并不友好,如下列antd官网的表单代码:

import { Button, Form, Input, Select } from 'antd';
import type { FormInstance } from 'antd/es/form';
import React from 'react';

const { Option } = Select;

const layout = {
  labelCol: { span: 8 },
  wrapperCol: { span: 16 },
};
const tailLayout = {
  wrapperCol: { offset: 8, span: 16 },
};

class App extends React.Component {
  formRef = React.createRef<FormInstance>();

  onGenderChange = (value: string) => {
    switch (value) {
      case 'male':
        this.formRef.current!.setFieldsValue({ note: 'Hi, man!' });
        return;
      case 'female':
        this.formRef.current!.setFieldsValue({ note: 'Hi, lady!' });
        return;
      case 'other':
        this.formRef.current!.setFieldsValue({ note: 'Hi there!' });
    }
  };

  onFinish = (values: any) => {
    console.log(values);
  };

  onReset = () => {
    this.formRef.current!.resetFields();
  };

  onFill = () => {
    this.formRef.current!.setFieldsValue({
      note: 'Hello world!',
      gender: 'male',
    });
  };

  render() {
    return (
      <Form {...layout} ref={this.formRef} name="control-ref" onFinish={this.onFinish}>
        <Form.Item name="note" label="Note" rules={[{ required: true }]}>
          <Input />
        </Form.Item>
        <Form.Item name="gender" label="Gender" rules={[{ required: true }]}>
          <Select
            placeholder="Select a option and change input text above"
            onChange={this.onGenderChange}
            allowClear
          >
            <Option value="male">male</Option>
            <Option value="female">female</Option>
            <Option value="other">other</Option>
          </Select>
        </Form.Item>
        <Form.Item
          noStyle
          shouldUpdate={(prevValues, currentValues) => prevValues.gender !== currentValues.gender}
        >
          {({ getFieldValue }) =>
            getFieldValue('gender') === 'other' ? (
              <Form.Item
                name="customizeGender"
                label="Customize Gender"
                rules={[{ required: true }]}
              >
                <Input />
              </Form.Item>
            ) : null
          }
        </Form.Item>
        <Form.Item {...tailLayout}>
          <Button type="primary" htmlType="submit">
            Submit
          </Button>
          <Button htmlType="button" onClick={this.onReset}>
            Reset
          </Button>
          <Button type="link" htmlType="button" onClick={this.onFill}>
            Fill form
          </Button>
        </Form.Item>
      </Form>
    );
  }
}

export default App;

image.png

解决思路

我们可以尝试封装一个表单容器,通过传入数组配置,让其自动生成模板:

image.png

我们给容器定义props:

//表单项类型
export enum FormItemType {
  input,
  select,
  switch,
}

//表单配置
export interface FormConfig extends FormItemProps {
  key: string;
  type: EFormType;
}

//props
interface IProp extends FormProps {
  config: IFormConfig[];
  form: FormInstance;
}

我们可以通过如下配置传入:

import FormItemType from '表单容器组件'
const config = [
    {
        key: '1',
        name: 'name',
        label: '用户名',
        type: FormType.input,
        placeholder: "请输入用户名"
    },
    {
        key: '2',
        name: 'hobby',
        label: '爱好',
        type: FormType.select,
        optins:[{name:'篮球',value:'bb'},{name:'足球',value:'ff']
        placeholder: "请选择爱好"
    },
    {
        key: '3',
        name: 'state',
        label: '状态',
        type: FormType.switch
    },
]

通过配置我们能够知道生成配置的意图,所以我们可以在容器内部进行遍历:

import { Form, Input, Select, Switch, TreeSelect } from "antd";
import type { FormInstance, FormProps, FormItemProps } from "antd/es/form";

export enum FormType {
  input,
  select,
  switch,
}

interface Options {
  name: string;
  value: string;
}

export interface FormItemConfig extends FormItemProps {
  key: string;
  type: FormType;
  name: string;
  label?: string;
  placeholder?: string;
  options?: Options[];
}

interface IProp extends FormProps {
  config: FormItemConfig[];
  form: FormInstance;
}

const ConfigForm: React.FC<IProp> = ({ config, ...props }) => {
  const handleFormConfigMap = (config: FormItemConfig[]): any => {
    const res = config.map((item) => {
      switch (item.type) {
        case FormType.input:
          return (
            <Form.Item {...item}>
              <Input placeholder={item.placeholder} />
            </Form.Item>
          );

        case FormType.select:
          return (
            <Form.Item {...item}>
              <Select placeholder={item.placeholder}>
                {_SelcetOption(item.options)}
              </Select>
            </Form.Item>
          );

        case FormType.switch:
          return (
            <Form.Item {...item} valuePropName="checked">
              <Switch />
            </Form.Item>
          );

        default:
          return null;
      }
    });

    return res;

    function _SelcetOption(data: Options[] | undefined) {
      if (data && Array.isArray(data)) {
        return data.map((item) => {
          return (
            <Select.Option
              key={`${item.name}_${item.value}`}
              value={item.value}
            >
              {item.name}
            </Select.Option>
          );
        });
      }
      return null;
    }
  };

  return (
    <Form form={form} {...props}>
      {handleFormConfigMap(config)}
    </Form>
  );
};

为了能够获取form的上下文,我们需要传入一个form的实例:

image.png

在外部我们可以使用antd Form提供的hook,然后传入给我们的表单容器:

 const [form] = Form.useForm();
 const config = [...配置]
 
 <自定义表单容器 config={config} form={form} />

在外部我们通过form.setFieldsValue进行数据设置,但是不建议在外部更改数据源。

如果我们要获取该表单容器的数据,可以通过下面进行表单校验并获取数据:

const validate = await form.validateFields()

或者直接调用获取所有表单数据:

form.getFieldsValue(true)

Form.useForm是antd封装的大量操作数据的方法,其中获取数据则是一个回调的过程。

小结

通过上面简单例子我们实现了基础的表单封装,为了拓展我们的表单功能,我们可以向其拓展更多的表单项类型。

配置式的表单能够完成大部分的业务场景,我们可以通过给配置项添加处理函数,在change事件里进行判断处理,实现表单联动等功能。