说到动态表单,常见的业务场景,比如固定表单场景和固定UI场景,比如问卷调查的表单业务,比如线上考试的题库等,有大量相似表单需求的场景。
实现一个如下所示简单的动态表单,所谓的动态,基本都可以封装在配置里面,然后基于这份配置渲染出表单。
得到的结论: 动态表单 = 动态的配置 + 渲染表单;
动态配置
为了配置动态表单,就要将表单描述变得序列化,如何实现表单结果可序列化,目前业内比较火的数据序列化json-schema, 除了有数据层面的基础描述,还包括各种校验属性。 但是动态表单,除了数据层面的描述,还包括UI上的描述;所以json-schema某种程度上还不能覆盖动态表单的描述; 为了增强json-schema;可有以下几种方式
- json-schema做数据层面的描述,自定义ui-schema做UI层面的描述;
- json-schema做数据层面的描述,扩展其上的属性,xx-aaa用作UI层面的属性表述;
- DSL 自定义一套标准,来用描述动态表单
方式一 ———json-schema + ui-schema
比如react-jsonschema-form, JSONSchema用来描述数据层面的,UISchema用来描述UI上面的不同; 可以完美的复用JSONSchema, 但是需要维护两份配置,对于哪些描述属于哪个配置文件,给使用者有一定心智负担,同时维护两份也带来维护成本;
方式二————对JSON-Schema做扩展
比如formilyjs, 基于JSON-Schema上做了扩展,只需维护一份配置。
方式三*————DSL
- DSL(Domain Specific Language )领域特定语法,为动态表单打造一套描述其数据结构的语法;定制化程度会更高,但是理解一套新的语法来描述,也有一定的上手成本。
{
type: '',
label: '',
defaultValue: '',
placeholder?: string;
options?:[],
rule: ["all", ["gte", 18], ["lte", 55]],
errorMsg: "xxxx",
when: []
}
目前我们采用基于DSL来实现一套简要的动态表单,因为自定义程度比较高,也不存在复杂的语法,更偏向于类似json,描述一些键值对来做解析。
const fieldList= [{
label: '用户名称',
name: 'username',
type: 'Input',
rule: {
validator: (val: unknown) => {
const hasError = /^[a-z]{1,}$/gi.test(`${val || ''}`) !== true;
return {
hasError,
message: hasError ? '仅支持a-z的大小写字母' : ''
};
}
}
}, {
label: '手机号码',
name: 'phone',
type: 'Input',
rule: {
validator: (val: unknown) => {
const hasError = /^[0-9]{1,}$/gi.test(`${val || ''}`) !== true;
return {
hasError,
message: hasError ? '仅支持0-9的数字' : ''
};
}
}
}, {
label: '是否参加活动',
name: 'sex',
type: 'Radio',
options: [{
name: '参加', value: '1'
}, {
name: '不参加(不参加需要备注原因)', value: '2'
}],
rule: {
validator: (val: unknown) => {
const hasError = `${val}`?.length === 0;
return {
hasError,
message: hasError ? '性别不能为空' : ''
};
}
}
}, {
label: '原因',
name: 'summmer',
type: 'Input',
binVisible: {
sign: '',
name: 'sex',
value: '2'
}
}]
动态渲染
统一基础组件的输入和输出
结合配置,可以初始化我们定义的一些基础组件,input, radio,并且我们统一所有组件的输入和输出API上的封装,这样可以在渲染的时候可以统一接入,不需要额外的处理不一致的行为。
// radio组件
const props = defineProps<{
value: string;
options: Array<{name: string; value: string}>;
}>()
const emits = defineEmits<{
(e: 'change', value: string): void
}>()
const props = defineProps<{
value: string;
options: Array<{name: string; value: string}>
}>()
const emits = defineEmits<{
(e: 'change', value: string): void;
}>();
渲染表单
说到vue的动态渲染,官网里面提供的方式有以下几种方式
- h() render函数;
- jsx——使用@vue/babel-plugin-jsx插件来支持对jsx的解析;
- 动态组件
目前我们选择动态组件结合type属性来实现
<template>
<div>
<Form ref="formRef" :model="internalModel" @finish="onFinish" @finishFail="onFinishFail">
<FormItem v-for="(filed, index) in fieldList" :key="index" :label="filed.label" :rule="filed.rule" :name="filed.name" :binVisible="filed.binVisible">
<component :is="registerComponentMap[filed.type]" :value="internalModel[filed.name]" :options="filed.options || []" @change="(value: unknown) => { onFieldChange({name: filed.name, value})}"></component>
</FormItem>
<Row v-if="$slots.default">
<slot></slot>
</Row>
</Form>
</div>
</template>
<template>
<form @submit="handleSubmit">
<slot />
</form>
</template>
<template>
<Row>
<Row v-if="showVisible">
<Col :span="labelCol">
<span>{{ props.label }}</span>
</Col>
<Col :span="wrapperCol">
<slot />
</Col>
</Row>
<Row v-if="props.name">
<Col :span="labelCol"></Col>
<Col :span="wrapperCol">
<span v-if="errorTip">{{ errorTip }}</span>
</Col>
</Row>
</Row>
</template>
表单统一校验
Form提供一个formContext供FormItem来将每一行的表单项的校验逻辑注入,并且统一在Form组件中触发;
FormItem中根据rule字段来解析每一行表单项的校验工作。
async function validateFieldValue(val) {
if(props.rule?.validator) {
const result = await props.rule?.validator?.(val);
if (result.hasError && result.message) {
errorTip.value = result.message;
} else {
errorTip.value = '';
}
return { ...result, ...{ name:props.name, value:toRaw(val)}}
}
return {
hasError: false
}
}
联动
利用formContext中共享表单所有数据,FormItem中获取到binVisible配置,结合formContext共享的model数据,可以实现表单控件间的联动。
const genVisiable = () => {
if(props.binVisible) {
showVisible.value = false;
if((formContext.model[props.binVisible?.name] === props.binVisible?.value)) {
showVisible.value = true;
} else {
showVisible.value = false;
}
}
}
总结
实际业务中,动态表单的场景更多适合为简单固定的表单的业务场景,这样减少一定的重复性工作。但是也有一些开放性的三方表单生成器,提供了自定义扩展,更有与低代码结合的拖拽式表单生成器,可以实现复杂场景的表单生成,
很多重复性的表单可以用动态表单生成,节约一定的时间成本,如果是复杂性的表单,需要有很多的自定义的扩展,生成的同时,其中的维护成本,扩展性也需要考虑。
本文结合例子,实现了一个简易的动态表单,从动态配置的三种方式,
- json-schema做数据层面的描述,自定义ui-schema做UI层面的描述;
- json-schema做数据层面的描述,扩展其上的属性,xx-aaa用作UI层面的属性表述;
- DSL 自定义一套标准,来用描述动态表单;
表单渲染的流程,对基础组件统一输入和输出,动态渲染的方式,用provider,inject来实现表单数据共享和通信,同时也借此实现了表单校验,表单联动。