背景
我们做的是后台类型的管理系统,因此相对应的表单就会很多。
相信做过类似项目的老哥懂得都懂。
因此我们希望能够通过一些相对简单的配置方式生成表单,不再需要写一大堆的组件。
尽量通过数据驱动。
思路
不管是哪个平台,思路都是相通的。
1.基于UI框架封装
react我们基于antd封装。
vue我们基于element封装。
这两个框架下的表单,几乎都满足了我们对表单的需要,只是需要写那么多标签代码,让人感到厌倦。
2.如何根据数据驱动
想要简化标签,首先就需要约定数据格式,什么样类型的数据渲染什么样的标签。
那么我可以暂定,需要一个type
,去做判断,渲染什么样的表单内容标签(是的,if
判断,没有那么多花里胡哨,最朴实无华的代码就能满足我们的需求)
3.确定需要渲染的标签
业务中其实常用的表单标签就如下几类:
select
checkbox
radio
-
input
(包括各个类型的,password
,textarea
之类的) switch
等等,需要再加
4.类型需要传递下去
需要把表单可能用到的属性传递下去。
实现
因为我们在vue和react上都有,所以我会给出两个框架的封装代码。
Vue
我使用的是vue3+element-plus
封装两个组件,Form和FormItem
代码如下:
Form
<!-- Form/index.vue-->
<template>
<el-form :ref="setFormRef" :model="form" label-width="80px">
<el-form-item
v-for="(item, index) in needs"
:key="index"
:prop="item.prop"
:label="item.label"
:rules="item.rules"
>
<!-- 内容 -->
<FormItem
v-model="form[item.prop]"
:type="item.type"
placeholder="请输入内容"
:options="item.options || []"
:disabled="item.disabled"
v-bind="item"
/>
</el-form-item>
</el-form>
</template>
<script>
import { defineComponent, computed, watch } from 'vue';
import FormItem from '../FormItem/index.vue';
export default defineComponent({
components: {
FormItem,
},
props: {
// 需要写的表单内容
needs: {
type: Array,
default: () => [],
},
// 已知的表单内容
modelValue: {
type: Object,
default: () => {},
},
instance: {
type: Object,
default: () => {},
},
},
emits: ['update:modelValue', 'update:instance'],
setup(props, context) {
const form = computed({
get: () => props.modelValue,
set: (val) => {
console.log('变化');
context.emit('update:modelValue', val);
},
});
const setFormRef = (el) => {
context.emit('update:instance', el);
};
// 变化触发更新
watch(form, (newValue) => {
context.emit('update:modelValue', newValue);
});
return { form, setFormRef };
},
});
</script>
FormItem
<!-- FormItem/index.vue-->
<template>
<el-input v-if="type === 'input'" clearable v-model="value" v-bind="$attrs" :class="propsClass" />
<el-input
v-else-if="type === 'password'"
type="password"
clearable
v-model="value"
v-bind="$attrs"
:class="propsClass"
/>
<el-radio-group
v-else-if="type === 'radio'"
v-model="value"
v-bind="$attrs"
:disabled="disabled"
:class="propsClass"
>
<el-radio
v-for="(item, index) in options"
:key="index"
:label="item.value"
:disabled="item.disabled"
>
{{ item.label }}
</el-radio>
</el-radio-group>
<el-checkbox-group
v-else-if="type === 'checkbox'"
v-model="value"
v-bind="$attrs"
:disabled="disabled"
:class="propsClass"
>
<el-checkbox
v-for="(item, index) in options"
:key="index"
:label="item.value"
:disabled="item.disabled"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
<el-input
v-else-if="type === 'textarea'"
type="textarea"
clearable
v-model="value"
v-bind="$attrs"
:class="propsClass"
/>
<el-select
v-else-if="type === 'select'"
clearable
v-model="value"
v-bind="$attrs"
:disabled="disabled"
:class="propsClass"
>
<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:disabled="item.disabled"
:value="item.value"
/>
</el-select>
<el-switch v-else-if="type === 'switch'" v-model="value" v-bind="$attrs" :class="propsClass" />
<el-time-select
v-else-if="type === 'timeSelect'"
v-model="value"
v-bind="$attrs"
:disabled="disabled"
:class="propsClass"
/>
</template>
<script>
import { defineComponent, computed, watchEffect } from 'vue';
export default defineComponent({
name: 'FormItem',
props: {
// 需要绑定的值
modelValue: {
type: [String, Boolean, Number, Array],
default: '',
},
// 传递下来的class
propsClass: {
type: String,
default: '',
},
/**
* 表单的类型 radio 单选 checkbox 多选 input 输入 select 选择 cascader 卡片 switch 切换 timeSelect 时间选择
* @values radio, checkbox, input, select, cascader, switch, timeSelect,
*/
type: {
type: String,
default: '',
require: true,
},
// {value,disabled,source}
options: {
type: Array,
default: () => [{}],
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(props, context) {
const value = computed({
get: () => props.modelValue,
set: (val) => {
context.emit('update:modelValue', val);
},
});
watchEffect(
() => props.modelValue,
(newValue) => {
value.value = newValue;
},
);
return {
value,
};
},
});
</script>
<style lang="less" scoped>
:deep(.el-*) {
width: 100%;
}
.width100 {
width: 100%;
}
</style>
这里要注意的点是v-bind="$attrs"
-
因为我们不可能将所有组件可能用到的
props
都写在这并导出没,而且也没有这个必要。 -
所以我们可以用到vue提供的$attrs来帮助我们透传下去
使用
比如像这样一个表单
1.png我们只需要如下代码
Rules规则是我们单独定义的符合async-validator
的规则,这里就不写引入了
<template>
<Form
v-model:instance="formRef"
v-model="formData"
:needs="needs"
/>
</template>
<script>
import {
defineComponent, reactive, computed, ref
} from 'vue';
export default defineComponent({
setup(){
const formRef = ref();
const options = reactive({
departments: [],
places: [],
roles: [],
});
const formData = reactive({
account: '',
department: [],
name: '',
password: '',
practicePlace: [],
rePassword: '',
roleId: '',
uniqueid: '',
});
const needs = computed(() => [
{
label: '用户名',
type: 'input',
prop: 'name',
propsClass: 'width100',
placeholder: '请输入2-20个汉字,字母或数字',
rules: [
Rules.required('用户名不得为空'),
Rules.dynamicLength(2, 20, '用户名长度为2-20位'),
Rules.cen,
],
},
{
label: '用户账号',
type: 'input',
prop: 'account',
propsClass: 'width100',
placeholder: '请输入2-20个字母或数字',
rules: [
Rules.required('用户账号不得为空'),
Rules.dynamicLength(2, 20, '用户账号长度为2-20位'),
Rules.en,
],
},
{
label: '密码',
type: 'password',
prop: 'password',
propsClass: 'width100',
placeholder: '支持6-20个字母、数字、特殊字符',
rules: [
Rules.required('密码不得为空'),
Rules.dynamicLength(6, 20, '密码长度为6-20位'),
Rules.password,
],
},
{
label: '再输一次',
type: 'password',
prop: 'rePassword',
propsClass: 'width100',
placeholder: '支持6-20个字母、数字、特殊字符',
rules: [
Rules.required('请再输入一次密码'),
Rules.dynamicLength(6, 20, '密码长度为6-20位'),
Rules.password,
Rules.same(formData.password, formData.rePassword, '两次密码输入不一致'),
],
},
{
label: '角色',
type: 'select',
prop: 'roleId',
propsClass: 'width100',
placeholder: '请选择角色',
rules: [Rules.required('角色不得为空')],
options: options.roles,
},
{
label: '执业地点',
type: 'select',
prop: 'practicePlace',
propsClass: 'width100',
placeholder: '请选择执业地点',
multiple: true,
filterable: true,
options: [{ label: '全部', value: 'all' }].concat(options.places),
},
{
label: '科室',
type: 'select',
prop: 'department',
propsClass: 'width100',
placeholder: '请选择科室',
multiple: true,
filterable: true,
options: [{ label: '全部', value: 'all' }].concat(options.departments),
},
]);
// 网络请求获取options,这里就简写了
// *********************
return {
formData,
needs,
formRef,
}
}
})
</script>
我们只需要聚焦数据,就可以构造出一张表单。
React
也是相似的,而且较之Vue的更加灵活,除了我们上述的这种常用表单,我们可以把后台管理的搜索项也认为是表单
Form
import React from 'react';
import { ColProps, Form, FormInstance } from 'antd';
import { FormLayout } from 'antd/lib/form/Form';
import FormItem, { IFormItem } from '../FormItem';
interface IForm {
form: FormInstance<any>;
itemLayout?: {
labelCol: ColProps;
wrapperCol: ColProps;
};
layout?: FormLayout;
options: IFormItem[];
initialValues?: { [key: string]: any };
onValuesChange?(changedValues: unknown, allValues: any): void;
}
// 这是个单独的表单校验模板
/* eslint-disable no-template-curly-in-string */
const validateMessages = {
required: '${label}是必填项',
};
/* eslint-enable no-template-curly-in-string */
const FormComponent = (props: IForm): JSX.Element => {
const {
form, onValuesChange, initialValues, options, layout, itemLayout,
} = props;
return (
<Form
form={form}
{...itemLayout}
layout={layout}
onValuesChange={onValuesChange}
initialValues={initialValues}
validateMessages={validateMessages}
>
{/* 内容 */}
{options.map((item) => (
<FormItem key={item.value} {...item} />
))}
</Form>
);
};
FormComponent.defaultProps = {
layout: 'horizontal',
itemLayout: {
labelCol: {},
wrapperCol: {},
},
initialValues: {},
// 此处默认定义为空函数
onValuesChange() {},
};
export default FormComponent;
export type { IFormItem };
需要注意的点
-
form
的引用实例由外部传入 - 取值赋值通过
formInstance
做,因为和vue不一样,react做父子双向绑定比较复杂(也可能是我不太熟练的缘故),所以建议是不要做成受控组件
FormItem
import React from 'react';
import {
Form, Radio, Select, Input, DatePicker, Switch,
} from 'antd';
import { Rule } from 'antd/lib/form';
const { Option } = Select;
const { RangePicker } = DatePicker;
export interface IFormItem {
type: 'input' | 'radio' | 'select' | 'rangePicker' | 'datePicker' | 'switch';
label: string;
// 需要绑定的key值
value: string;
// 可选项
placeholder?: string;
options?: { label: string; value: string | number }[];
otherConfig?: any;
itemConfig? : any;
rules?: Rule[];
itemClass?: string;
}
// Form.Item似乎也不允许HOC
const FormItemComponent = (props: IFormItem): JSX.Element => {
const {
type, label, value, rules, placeholder, otherConfig, options, itemClass, itemConfig,
} = props;
// 判断类型
return (
<Form.Item label={label} name={value} rules={rules} className={itemClass} {...itemConfig}>
{(() => {
switch (type) {
case 'input':
return <Input placeholder={placeholder} {...otherConfig} />;
case 'radio':
return (
<Radio.Group {...otherConfig}>
{options?.map((item) => (
<Radio key={item.value} value={item.value}>
{item.label}
</Radio>
))}
</Radio.Group>
);
case 'select':
return (
<Select {...otherConfig} placeholder={placeholder}>
{options?.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
);
case 'rangePicker':
return <RangePicker {...otherConfig} />;
case 'datePicker':
return <DatePicker {...otherConfig} />;
case 'switch':
return <Switch {...otherConfig} />;
default:
return <div />;
}
})()}
</Form.Item>
);
};
export default FormItemComponent;
这里要注意的点
- antd的FormItem,似乎不允许HOC(反正我试过是会出bug),也就是说判断渲染组件需要放在Item组件的内部做,不能单独抽出方法render!
- A List of antd's components that cannot work with HOC
使用
例如下面两个例子
2.pngimport React, { useEffect, useState } from 'react';
import {
Form
} from 'antd';
import FormComponent, { IFormItem } from '../../components/FormComponent';
const Welcome = (): JSX.Element => {
const [form] = Form.useForm();
const [saleList, setSaleList] = useState<Options[]>([]);
const [firmList, setFirmList] = useState<Options[]>([]);
const options: IFormItem[] = [{
type: 'select',
label: '厂商名称',
value: 'clientId',
options: firmList,
itemClass: 'width25',
otherConfig: {
onChange: () => {
// 选中触发搜索,具体的就不写了
search();
},
},
}, {
type: 'select',
label: '销售人员',
value: 'saleId',
options: saleList,
itemClass: 'width25',
otherConfig: {
onChange: () => {
// 选中触发搜索,具体的就不写了
search();
},
},
}];
useEffect(() => {
// 获取两个列表,具体的就不写了
getFirmList();
getSaleList();
}, []);
return (
<FormComponent
form={form}
layout="inline"
options={options}
initialValues={{
clientId: '',
saleId: '',
}}
/>
)
};
export default Welcome;
3.png
import React, { useEffect, useState } from 'react';
import {
Form
} from 'antd';
import FormComponent, { IFormItem } from '../../components/FormComponent';
const UserList = (): JSX.Element => {
const initialValues = {
name: '',
email: '',
account: '',
password: '',
rePassword: '',
roleId: '',
};
const [userForm] = Form.useForm();
const userOptions: IFormItem[] = [{
type: 'input',
label: '名称',
value: 'name',
rules: [
{
required: true,
},
Rules.dynamicLength(2, 20),
Rules.chinese,
],
}, {
type: 'input',
label: '邮箱',
value: 'email',
}, {
type: 'input',
label: '账号',
value: 'account',
rules: [
{
required: true,
},
Rules.dynamicLength(2, 20),
Rules.cen,
],
}, {
type: 'input',
label: '密码',
value: 'password',
rules: [
{
required: true,
},
Rules.minLength(6),
Rules.englishAndNumber,
],
}, {
type: 'input',
label: '再次确认密码',
value: 'rePassword',
itemConfig: {
dependencies: ['password'],
},
rules: [
{
required: true,
},
Rules.minLength(6),
Rules.englishAndNumber,
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次密码不一致'));
},
}),
],
}, {
type: 'select',
label: '用户角色',
value: 'roleId',
options,
rules: [
{
required: true,
},
],
}];
return (
<FormComponent
form={userForm}
options={userOptions}
itemLayout={{
labelCol: {
sm: { span: 5 },
},
wrapperCol: {
sm: { span: 18 },
},
}}
initialValues={initialValues}
/>
)
};
export default UserList;
网友评论