nf-form 表单控件的功能

基于 el-form 封装了一个表单控件,包括表单的子控件。

既然要封装,那么就要完善一些,把能想到的功能都要实现出来,不想留遗憾。

毕竟UI库提供的功能都很强大了,不能浪费了对吧。

  • 依赖 json 动态创建表单
  • 可以多行多列
  • 可以调整布局
  • 可以自定义子控件(插槽和动态组件)
  • 可以扩展表单子控件
  • 数据验证
  • 数据联动
  • 组件联动
  • 依据 json 自动创建 model

功能演示

介绍代码之前先看看效果。

  • 单列表单

    这个比较基础,直接贴图。

  • 多列表单

    有时候需要双列或者三列的表单,这个也是要支持的。

因为采用的是 el-col 实现的多列,所以理论上最多支持 24 列,当然要看屏幕的宽度了。

  • 调整布局

    看上面的图片,可以发现个问题,改变列数之后,表单页面变得不好看了,这时候需要我们做一些调整,比如让某个组件占用两份空间,调整一下组件的先后顺序。

【单列中的合并】

调整之后,页面可以更紧凑。可以两个组件占一行,也可以三个组件占一行,具体看屏幕的宽度和一个组件的大小。

【多列里的占一行】

  • 自定义子控件

    如果表单提供的子控件不能满足需求,那么怎么办?我们可以自己来定义一个子控件。
  1. 使用插槽

    使用插槽比较简单和灵活,可以在表单控件外部完全控制,适合临时的情况,插槽里可以有多个组件。

  1. 使用动态组件

    插槽的方式虽然灵活,但是不便于复用,如果需要在多个地方使用的话,可以先做成一个组件,然后用动态组件的方式加入表单。

这里使用动态组件的方式加入了 element 的穿梭控件,也可以加入其它各种组件。

  • 数据验证

    可以直接使用 el-form 提供的验证功能,在json里面设置好验证规则即可。

  • 数据联动
  1. 一个组件内的联动

    这个可以使用 el-cascader 来实现。

  2. 多个组件的联动

    可以用简单来实现。

  • 组件联动

    可以根据某个组件的值,设置其他组件是否显示。

封装表单子控件

表单控件需要很多子控件,所以要先封装一下子控件,然后才方便封装表单控件。

定义接口,统一规范

表单子控件有一个相同的需求,都需要实现属性和 v-model 数据交换,因为 element 把 value 给封装成了v-model,所以无法直接绑定组件的属性,必须建立一个内部变量来绑定。

所以需要一个转换的方式,这里采用自定义ref来实现,顺便实现了一下防抖功能。

虽然在表单控件里面并不需要防抖功能,但是查询的时候需要,而表单子控件是可以通用到查询控件里面的。

定义一个 v-model 和 my-change

// 自定义 ref
/**
* 自定义的ref,实现属性和内部变量的数据转换
* @param { reactive } props 组件的属性
* @param { object } context 组件的上下文
* @param { number } delay 延迟刷新的时间,单位:毫秒,默认:0
* @param { string } name 要对应的属性名称,默认:modelValue
* @returns 自定义的ref
*/
export const debounceRef = (props, context, delay = 0, name = 'modelValue') => {
let _value = props[name] // 计时器
let timeout
// 是否输入状态。输入时取 value;输入完毕取 modelValue 属性
let isInput = false
return customRef((track, trigger) => {
return {
get () {
track()
if (isInput) {
// console.log(isInput)
return _value
} else {
// console.log(isInput)
return props[name]
}
},
set (newValue) {
isInput = true
_value = newValue // 绑定值
trigger() // 组件内部刷新模板
clearTimeout(timeout) // 清掉上一次的计时
timeout = setTimeout(() => {
// 修改 modelValue 属性
context.emit(`update:${name}`, newValue) // 提交给父组件
// 用于区分是哪个组件触发的事件。
context.emit('my-change', newValue, props.controlId, props.colName)
isInput = false
}, delay)
}
}
})
}

封装各种表单子控件

按照原子性原则,子控件封装的比较细,直接看图:

代码有点多,不一一介绍了,感兴趣的可以看源码。

封装表单控件

基础工作做好之后,我们就可以封装 el-form 了。

定义属性

依据 el-form 的属性我们定义几个关键性属性

介绍属性
/**
* 表单控件需要的属性
*/
export const formProps = {
modelValue: Object, // 完整的model
partModel: Object, // 根据选项过滤后的model
miniModel: Object, // 精简的model
/*
* 自定义子控件 key:value形式
* * key: 编号。1:插槽;100-200:保留编号
* * value:string:标签;函数:异步组件,类似路由的设置
*/
customerControl: { // 自定义的表单子组件
type: Object,
defaule: () => {}
},
colOrder: { // 表单字段的排序的依据
type: Array,
default: () => []
},
formColCount: { // 表单的列数
type: Number,
default: 1
},
reload: {
type: Boolean, // 是否重新加载配置,需要来回取反
default: false
},
itemMeta: {
type: Object, // 表单子控件的属性
default: () => {}
},
ruleMeta: { // 验证信息
type: Object,
default: () => {}
},
formColShow: { // 数据变化,联动组件是否显示
type: Object,
default: () => {}
}
}

定义内部model

一般一个 model 就可以,只是这里做了一个组件联动的,那么如果只需要获取可见的组件的值呢,于是做了局部model。

实现多行多列和布局调整

采用 el-col 实现,通过控制 span 来实现多列,所以理论上最多支持24列,当然这个要看屏幕宽度了。

/**
* 处理一个字段占用几个td的需求
* @param { object } props 表单组件的属性
* @returns
*/
const getColSpan = (props) => {
// 确定一个组件占用几个格子
const formColSpan = reactive({}) // 表单子控件的属性
const formItemProps = props.itemMeta // 根据配置里面的colCount,设置 formColSpan
const setFormColSpan = () => {
const formColCount = props.formColCount // 列数
const moreColSpan = 24 / formColCount // 一个格子占多少份 if (formColCount === 1) {
// 一列的情况
for (const key in formItemProps) {
const m = formItemProps[key]
if (typeof m.colCount === 'undefined') {
formColSpan[m.controlId] = moreColSpan
} else {
if (m.colCount >= 1) {
// 单列,多占的也只有24格
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount < 0) {
// 挤一挤的情况, 24 除以 占的份数
formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
}
}
}
} else {
// 多列的情况
for (const key in formItemProps) {
const m = formItemProps[key]
if (typeof m.colCount === 'undefined') {
formColSpan[m.controlId] = moreColSpan
} else {
if (m.colCount < 0 || m.colCount === 1) {
// 多列,挤一挤的占一份
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount > 1) {
// 多列,占的格子数 * 份数
formColSpan[m.controlId] = moreColSpan * m.colCount
}
}
}
}
} return {
formColSpan,
setFormColSpan
}
}

首先计算一下一列要用多少个span,也就是用24除以列数。

然后判断是不是单列,单列要处理多个组件占用一个位置的需求,多列要处理一个组件占用多个位置的需求。

实现扩展

表单子控件可以多种多样,无法完全封装进入表单控件,那么就需要表单控件支持子控件的扩展。

这里要感谢 vue 的动态组件功能,让扩展子控件变得非常方便。

我们使用 component 和动态组件来实现表单子控件的加载。

<component
:is="formItemListKey[getCtrMeta(ctrId).controlType]"
v-model="formModel[getCtrMeta(ctrId).colName]"
v-bind="getCtrMeta(ctrId)"
@my-change="myChange">
</component>
export const formItemList = {
// 文本类 defineComponent
'el-form-text': defineAsyncComponent(() => import('./t-text.vue')),
'el-form-area': defineAsyncComponent(() => import('./t-area.vue')),
'el-form-url': defineAsyncComponent(() => import('./t-url.vue')),
'el-form-password': defineAsyncComponent(() => import('./t-password.vue')),
// 数字
'el-form-number': defineAsyncComponent(() => import('./n-number.vue')),
'el-form-range': defineAsyncComponent(() => import('./n-range.vue')),
// 日期、时间
'el-form-date': defineAsyncComponent(() => import('./d-date.vue')),
'el-form-datetime': defineAsyncComponent(() => import('./d-datetime.vue')),
'el-form-year': defineAsyncComponent(() => import('./d-year.vue')),
'el-form-month': defineAsyncComponent(() => import('./d-month.vue')),
'el-form-week': defineAsyncComponent(() => import('./d-week.vue')),
'el-form-time-select': defineAsyncComponent(() => import('./d-time-select.vue')),
'el-form-time-picker': defineAsyncComponent(() => import('./d-time-picker.vue')),
// 选择、开关
'el-form-checkbox': defineAsyncComponent(() => import('./s-checkbox.vue')),
'el-form-switch': defineAsyncComponent(() => import('./s-switch.vue')),
'el-form-checkboxs': defineAsyncComponent(() => import('./s-checkboxs.vue')),
'el-form-radios': defineAsyncComponent(() => import('./s-radios.vue')),
'el-form-select': defineAsyncComponent(() => import('./s-select.vue')),
'el-form-selwrite': defineAsyncComponent(() => import('./s-selwrite.vue')),
'el-form-select-cascader': defineAsyncComponent(() => import('./s-select-cascader.vue')) } /**
* 动态组件的字典,便于v-for循环里面设置控件
*/
export const formItemListKey = {
// 文本类
100: formItemList['el-form-area'], // 多行文本
101: formItemList['el-form-text'], // 单行文本
102: formItemList['el-form-password'], // 密码
103: formItemList['el-form-text'], // 电话
104: formItemList['el-form-text'], // 邮件
105: formItemList['el-form-url'], // url
106: formItemList['el-form-text'], // 搜索
// 数字
120: formItemList['el-form-number'], // 数字
121: formItemList['el-form-range'], // 滑块
// 日期、时间
110: formItemList['el-form-date'], // 日期
111: formItemList['el-form-datetime'], // 日期 + 时间
112: formItemList['el-form-month'], // 年月
113: formItemList['el-form-week'], // 年周
114: formItemList['el-form-year'], // 年
115: formItemList['el-form-time-picker'], // 任意时间
116: formItemList['el-form-time-select'], // 选择固定时间
// 选择、开关
150: formItemList['el-form-checkbox'], // 勾选
151: formItemList['el-form-switch'], // 开关
152: formItemList['el-form-checkboxs'], // 多选组
153: formItemList['el-form-radios'], // 单选组
160: formItemList['el-form-select'], // 下拉
161: formItemList['el-form-selwrite'], // 下拉多选
162: formItemList['el-form-select-cascader'] // 下拉联动
}

需要扩展子控件的时候,我们只需要向字典(dict)里面添加需要的组件即可,然后设置一个新的编号。

  // 添加临时动态组件
formProps.customerControl = {
300: 'el-transfer'
}
// 设置表单字段
childMeta.select.controlType = 300

为啥用编号?虽然编号不易读,但是编号稳定,而且灵活。如果我们要基于ant design Vue 封装控件的话,我可以直接用编号,但是如果用名称的话,那么要不要区分 el- 和 a- 呢?

实现数据联动

联动分为数据联动,和组件联动,数据联动可以依赖UI库的组件来实现,或者依赖Vue的数据的响应性来实现。

比如常见的省市区县联动,我们可以用 el-cascader。

如果需要使用多个组件的话,我们可以监听组件的值的变化,然后获取数据绑定下一个组件的options。

// 数据联动
watch (() => model.provinces, (v1, v2) => {
console.log('监听值的变化', v1)
const arr = [
{"value": 1 + v1, "label": "多选 选项一" + v1},
{"value": 2 + v1, "label": "多选 选项二" + v1}
] childMeta.city.optionList.length = 0
childMeta.city.optionList.push(...arr)
})

Vue 就是数据驱动的,所以联动的话也是直接监听value的改变即可,不用像以前那样要设置change事件了。

实现组件联动

组件联动,就是一个组件的值发生变化,影响其他组件的显示状态。

比如在注册的时候,需要选择企业用户还是个人用户。

如果是企业用户,需要添加企业名称(以及相关信息);

如果是个人注册那么只需要填写个人姓名即可。

这样表单里面显示的组件就要随之变化。

对于这类的需求,我们可以配置一下 formColShow 属性。

    "formColShow": {
"90": { // 组件ID
"1": [90, 101, 100, 102, 105], // 组件值对应的需要显示的组件ID,下同
"2": [90, 120, 121],
"3": [90, 110, 114, 112, 113, 115, 116],
"4": [90, 150, 151, 152, 153, 160, 162]
}
},

配置好之后就可以实现了,表单控件内部代码会做一个 watch 监听:

  // 数据变化,联动组件的显示
if (typeof props.formColShow !== 'undefined') {
for (const key in props.formColShow) {
const ctl = props.formColShow[key]
const colName = props.itemMeta[key].colName
// 监听组件的值,有变化就重新设置局部model
watch(() => formModel[colName], (v1, v2) => {
if (typeof ctl[v1] === 'undefined') {
// 没有设定,显示默认组件
setFormColSort()
} else {
// 按照设定显示组件
setFormColSort(ctl[v1])
// 设置部分的 model
createPartModel(ctl[v1])
}
})
}

json格式

整个表单是依据 json 动态渲染出来的,那么json格式是啥样的呢?分为两个部分,一个是表单控件自己需要的属性,另一个是表单子控件需要的属性,还有验证规则等。

{
"formTest": {
"baseProps": { // 表单控件自己的属性
"formColCount": 1, // 列数
"colOrder": [ // 需要显示的组件的ID
90, 101, 102,
110, 111, 114, 112, 113, 115, 116,
120, 121, 100,
150, 151, 152, 153,
160, 162
]
},
"formColShow": { // 组件联动的信息
"90": { // 触发的组件
"1": [90, 101, 100, 102, 105], // 组件值对应的需要显示的组件的ID
"2": [90, 120, 121],
"3": [90, 110, 114, 112, 113, 115, 116],
"4": [90, 150, 151, 153, 152, 160, 162]
}
},
"ruleMeta": { // 验证规则
"101": [ // 表单子控件的ID,下面是验证规则
{ "trigger": "blur", "message": "请输入活动名称", "required": true },
{ "trigger": "blur", "message": "长度在 3 到 5 个字符", "min": 3, "max": 5 }
]
},
"itemMeta": { // 表单子控件的属性
"90": {
"controlId": 90,
"colName": "kind",
"label": "分类",
"controlType": 153,
"isClear": false,
"defaultValue": "",
"placeholder": "分类",
"title": "编号",
"optionList": [
{"value": 1, "label": "文本类"},
{"value": 2, "label": "数字类"},
{"value": 3, "label": "日期类"},
{"value": 4, "label": "选择类"}
],
"colCount": 1
},
"100": {
"controlId": 100,
"colName": "area",
"label": "多行文本",
"controlType": 100,
"isClear": false,
"defaultValue": 1000,
"placeholder": "多行文本",
"title": "多行文本",
"colCount": 1
},
...
}
}
}

遍历子控件

因为子控件都封装好了,所以只需要简单遍历即可:

  <el-form
:model="formModel"
:rules="rules"
ref="formControl"
:inline="false"
class="demo-form-inline"
label-suffix=":"
label-width="130px"
size="mini"
>
<el-row>
<!--不循环row,直接循环col,放不下会自动往下换行。-->
<el-col
v-for="(ctrId, index) in formColSort"
:key="'form_'+index"
:span="formColSpan[ctrId]"
><!--:prop="getCtrMeta(ctrId).colName"-->
<el-form-item
:label="getCtrMeta(ctrId).label"
:prop="getCtrMeta(ctrId).colName"
>
<!--判断要不要加载插槽-->
<template v-if="getCtrMeta(ctrId).controlType === 1">
<!--<slot :name="ctrId">父组件没有设置插槽</slot>-->
<slot :name="getCtrMeta(ctrId).colName">父组件没有设置插槽</slot>
</template>
<!--表单item组件,采用动态组件的方式-->
<template v-else>
<component
:is="dictControl[getCtrMeta(ctrId).controlType]"
v-model="formModel[getCtrMeta(ctrId).colName]"
v-bind="getCtrMeta(ctrId)"
@my-change="myChange">
</component>
</template>
</el-form-item>
</el-col>
</el-row>
</el-form>

篇幅有限无法一一介绍,其他部分可以看源码。

源码

https://gitee.com/naturefw/nf-vite2-element

基于 el-form 封装一个依赖 json 动态渲染的表单控件的更多相关文章

  1. Vue3组件(九)Vue + element-Plus + json = 动态渲染的表单控件

    一个成熟的表单 表单表单,你已经长大了,你要学会: 动态渲染 支持单列.双列.多列 支持调整布局 支持表单验证 支持调整排列(显示)顺序 依据组件值显示需要的组件 支持 item 扩展组件 可以自动创 ...

  2. 基于 element-plus 封装一个依赖 json 动态渲染的查询控件

    前情回顾 基于 el-form 封装一个依赖 json 动态渲染的表单控件 Vue3 封装第三方组件(一)做一个合格的传声筒 功能 使用 vue3 + element-plus 封装了一个查询控件,专 ...

  3. 如何给动态添加的form表单控件添加表单验证

    最近使用jQuery Validate做表单验证很方便,api地址为http://www.runoob.com/jquery/jquery-plugin-validate.html 但是在使用的时候也 ...

  4. C#中缓存的使用 ajax请求基于restFul的WebApi(post、get、delete、put) 让 .NET 更方便的导入导出 Excel .net core api +swagger(一个简单的入门demo 使用codefirst+mysql) C# 位运算详解 c# 交错数组 c# 数组协变 C# 添加Excel表单控件(Form Controls) C#串口通信程序

    C#中缓存的使用   缓存的概念及优缺点在这里就不多做介绍,主要介绍一下使用的方法. 1.在ASP.NET中页面缓存的使用方法简单,只需要在aspx页的顶部加上一句声明即可:  <%@ Outp ...

  5. Flutter Form表单控件超全总结

    注意:无特殊说明,Flutter版本及Dart版本如下: Flutter版本: 1.12.13+hotfix.5 Dart版本: 2.7.0 Form.FormField.TextFormField是 ...

  6. 基于CkEditor实现.net在线开发之路(3)常用From表单控件介绍与说明

    上一章已经简单介绍了CKEditor控件可以编写C#代码,然后可以通过ajax去调用,但是要在网页上面编写所有C#后台逻辑,肯定痛苦死了,不说实现复杂的逻辑,就算实现一个简单增删改查,都会让人头痛欲裂 ...

  7. C# 添加Excel表单控件(Form Controls)

    在Excel中,添加的控件可以和单元格关联,我们可以操作控件来修改单元格的内容,在下面的文章中,将介绍在Excel中添加几种不同的表单控件的方法,包括: 添加文本框(Textbox) 单选按钮(Rad ...

  8. 基于Extjs的web表单设计器 第二节——表单控件设计

    这一节介绍表单设计器的常用控件的设计. 在前面两章节的附图中我已经给出了表单控件的两大分类:区域控件.常用控件.这里对每个分类以及分类所包含的控件的作用进行一一的介绍,因为它们很重要,是表单设计器的基 ...

  9. Asp.Net Form表单控件的回车默认事件

    当form表单文本框控件在收到回车事件时,默认会触发表单内第一个可提交按钮的事件,但业务中可能要求有其它控件进行提交,而不是这个默认的 这时需要脚本控件事件冒泡传递取消回事事件. $(document ...

随机推荐

  1. 世界国省市区SQL语句(mysql)

    CREATE TABLE loctionall ( country VARCHAR(40) , provice VARCHAR(40) , city VARCHAR(40) , CONSTRAINT ...

  2. ASP.NET Core与Redis搭建一个简易分布式缓存

    ​本文主要介绍了缓存的概念,以及如何在服务器内存中存储内容.今天的目标是利用IDistributedCache来做一些分布式缓存,这样我们就可以横向扩展我们的web应用程序. 在本教程中,我将使用Re ...

  3. 归并排序(JAVA语言)

    public class merge { public static void main(String[] args) { // TODO Auto-generated method stub int ...

  4. VS2015上OpenCV-2.4.13安装与Hi35xx .jpg/.bmp格式转.bgr格式开发

    因为Hi3559AV100后期深度学习开发需要用到.bgr格式的图片,而目前在手的一般为.jpg或.bmp格式的图片,下面随笔将给出基于OpenCV-2.4.13的格式转换,实现Hi35xx .jpg ...

  5. 【Linux学习笔记0】-虚拟机运行CentOS(VMware12+CentOS)

    目录 一,资源 二,VMware12安装 记录自己学习linux的过程.这将会是一个系列,本文是该系列的第一部分,主要记录虚拟机(VMware12)及对应操作系统(CentOS)的安装过程. 虚拟机( ...

  6. Python - 关于类(self/cls) 以及 多进程通讯的思考

    Python-多进程中关于类以及类实例的一些思考 目录 Python-多进程中关于类以及类实例的一些思考 1. 背景 2. Python 类中的函数 - staticmethod / classmet ...

  7. 批量实现SSH无密码登陆认证脚本

    批量实现SSH无密码登陆认证脚本 问题背景 使用为了让linux之间使用ssh不需要密码,可以采用了数字签名RSA或者DSA来完成.主要使用ssh-key-gen实现. 1.通过 ssh-key-ge ...

  8. [树形DP]没有上司的晚会

    没 有 上 司 的 晚 会 没有上司的晚会 没有上司的晚会 题目描述 Ural大学有N个职员,编号为1~N.他们有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司.每个职 ...

  9. SpringCloud LoadBalancer灰度策略实现

    如何使用 Spring Cloud 2020 中重磅推荐的负载均衡器 Spring Cloud LoadBalancer (下文简称 SCL),如何扩展负载均衡策略? 你将从本文中获取到答案 快速上手 ...

  10. Spring Boot XSS 攻击过滤插件使用

    XSS 是什么 XSS(Cross Site Scripting)攻击全称跨站脚本攻击,为了不与 CSS(Cascading Style Sheets)名词混淆,故将跨站脚本攻击简称为 XSS,XSS ...