教你如何基于Element-UI通过配置来生成表单__Vue.js
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利
前言
作为一名sass系统开发的前端工程师,少不了要和表单打交道,我们选择的UI库是Element-UI,但是使用久了就感觉是在复制粘贴,而且特别长,为了解决这种重复枯燥的问题。所以通过配置组件来了。
解决思路
就是根据配置规则,通过vue里面的render函数,把对应的规则渲染出来。好了,直接上代码
代码实现
### index.js
import { deepClone } from '@/libs/util'
import defaultController from './default.controller'
import selectController from './select.controller'
import datePickerController from './datePicker.conrtroller'
export default {
name: 'v-form',
props: {
visible: {
type: Boolean,
default: false
},
value: {
type: Object,
default: () => {}
},
rules: {
type: Object,
default: () => {}
},
schema: {
type: Array,
default: () => []
}
},
data () {
return {
formData: {}
}
},
created () {
this.defaultProps = {
clearable: true,
multiple: false
}
},
computed: {
len () {
return this.schema.length
},
mergeProps () {
return [this.value, this.visible]
}
},
methods: {
get () {
this.$emit('input', this.formData)
},
validate (fn) {
this.$refs.form.validate(valid => {
fn && typeof fn === 'function' && fn(valid)
})
},
resetFields () {
this.$refs.form.resetFields()
},
renderController (h, item) {
if ('render' in item) {
return item.render.call(this, h, item)
}
switch (item.controller) {
case 'el-date-picker':
return datePickerController.call(this, h, item)
case 'el-select':
return selectController.call(this, h, item)
default:
return defaultController.call(this, h, item)
}
},
renderSpec (h) {
return h('v-form-item', {
props: {
span: '12'
}
}, [])
},
renderValidate (h, item, isRule) {
if (isRule) {
return h('el-form-item', {
props: {
prop: item.field,
rules: this.rules[item.field] || {}
}
}, [
this.renderController(h, item)
])
}
return this.renderController(h, item)
},
renderFormItem (h, item, span = '12') {
const isRule = item.field && this.rules && item.field in this.rules
return h('v-form-item', {
props: {
span,
required: isRule || item.required || false,
label: item.label || '',
tip: item.tip || ''
}
}, [
this.renderValidate(h, item, isRule)
])
},
initSchema (h) {
const children = []
this.schema.reduce((pre, item, currentIndex) => {
if ('span' in item && item.span === 24) {
if (pre.length < 1) {
children.push(
h('el-row', {}, [
this.renderFormItem(h, item, '24')
])
)
return []
}
if (pre.length === 1) {
pre.push(this.renderSpec(h))
children.push(h('el-row', {}, pre))
children.push(
h('el-row', {}, [
this.renderFormItem(h, item, '24')
])
)
return []
}
} else {
pre.push(
this.renderFormItem(h, item)
)
if (pre.length === 2) {
children.push(h('el-row', {}, pre))
return []
}
if (currentIndex + 1 === this.len) {
pre.push(this.renderSpec(h))
children.push(h('el-row', {}, pre))
}
return pre
}
}, [])
children.unshift(this.$slots.header)
children.push(this.$slots.footer)
return children
}
},
render (h) {
return h('el-form', {
ref: 'form',
props: {
model: this.formData,
hideRequiredAsterisk: true,
labelPosition: 'top',
rules: this.rules
}
}, this.initSchema(h))
},
watch: {
mergeProps: {
handler ([value, visible]) {
if (visible) {
this.formData = deepClone(value)
} else {
this.timer = setTimeout(() => {
this.schema = []
this.formData = Object.create(null)
clearTimeout(this.timer)
}, 300)
}
},
deep: true,
immediate: true
}
}
}
加载下拉组件controller
### select.controller.js
export default function (h, item) {
const props = Object.assign({}, this.defaultProps, item.props, {
value: this.formData[item.field]
})
const attrs = Object.assign({}, item.attrs)
const renderOptions = []
if (item.store && item.store.length) {
item.store.map(option => {
renderOptions.push(h('el-option', {
props: option
}))
})
}
return h(item.controller, {
class: 'w100',
props,
attrs,
on: Object.assign({}, item.on || {}, {
'input': v => {
this.formData[item.field] = v
item.on && item.on.input && item.on.input.call(this, v, item)
}
})
}, renderOptions)
}
加载日期型的controller
### datePicker.conrtroller.js
import { formatDate } from '@/libs/util'
export default function (h, item) {
const props = Object.assign({}, this.defaultProps, item.props, {
value: this.formData[item.field]
})
const attrs = Object.assign({}, item.attrs)
return h(item.controller, {
class: 'w100',
props,
attrs,
on: Object.assign({}, item.on || {}, {
'input': v => {
if (v) {
this.formData[item.field] = formatDate(v)
} else {
this.formData[item.field] = v
}
item.on && item.on.input && item.on.input.call(this, v, item)
}
})
})
}
加载默认的controller
### default.controller.js
export default function (h, item) {
const props = Object.assign({}, this.defaultProps, item.props, {
value: this.formData[item.field]
})
const attrs = Object.assign({}, item.attrs)
return h(item.controller, {
props,
attrs,
on: Object.assign({}, item.on || {}, {
'input': v => {
this.formData[item.field] = v
item.on && item.on.input && item.on.input.call(this, v, item)
}
})
})
}
util工具提供两个方法
### util.js
/**
* 对象拷贝方法
* @param { any } data
*/
export const deepClone = (data) => {
if (typeOf(data) === 'array') {
return data.map(deepClone)
} else if (data && typeof data === 'object') {
const obj = Object.assign({}, data)
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue
}
if (typeof obj[key] === 'object') {
obj[key] = deepClone(obj[key])
}
}
return obj
} else {
return data
}
}
/**
* 日期格式化
* @param { Date } date
* @param { String } fmt
*/
export const formatDate = (date, fmt = 'YYYY-MM-DD HH:mm:ss') => {
return dayjs(date).format(fmt)
}
还涉及到了一个排版组件v-form-item
<template>
<el-col
:class="['v-form-item', required && 'v-form-item__required']"
:span="span"
>
<el-form-item :label="label" :prop="prop">
<slot></slot>
<div class="v-form-item__tip line-clamp fs12">{{ tip }}</div>
</el-form-item>
</el-col>
</template>
<script>
export default {
name: 'v-form-item',
props: {
span: {
type: String,
default: '12'
},
required: {
type: Boolean,
default: false
},
prop: {
type: String
},
label: {
type: String,
default: ''
},
tip: {
type: String,
default: ''
}
}
}
</script>
<style lang="scss">
.el-form--label-top .el-form-item__label {
padding: 0 !important;
}
.v-form-item__required {
.el-form-item__label {
&::after {
content: '*';
position: relative;
left: 5px;
}
}
}
.el-form-item--small.el-form-item {
margin-bottom: 0;
}
.el-form-item__error {
padding-top: 6px !important;
background: #fff;
width: 100%;
z-index: 1;
}
.v-form-item {
margin-bottom: 24px;
& +.v-form-item {
padding-left: 10px;
}
&:nth-last-child(2):first-child {
padding-right: 10px;
}
&__tip {
width: 100%;
height: 20px;
line-height: 24px;
position: absolute;
color: #999;
background: #fff;
z-index: 0;
}
}
</style>
最后看看怎么来写配置
<template>
<section>
<v-form
:schema="schema"
v-model="formData"
:rules="rules"
ref="form"
/>
<el-button @click="handleValidate">验证表单</el-button>
<el-button @click="handleSave">获取数据</el-button>
<el-button @click="handleReset">重置表单</el-button>
</section>
</template>
<script>
export default {
data () {
return {
rules: {
name: [
{ required: true, message: '请输入员工名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
mobile: [
{ required: true, message: '手机号不能为空', trigger: 'blur' }
]
},
schema: [
{
field: 'name',
label: '员工',
tip: '员工的全名,会出现在企业通讯录中',
required: true,
controller: 'el-input',
attrs: {
placeholder: '请输入'
}
},
{
field: 'mobile',
label: '手机',
tip: '需要员工用手机号绑定的微信激活账号,激活后手机号不可编辑',
required: true,
controller: 'el-input',
attrs: {
placeholder: '请输入'
}
},
{
field: 'deptIds',
label: '部门',
tip: '选择的第一个部门将作为该员工的主部门',
controller: 'v-select-dept',
props: {
multiple: true,
store: []
}
},
{
field: 'roleIds',
label: '角色',
controller: 'el-select',
store: [
{
label: '管理员',
value: '0'
}
],
attrs: {
placeholder: '请选择角色'
},
props: {
multiple: true
}
},
{
field: 'position',
label: '职位',
controller: 'el-input',
attrs: {
placeholder: '请输入职位'
}
},
{
field: 'jobNumber',
label: '工号',
controller: 'el-input',
attrs: {
placeholder: '请输入工号'
}
},
{
field: 'email',
label: '邮箱',
controller: 'el-input',
attrs: {
placeholder: '请输入邮箱地址'
}
},
{
field: 'tel',
label: '座机',
controller: 'el-input',
attrs: {
placeholder: '请输入电话号码'
}
},
{
field: 'hiredDate',
label: '入职时间',
controller: 'el-date-picker',
attrs: {
placeholder: '请选择入职时间'
},
props: {
type: 'date'
}
},
{
field: 'workPlace',
label: '办公地点',
controller: 'el-input',
attrs: {
placeholder: '请输入办公地点'
}
},
{
field: 'bySort',
label: '排序权重',
span: 12,
tip: '权重值越大,排序越靠前',
type: 'number',
max: 9999999999,
precision: 4,
controller: 'el-input',
attrs: {
placeholder: '请输入权重数值'
}
},
{
field: 'remark',
label: '备注',
controller: 'el-input',
span: 24,
attrs: {
placeholder: '请输入内容',
rows: 2
},
props: {
type: 'textarea'
}
}
],
formData: {
bySort: null,
name: null,
email: null,
hiredDate: null,
jobNumber: null,
leaveDate: null,
mobile: null,
workPlace: null,
position: null,
remark: null,
roles: null,
depts: null,
tel: null
}
}
},
methods: {
handleSave () {
this.$refs.form.get()
},
handleReset () {
this.$refs.form.resetFields()
},
handleValidate () {
this.$refs.form.validate(res => {
})
}
}
}
</script>
最终效果
这样写配置是不是更简单
- field 对应表单字段。
- label 表单头部。
- span 表单是否占一行还是半行 使用的el-row, el-cel控制布局。
- tip 表单下面的提示。
- controller 下面的就是对应组件以及组件自身的props, attrs, events等等。
- rules其实就跟element-ui是一样的没什么好说的。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: tcly861204 原文链接:https://juejin.im/post/6869204485622169614