Vue2 Element Schema Form 配置式生成表单的实现
前置知识点
为了实现一个Schema Form
配置式表单的生成,需要了解一部分前置知识点。
Component
vue
提供了一个内置组件 Component
,用来动态渲染组件,举个例子,本篇文章以Element UI
为例,假设我们全局注册了Element UI
的组件,那么下面一段代码
<Component is="el-input"></Component>
将被渲染为el-input
组件。
好了,最重要的实现知识点已经完了,只要了解了这一点,我这个Schema Form
的实现思路,你就能完全看懂了,就这么简单。
但是为了我们表单的易用性,我们再了解一点儿其他的。
比如Component
组件并非只能接收一个字符串,渲染全局组件,同时可以接收一个Component
组件,也就是说,当我们Element UI
组件没有全局注册的时候,我们可以通过import { ElInput } from 'element'
,传递给Component
组件,同样可以完成渲染一个el-input
组件的功能。
$attrs
第二个知识点,vue
的属性透传
,假如你有这么一个疑惑 —— 我对el-input
进行了二次封装
,那el-input
接收的props
我是否需要在二次封装的组件
中进行props的定义,再逐一传递给el-input
才能生效。
如果有这样的疑惑那么$attrs
可以帮你,对于二次封装的组件,通过定义v-bind="$attrs"
,传递给父组件的属性即可透传给el-input
,于是就有了这么一个疑问,$attrs
可以绑定给v-bind
,那么一个普通的对象可以吗,答案是可以的。
这是我们 schema form
变得好用的第二个重要的知识点。
$listeners
与 $attrs
相同,做事件透传的,通过v-on
绑定。
以上就是我认为,实现Schema Form
配置式生成表单所需要掌握的全部知识及扩展思考,接下来,我们简单完成一个配置式表单.
表单的结构是什么样的
1、我可能希望对表单进行一部分特殊的处理,所以,包一层div
总是没错的,当然,这是我的习惯,你们可以自行选择。
2、既然是表单,那一定被el-form
包裹。
3、为了配置式布局的需要,我希望引入el-row el-col
栅格系统
可以参考我新的文章 基于Vue2、Element开发的自适应栅格组件 弄一个更简单易用的栅格组件
4、无论如何完善表单,必然不可能满足所有要求,那最简单的方式就是抛出slot
5、有些时候我希望渲染文本呢?有些时候我希望渲染字段的值而不涉及任何组件呢?
6、最后才是渲染对应的组件
那么,一个表单的结构化雏形就完成了。
<div class="schema-form">
<el-form>
<el-row>
<el-col v-for="item in config" :key="item.key">
<el-form-item>
<slot v-if="item.component === 'slot'" :name="item.slotName"></slot>
<div v-else-if="item.component === 'innerText'"></div>
<template v-else>
渲染对应组件,但需要对某些组件特殊处理
</template>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
从上面的结构中,我们再来思考,我们的配置config
数组的字段结构应该是什么样的。
配置数组数据结构
1、el-form
可能需要一些字段注入,作为最外层的组件,将传入schema form
的字段都给它
2、el-form
需要传入一个数据源,这个数据源可以内部定义再传给外部,也可以传入一个对象,利用对象的特性做双向绑定,我选择了后者
3、el-col
项可能有时不显示,所以config
上扩展一个字段hidden
,用于控制该项是否显示。
4、el-form-item
的属性透传
5、innerText
的样式定义
一些特殊的描述出来了,其余的再赘述,那么,config
的数据结构就是这样
{
key: '', // 当前字段的 key 值,同时会传到 el-form-item 的prop,不传数据验证和重置会失效
label: '', // 当前 el-form-item 的label
hidden: '', // 当前表单项是否展示
labelWidth: '', // el-form-item 的label宽度
component: '', // 支持 slot、innerText、Component,渲染成什么
slotName: '', // compoment 为 slot 时,该值为对应slot的名字供外部使用
innerClass: '', // component 为 innerText 时,给文本的样式,通常为全局样式
innerStyle: '', // 同上
innerText: '', // component 为 innerText 时,优先显示的文本,否则会显示当前的字段值
itemProps: '', // 注入到 el-form-item 的属性
props: '', // 当 component 为渲染组件时,注入到渲染组件当中的属性
listeners: '', // 当 component 为渲染组件时,注入到渲染组件当中的事件
}
当把这些实现之后,其余需要扩展的功能可以自行实现,我这里也只是在业务中的扩展,并非完善的。
于是,我们的表单组件就变成了这样
<template>
<div class="nx-form">
<el-form
ref="form"
v-bind="$attrs"
:model="source"
class="nx-form"
>
<el-row :gutter="20">
<el-col
v-for="item in config"
:key="item.key"
:span="item.hidden ? 0 : item.span || 8"
>
<el-form-item
v-if="!item.hidden"
:label="item.label"
:prop="item.key"
:label-width="item.labelWidth || labelWidth || '120px'"
v-bind="item.itemProps"
>
<slot v-if="item.component === 'slot'" :name="item.slotName"></slot>
<div
v-else-if="item.component === 'innerText'"
:class="item.innerClass"
:style="item.style"
>
{{ item.innerText || source[item.key] }}
</div>
<template v-else>
<div class="nx-flex-align-center">
<component
:is="item.component"
v-model="source[item.key]"
style="width: 100%;flex: 1;"
v-bind="item.props"
v-on="item.listeners"
>
<template v-if="item.component === 'el-radio-group'">
<el-radio
v-for="(radio, radioIndex) in item.data"
:key="radioIndex"
style="margin-top: 10px;"
:label="radio.value"
:disabled="radio.disabled"
>
{{ radio.label }}
</el-radio>
</template>
<template v-if="item.component === 'el-checkbox-group'">
<el-checkbox
v-for="(checkbox, checkboxIndex) in item.data"
:key="checkboxIndex"
:label="checkbox.value"
>
{{ checkbox.label }}
</el-checkbox>
</template>
<template v-if="item.component === 'el-select'">
<el-option
v-for="(option, optionIndex) in item.data"
:key="optionIndex"
:label="option.label"
:value="option.value"
></el-option>
</template>
</component>
<div
v-if="item.after"
class="nx-form__after"
>
{{ item.after }}
</div>
</div>
<div
v-if="item.tips"
class="nx-form__tips"
v-html="item.tips"
></div>
</template>
</el-form-item>
<!-- <div
v-if="item.tips"
:style="{ marginLeft: item.labelWidth || '120px' }"
class="nx-form__tips"
v-html="item.tips"
></div> -->
</el-col>
</el-row>
</el-form>
</div>
</template>
export default {
name: 'NxForm',
components: {},
props: {
config: {
type: Array,
default: () => []
},
source: {
type: Object,
default: () => ({})
}
},
methods: {
resetFields() {
this.$refs.form.resetFields();
},
clearValidate() {
this.$refs.form.clearValidate();
},
async validate() {
const valid = await this.$refs.form.validate();
return valid;
}
}
};
可以看到,我扩展了一个data
字段,用来作为el-select
el-checkbox-group
el-radio-group
的数据源,这些特殊的组件单独列出就行了,基本上也之后有这么几个。
methods
里也只是为了方便添加了一些常用的form
方法,基本不做逻辑处理。
现在看一下,这样的配置可以生成怎样的表单
注意:我这里定义了一部分全局样式,比如在
schema form
下的组件,全部占满整格,使其比较美观。
生成这样一个常用的筛选项表单
<template>
<div>
<nx-form
ref="searchForm"
:source="searchForm"
:config="searchConfig"
>
<div slot="search">
<el-button type="primary" @click="$refs.nxTable.search(1)">查询</el-button>
<el-button @click="reset()">重置</el-button>
</div>
</nx-form>
</div>
</template>
<script>
export default {
data() {
searchForm: {
keyWord: '',
newsType: '',
newsStatus: '',
publishTime: [],
createTime: []
},
},
computed: {
searchConfig() {
return [
{
key: 'keyWord',
component: 'el-input',
span: 8,
label: '关键字',
props: {
placeholder: '标题/创建人',
clearable: true
}
},
{
key: 'newsType',
component: 'el-select',
span: 8,
label: '类型:',
props: {
placeholder: '请选择',
clearable: true
},
data: this.newsTypes
},
{
key: 'newsStatus',
component: 'el-select',
span: 8,
label: '状态:',
props: {
placeholder: '请选择',
clearable: true
},
data: statusTypes
},
{
key: 'publishTime',
component: 'el-date-picker',
label: '发布时间:',
span: 8,
props: {
clearable: true,
valueFormat: 'timestamp',
type: 'datetimerange',
defaultTime: ['00:00:00', '23:59:59'],
rangeSeparator: '-',
startPlaceholder: '请选择开始时间',
endPlaceholder: '请选择结束时间'
}
},
{
key: 'createTime',
component: 'el-date-picker',
label: '创建时间:',
span: 8,
props: {
clearable: true,
valueFormat: 'timestamp',
type: 'datetimerange',
defaultTime: ['00:00:00', '23:59:59'],
rangeSeparator: '-',
startPlaceholder: '请选择开始时间',
endPlaceholder: '请选择结束时间'
}
},
{
component: 'slot',
slotName: 'search',
span: 8,
labelWidth: '0'
}
];
}
}
}
</script>
其余的找了一些,也没啥特别的,就不贴了,简单来说,这个百来行的组件,应用在多个大型项目当中,易用性,扩展性,没有出现什么问题,配合我们自定义的其他table
组件、dialog
组件,十分钟就可以实现一个完整的B端增删改查。
值得一提的是,为了表单的易用性,我们基本上所有的自定义组件都支持了使用v-model
做数据绑定,达到简单易用的效果。
结语
代码是找了一个很久的项目写的一个思路,之后的项目中都有改进优化,大致只是分享一个思路,如果有什么想法的,欢迎指正。
补充
有几位朋友对 listeners
比较疑惑,我没讲的太仔细,补充一下。
<component
:is="item.component"
v-model="source[item.key]"
style="width: 100%;flex: 1;"
v-bind="item.props"
v-on="item.listeners"
>
可以看到每一个 item
的 listeners
被透传到了 Component
组件上,这里用 el-input
举例,我们渲染一个单个的input。
[
{
key: 'input',
component: 'el-input',
span: 24,
labelWidth: '120px',
label: '测试 Input',
props: {
placeholder: '请输入...'
},
listeners: {
change: (e) => {
}
}
}
]
上面这段代码中,listeners
中传递了 change
事件,相当于
<el-input @change="change"></el-input>
主要是为了满足复杂情况,实际使用还是比较少的。
闲言碎语
来这篇文章的朋友们大多应该是因为另一篇文章下的争论,说实话,我很气愤,"若批评无自由,则赞美无意义",我实在不太理解,为什么技术社区要搞政治正确那一套,我是个暴躁老哥,最为痛恨的就是政治正确那一套,好就是好,坏就是坏,如果缺点不被指出,则社区论坛失去了意义,还是一个人在思考。
我刚入行技术很菜的时候,空有想法而难以实践,因为自身不知道知识点到底缺在哪儿,想搜索都不知道关键词,搜索出来之后还要在茫茫文章里屎里淘金,我不否定"写文章不是为了给你看的,我只是为了记录"这种说法,可是大部分人取一个震惊体的标题,引人进去,到头来发现不过是一个很一般甚至莫名其妙的东西,难道不会很气愤吗?
以前我就很感谢评论区的老哥们,对于一部分很震惊体的文章,我都会直接拉到最后看评论,看一下评论区的老哥们如何评价,基本上对于我来说省去了很多时间,因为确实很多文章,不值得一观;现在,我技术提升了一些,我逛着社区看到一个我做过已经很成熟的东西,我依然想要了解一下别人是如何实现的,是否有借鉴点能帮助我改进我的代码,以那篇争论的文章为例,说实话,我看到一半我就不想看了,我第一想法是"这什么破玩意儿",说实话,不是因为他菜而带来的辱骂,而是他明明有实力有想法能折腾,却让这样的东西存在了一年,还取了这么个震惊标题发出来,实在令我气愤,于是带上了优化实现技术点,语气不佳的回了一句,然后,争端开始了。
回到上面说的,技术社区是应该政治正确的地方吗?哈哈哈,如果社区是政治正确的,那我认为也没有存在的必要了,我入行以来,但凡对我有过帮助的,技术上对我有过指点的,哪怕只是对我代码格式规范提的意见,只是是好的,无论语气,我都会至少请一杯奶茶,或者请吃饭,所以我渐渐的和那些大佬关系越来越好,也从他们身上学到了更多的东西,我也遇到过那种政治正确的人,就是那种你不能逼逼他的代码,哪怕你配好了eslint,他也只会想办法去把这玩意儿弄掉,而不是规范化自己的代码,后来他被开了,我想如果我当初也这么政治正确的话,我也被开了。
其实我也曾经想过写文章分享很多知识点,一方面是因为自己在解决这些问题的时候看了很多文章,几乎都讲的很片面,另一方面也是对自己技术的一个记录,但总是写着写着就觉得不尽人意,也担心会写的不好误人子弟,我不怕有人批评我做的不好,我担心的是别人看了我的文章,还是什么都没弄出来,浪费别人的时间,给组员们的公用组件也一样,譬如axios的封装,vue2、vue3的、js、ts的,都是在背后测试了各种可能会出现的情况,模拟了很多场景,确定这样的实现至少可以覆盖到绝大部分场景,覆盖不到的场景也给了自定义的能力才会开放出来,也尽量少的加入一些过于私人化的东西,减少变成公司个人组件库,换一家公司重学一个组件库这种情况;我始终认为,一个被公开出来的东西,至少要解决掉大部分的问题,而不是明显可以感知到的,这个东西不配作为一个全局组件。
我不想做一个政治正确的人,甚至不想做一个"有素质"的人,所以,对于不好的习惯,不好的实现指正,得到的回复是PUA的话,如果是我的组员,我只会让他滚;我经常说,我不介意菜的人,我也不介意笨的人,我更不介意教又菜又笨的人,但是,你要是不愿意学,不考量我的建议就拒绝的话,我会很介意让你待在我的组内。
转载自:https://juejin.cn/post/7099644339449659429