最近做的小程序关于表单的字段需要用户自定义编辑功能,这个表单编辑是整个项目的重点。
选择一个靠谱的表单编辑器,然后实现表单的数据处理是不得不深入研究的问题。
总的来说,自定义表单程序至少需要:设计器(支持拖拉拽修改各种参数)、数据管理(用户填写的表单数据进行处理)
展示层面
表单展示层面分为后台设计表单和用户填写表单页面。
经过调研,我决定使用这款表单生成组件。
对,他是个组件,读取配置列表就可以生成不同类型的表单项,这种方式存储方便,展示也方便(没涉及复杂的功能)
form-create 是一个可以通过 JSON 生成具有动态渲染、数据收集、验证和提交功能的表单生成组件。支持5个UI框架,并且支持生成任何 Vue 组件。内置20种常用表单组件和自定义组件,再复杂的表单都可以轻松搞定。
form-create: xaboy/form-create: :fire::fire::fire: 强大的动态表单生成器|form-create is a form generation component that can generate dynamic rendering, data collection, verification and submission functions through JSON. (github.com)
项目简介中说到:支持5个UI框架,分别是:
- element-plus
- ant-design-vue
- naive-ui
- arco-design
- tdesign
嗯,没有小程序版本,我们用的小程序是Vue3 + uniapp
,项目作者虽说后续会支持小程序,但是我们等不了他了,自己手动实现好了。
实现思路大概是:编辑出来表单JSON,前台去读取渲染
其中作者实现了编辑器,通过form-create组件去渲染表单。
form-create-designer: xaboy/form-create-designer: 好用的vue可视化表单设计器 (github.com)
编辑器预览的样式是element-ui
的,无所谓,我们用的是小程序,能生成配置,让我们小程序读到就可以了。
输入框JSON格式示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| { type: "input", title: "商品名称", field: "goods_name", value: "", props: { "type": "text", "clearable": false, "disabled": false, "readonly": false, "rows": 4, "autosize": false, "number": false, "autofocus": false, "autocomplete": "off", "placeholder": "请输入商品名称", "size": "default", "spellcheck": false, "required": false, }, validate: [{ required: true, message: '请输入商品名称', trigger: 'blur' }, ], },
|
其中props
在编辑器面板右侧可以编码处理,这个是我定义的姓名表单项:用户可以配置placeholder,是否可清空,是否禁用,是否只读……. 当然你可以加任何属性,业务上的也不耽误渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import uniqueId from '@form-create/utils/lib/unique' import { localeProps, makeRequiredRule, makeHiddenRule } from '@/utils' import phoneCmp from '@/components/form-item/phone.vue' const label = '姓名' const name = 'name'
export default { icon: 'icon-input', label, name, rule({ t }) { return { type: name, field: uniqueId(), title: label, info: '', $required: false, props: {}, } }, component: phoneCmp, props(_, { t }) { return localeProps(t, name + '.props', [ makeRequiredRule(), { type: 'input', field: 'placeholder', title: '输入框占位文本', }, { type: 'switch', field: 'clearable', title: '是否可清空' }, { type: 'switch', field: 'disabled', title: '禁用' }, { type: 'switch', field: 'readonly', title: '是否只读' }, ]) }, }
|
uniapp
小程序端
小程序端因为设计器没有实现UI库适配,我使用相对稳定一些的uni-ui
,相对比其他的功能少点,样式差点,不过这都不重要。
基本上,拿到了配置后走个v-for
循环渲染一下表单项就可以了,大概是:
1 2 3 4 5 6 7 8 9 10 11
| <!-- diy-form --> <template> <uni-forms ref="baseFormRef" label-position="top" :rules="diyRules" :modelValue="diyFormPayload"> <formItem v-for="item in formList" :key="item.id" :field="item" @change-data="changeOtherData" /> </uni-forms> </template>
|
uni-forms
可以接收我自定义的rule
,表单规则是根据生成的配置进行生成的,在尽量适配uni-ui
的方式去实现,比如required
,邮箱、身份证、手机号之类的验证,都是根据表单项中的参数来确定的。
对于表单项组件,为各种表单项添加v-if
进行类型判断分别渲染。
重点说一下数据的流动:
uni-forms
动态传入的rule
,需要在onReady
之后触发setRules()
方法(最早要晚于ready
)。
所以在生成自定义rule后要调用一下setRules()
,但是请求可能早于onReady
,那就在onReady
同时调用一次即可。
1 2 3 4 5 6
| onReady(() => { initForm() }) watch(formList, () => { initForm() })
|
从diy-form
传来的field
,经过computed
处理一层统一适配form-item
1 2 3 4 5 6 7 8 9 10 11 12 13
| const _field = computed(() => { const field = props.field return { ...field, ...(field?.props || {}), required: field.$required != false, readonly: field?.props?.readonly, disabled: field?.props?.disabled || field?.props?.readonly || false, placeholder: field?.props?.placeholder || `请输入${field.title}`, type: field.type } })
|
这里的formVal
是要绑定到表单项的,表单项的值要通过刚才说的uni-forms
中绑定的modelValue
中获取,所以为了不用统一传递,我直接从uni-forms
中读取了。
修改也同样,直接改uni-forms
中的modelValue
即可。
1 2 3 4 5 6 7 8 9 10 11
| const emit = defineEmits(['changeData']) const formVal = computed({ get: () => { const form = inject<any>('uniForm') let formVal = form._getDataValue(_field.value.field, form.localData) return formVal }, set: (val) => { emit('changeData', _field.value.field, val) } })
|
最后,给diy-form
组件暴露两个方法:验证表单和更新表单数据。
1 2 3 4 5 6 7 8
| defineExpose({ validate: async () => { }, updateData: (data) => { } })
|
数据管理
先放一下,吃饭去了。。。