前沿:中后台应用中表单需求颇多,左手一个表单,右手又是一个表单,无穷无尽,如果用模版一个个来写,不单写起来费时费力,而且看起来也是天花乱坠,于是这个时候你会去设想,那有没有什么方式可以去替换琐碎的手写表单模版的方式呢?让表单是“配出来”的,而不是撸出来的,让你轻松解决 form 表单,也不再为表单而烦恼。答案就是:动态表单
1.传统表单模版
❝一个表单需要什么?无疑是包含了form数据的收集、验证及提交等等功能,让我们看看下面这个基于iview组件库的form表单
❞
这个简单的表单,如果我们用手写模版的方式撸出来,模版部分就是如下所示👇
数据初始化定义和验证提交逻辑如下
❝以上就完成一个具备数据收集、验证、提交、重制的表单,但是相对应问题也来了,这里用模板并不是最好的选择,代码过于冗长,也存在重复代码,如果我的项目中十几个表单甚至更多,我岂不是都要去写怎么多代码去维护这类表单,会不会显得太冗余,接下来进入我们今天的主角:动态表单,让我们看看怎么让他“动”💃起来
❞
2 动态表单
2.1 我所期望的表单
❝我期望的表单是可以配出来的,通过JSON来动态渲染生成相应的表单,表单中涉及的组件(比如Input、Select)可以通过获取JSON的配置所需的去渲染,上一小节提到的模版渲染显然就不适用这次场景了,虽然vue官方推荐在绝大多数情况下使用模板来创建你的temlate,但是一些场景还是需要用到渲染函数render 官方文档点我👈
❞
2.2 关于渲染函数
❝我们先看看这个例子,Vue.js 的 mount 函数,将h()生成的VNode节点函数,渲染成真实 DOM 节点,并挂载到根节点上
❞
这个h()
函数本质上是createElement 函数,这个函数的作用就是生成一个 VNode节点(虚拟节点),它不是一个实际的 DOM 元素。叫createNodeDescription(创建节点描述),我们是通过它所包含的信息会来告诉 Vue 页面上需要渲染什么样的节点,再通过diff算法可以追踪dom的变化
拓展:你可能会好奇为啥是叫h()函数
,而不是createElement()
的简称c()
❝h出自hyperscript首字母,最原始的定义是“Create HyperText with JavaScript”,而HyperText则是出自我们熟悉的则HTML 是 hyper-text markup language 的缩写(超文本标记语言),所以可以理解为Hyperscript是指生成HTML的 script 脚本
❞
createElment函数接受三个参数,分别是:
- 参数一:标签名、组件的选项对象、函数等等(必选);
- 参数二:设置这个对象的样式、属性、传的组件的参数、绑定事件等(可选);
- 参数三:该节点下的其他节点,即子级虚拟节点,可以是字符串形式或数组形式,也需要使用createElement构建。
下面用一个简单例子说明渲染函数的使用👇
面例子的模版渲染和渲染函数渲染的结果是一样的,当然模板本质上也是通过 Compile 编译 得到 渲染函数render()
,所以说其实渲染函数更高效,更快,减少了编译的时间,关于编译可以看这篇vue 编译过程,由templete编译成render函数
渲染函数render与模板template的区别
- render(高)的性能要比tempate(低)要高。
- template简单,可以直观看出内容想要表述的含义,但是不够灵活而;render渲染
函数则是通过createElement的方式创建VNode,适合开发复发性强的组件。
扯完渲染函数,接下来介绍下动态表单的思路
3 动态表单的实现
❝这里使用的是iview组件库的基础上实现的动态表单,创建的组件都是基于iview来实现的,下面是具体的流程图
❞
3.1配置表单配置内容
❝我用第一节的例子来配置一个JSON格式的表单配置(因为配置文件过长,改用文字)
❞
const formOption = { ref: 'formValidate', style: { //表单样式,非必须 width: '300px', margin: 'auto', }, className: 'form', formProps: { //非必须 'label-width': 80, }, formData: {//所要监听的表单字段数据,必须 name: '', city: '', sex: 'male', }, formItem: [ //iview form表单的每个formItem,必须 { type: 'input', label: '名称', //对应formItem的label key: 'name', //key对应formData中的字段 props: { placeholder: '请输入名称', }, rules: { //表单检测规则,非必须 required: true, message: '请填写名称', trigger: 'blur', }, }, { type: 'select', label: '城市', //对应formItem的label key: 'city', //key对应formData中的字段 props: { placeholder: '请输入名称', }, children: [{ label: 'xml', value: '1' }, { label: 'json', value: '2' }, { label: 'hl7', value: '3' } ], rules: { //表单检测规则,非必须 required: true, message: '请选择城市', trigger: 'blur', }, }, { type: 'radioGroup', key: 'type', label: 'sex', children: [ { text: 'female', label: 'female', }, { text: 'male', label: 'male', }, ], events: { 'on-change': (vm, value) => { vm.$emit('on-change', value); }, }, } ], events: events('formValidate'),//表单按钮组 }
还有相应的事件按钮统一在events中处理(可复用)
3.2 render函数渲染组件
❝第一节例子涉及到表单组件分别是Input、Select、radioGroup、formItem。分别是定义它们的render函数
❞
- 暴露不同组件渲染的api
- Input组件渲染函数
❝集合iview组件库Input的API,包括props属性、events事件、slot插槽、methods方法等来定义渲染函数,具体实现如下图所示
❞
function generateInputComponent(h, formData = {}, obj, vm) { const key = obj.key? obj.key : '' let children = [] if (obj.children) { //input有子集,走这里 children = obj.children.map(item => { let component if (item.type == 'span') { //复合型输入框情况 component = h('span', { slot: item.slot }, [item.text]) } else { let func = componentObj[item.type] component = func? func.call(vm, h, formData, item, vm) : null } return component }) } return h('Input', { props: { value: key? formData[key] : '', ...obj.props }, style: obj.style, on: { ...translateEvents(obj.events, vm), //时间绑定 input(val) { if (key) { formData[key] = val } } }, slot: obj.slot }, children) } //事件bind function translateEvents(events = {}, vm, formData = {}) { const result = {} for (let event in events) { result[event] = events[event].bind(vm, vm, formData); } return result }
- Select 组件渲染函数
function generateSelectComponent(h, formData = {}, obj, vm) { const key = obj.key? obj.key : '' let components = [] if (obj.children) { components = obj.children.map(item => { if (item.type == 'optionGroup') { return h('OptionGroup', { props: item.props? item.props : item }, item.children.map(child => { return h('Option', { props: child.props? child.props : child }) })) } else { return h('Option', { props: item.props? item.props : item }) } }) } return h('Select', { props: { value: formData[key], ...obj.props }, style: obj.style, on: { ...translateEvents(obj.events, vm), input(val) { if (key) { formData[key] = val } } }, slot: obj.slot }, components) }
❝这里只是展示部分组件的实现方式,主要目的是梳理开发及应用的流程思路
❞
- events 按钮生成
function generateEventsComponent(h, formData = {}, obj, vm) { const components = []; if(obj.submit) { const submit = h('Button', { props: obj.submit.props, style: obj.submit.style, class: obj.submit.className, on: { click() { //提交前校验 vm.$refs[obj.ref].validate((valid) => { if (valid) { obj.submit.success.call(vm, formData, vm) } else { obj.submit.fail.call(vm, formData, vm) } }) } } }, [obj.submit.text]) components.push(submit) } if (obj.reset) { const reset = h('Button', { props: obj.reset.props, style: { ...obj.reset.style, }, class: obj.reset.className, on: { click() { vm.$refs[obj.ref].resetFields() //重置表单 obj.reset.success.call(vm, formData, vm); } } }, [obj.reset.text]) components.push(reset) } return h('div',{ class: 'vue-events', style: { ...obj.style } }, components) }
- formBuild动态表单组件的定义
❝实现好组件的动态生成逻辑,这个时候需要一个入口(formBuild.js),就是根据配置去映射相应的组件并生成合并,组合成为最终要的表单
❞
// form-build.js import componentObj from './utils' export default { props: { options: { type: Object, required: true }, }, render(h) { const options = this.options const formData = options.formData if (!options.formItem) { return h('div') } const components = options.formItem.map(item => { let func = componentObj[item.type] let subComponent = func? func.call(this, h, formData, item, this) : null let component = componentObj.formItem(h, item, subComponent, formData) return componentObj.col(h, item, component) }) const childComp = []; const fromComp = h('Form', { ref: options.ref, style: options.style ? options.style : '', props: { model: formData, ...options.formProps }, class: 'vue-generate-form' }, [ h('Row', { props: options.rowProps }, components) ]); childComp.push(fromComp); if (options.events) { const eventComo = componentObj.events(h, formData, obj.events , vm) childComp.push(eventComp) } return h('div', [childComp]); } }
还需要定义vue的插件安装
2.3 如何使用
- 注意事项
- 某些组件(例如 button) iview 并没有提供类似于
on-click
这样的事件。可以使用 DOM 元素原生事件代替,例如click
- 所有表单数据都要在formData里定义
4.总结
❝以上就可以通过render渲染函数来完成动态表单工具的实现,本文主要是通过一种思路去介绍整个开发,动态表单有多种实现方式,当然你可能也有疑惑
❞
- 如何支持多种UI组件库的动态表单配置?
❝你可以参考下开源的form-create(支持3种 UI 框架:Iview、ElementUI、Ant-design-vue)是如何实现的 form-create工具库
❞
- 如何开发在线编辑配置的动态表单工具?
❝
可视化表单设计工具也很香,有兴趣的童鞋可以了解 vue-ele-form-generator
❞
文章思路来源:vue-form-make
往期文章