持续创作,加速成长!这是我参与「掘金日新计划 · 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;
解决思路
我们可以尝试封装一个表单容器,通过传入数组配置,让其自动生成模板:
我们给容器定义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的实例:
在外部我们可以使用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事件里进行判断处理,实现表单联动等功能。