动态表单引擎:基于Vue2.7 加 Element UI 的实践
动态表单引擎组件目录结构
markdown
FormBuilder/
├── components/
│ ├── BuilderItemRender.vue
├── hooks/
│ ├── use-form-builder.js
└── FormBuilder.vue具体代码
vue
<script>
import {computed, set, watch, toRef} from 'vue'
import {Row, Form} from 'element-ui'
import BuilderItemRender from './components/BuilderItemRender.vue'
export default {
props: {
formProps: Object,
builderItems: Array,
modelValue: Object,
refsMap: Object,
rowProps: {
default: () => ({gutter: 20})
},
span: {
default: 24
}
},
setup(builderProps, {expose}) {
const formProps = computed(() => {
return {
...{
size: 'mini',
labelPosition: 'left',
labelWidth: 'auto'
},
...builderProps.formProps
}
})
const {eventSubMap, defineWatchMap} = getBuilderItemHandler(builderProps.builderItems)
const modelValue = toRef(builderProps, 'modelValue')
if (process.env.NODE_ENV === 'development') {
watch(() => modelValue.value, () => {
console.log('form')
console.log(modelValue.value)
}, {deep: true})
}
expose({
validate: (...args) => builderProps.refsMap['formRef']?.validate?.(...args),
resetFields: () => builderProps.refsMap['formRef']?.resetFields?.(),
clearValidate: (props) => props.refsMap['formRef']?.clearValidate?.(props),
validateField: (prop) => builderProps.refsMap['formRef']?.validateField?.(prop)
})
function processBuilderItems(builderItems) {
const rowGroup = []
let currentGroup = []
for (const item of builderItems) {
item.onInput = val => {
if (item.prop) {
set(modelValue.value, item.prop, val)
}
}
if (item.vIf === false || item.vShow === false || item.invisible === true) {
item.hiddenCallBack?.()
}
if (item.newRow) {
rowGroup.push(currentGroup)
currentGroup = []
}
currentGroup.push(item)
}
rowGroup.push(currentGroup)
return rowGroup
}
const rowGroup = computed(() => processBuilderItems(builderProps.builderItems))
return () => {
return <Form ref={el => builderProps.refsMap['formRef'] = el}
props={{...formProps.value, model: modelValue.value}}
>
{
rowGroup.value.map((group, index) => {
const {rowProps} = group[0]
return <Row key={`row-${index}`} props={{...builderProps.rowProps, ...rowProps}}>
{
group.map(builderItem => <BuilderItemRender
key={builderItem.key}
value={modelValue.value[builderItem.prop]}
onInput={builderItem.onInput}
builderItem={builderItem}
builderProps={builderProps}
modifiers={builderItem.modifiers}
watchHandlers={defineWatchMap.get(builderItem.prop)}
eventHandlers={eventSubMap.get(builderItem.prop)}
/>)
}
</Row>
})
}
</Form>
}
}
}
function getBuilderItemHandler(builderItems) {
const eventSubMap = new Map()
const defineWatchMap = new Map()
for (const item of builderItems) {
if (item.eventSub) {
Object.entries(item.eventSub).forEach(([dependFormKey, dependFormKeyEventHandlers]) => {
if (!eventSubMap.has(dependFormKey)) {
eventSubMap.set(dependFormKey, [dependFormKeyEventHandlers])
} else {
eventSubMap.get(dependFormKey).push(dependFormKeyEventHandlers)
}
})
}
if (item.watch) {
Object.entries(item.watch).forEach(([prop, handler]) => {
if (!defineWatchMap.has(prop)) {
defineWatchMap.set(prop, [handler])
} else {
defineWatchMap.get(prop).push(handler)
}
})
}
}
return {eventSubMap, defineWatchMap}
}
</script>vue
<script>
import {computed, onUpdated, watch} from "vue";
import {Col, FormItem, Input} from "element-ui";
import CollapseTransition from 'element-ui/lib/transitions/collapse-transition'
export default {
props: ['builderItem', 'builderProps', 'value', 'modifiers', 'watchHandlers', 'eventHandlers'],
emits: ['input'],
setup(builderItemProps, {emit}) {
const {builderProps, modifiers, watchHandlers} = builderItemProps
if (process.env.NODE_ENV === 'development') {
onUpdated(() => {
console.log('onUpdated', builderItemProps.builderItem.label)
})
}
const builderItem = computed(() => {
if (!builderItemProps.builderItem.component) {
builderItemProps.builderItem.component = Input
}
return builderItemProps.builderItem
})
const modelValue = computed({
get: () => builderItemProps.value,
set: val => emit('input', val)
})
let immediate = true
watch(() => modelValue.value, (val, oldV) => {
modelValue.value = modifiers?.reduce((acc, cur) => cur(acc), val) ?? val
if (val !== oldV) {
watchHandlers?.forEach(fn => fn(val, oldV, immediate))
}
immediate = false
}, {immediate: true})
const eventHandlers = computed(() => {
return (builderItem.value.component?.emit ?? builderItem.value.component?.emits ?? []).reduce((acc, cur) => {
acc[cur] = (...args) => {
builderItem.value.on?.[cur]?.(...args)
builderItemProps.eventHandlers?.map(m => m?.[cur]?.(...args))
}
return acc
}, {})
})
const colStyle = computed(() => ({
visibility: builderItem.value.invisible ? 'hidden' : 'visible'
}))
const colProps = computed(() => {
return {
span: builderItem.value.span ?? builderProps.span,
...builderItem.value.colProps
}
})
function onInput(val) {
modelValue.value = modifiers?.reduce((acc, cur) => cur(acc), val) ?? val
}
const renderContent = () => {
const {
prop,
component: ComponentObj,
props: componentProps = {},
slots,
style,
clazz,
scopedSlots
} = builderItem.value
if (typeof ComponentObj === 'function') {
return ComponentObj()
}
return <ComponentObj
ref={el => {
if (prop) {
builderProps.refsMap[prop] = el
}
}}
props={componentProps}
attrs={componentProps}
on={eventHandlers.value}
value={modelValue.value}
onInput={onInput}
style={style}
class={clazz}
scopedSlots={scopedSlots}
>
{slots?.map(m => m())}
</ComponentObj>
}
const renderFormItem = (content) => {
const {prop, label, rules, labelWidth, showMessage, formItem} = builderItem.value
return formItem === false
? content
: <FormItem
prop={prop}
label={label}
rules={rules}
label-width={labelWidth}
show-message={showMessage}
>
{content}
</FormItem>
}
return () => <CollapseTransition>
{
builderItem.value.vIf === false
? null
: <Col vShow={builderItem.value.vShow !== false}
style={colStyle.value}
props={colProps.value}
>
{renderFormItem(renderContent())}
</Col>
}
</CollapseTransition>
}
}
</script>js
import {ref, set, defineComponent} from 'vue'
import FormBuilder from '../FormBuilder.vue'
import {zhEn} from '@/composables/use-i18n'
import _ from 'lodash'
export const ModifiersFn = {
// 两端去除空格
trim: val => typeof val === 'string' ? val.trim() : val,
// 不允许有任何空白
noBlank: val => typeof val === 'string' ? val.replace(/\s/g, '') : val,
// 不允许有任何换行符
noNewline: val => typeof val === 'string' ? val.replace(/[\r\n]/g, '') : val,
}
export const RequiredRule = {
required: true,
message: zhEn('不能为空', 'Must not be empty'),
trigger: ['change', 'blur']
}
/**
*
* @param props
* @param props.formProps
* @param props.builderItems
* @param [props.rowProps]
* @param [props.span]
* @param form
*/
export const userFormBuilder = (props, form) => {
const formRef = ref(null)
const refsMap = ref({})
const Component = defineComponent({
setup() {
return () => <FormBuilder
ref={el => formRef.value = el}
modelValue={form.value}
formProps={props.formProps}
builderItems={props.builderItems.value}
refsMap={refsMap.value}
rowProps={props.rowProps}
span={props.span}
/>
}
})
function setFormProp(key, value) {
set(form.value, key, value)
}
function assignForm(obj) {
form.value = Object.assign({}, form.value, obj)
}
const BuilderItemCache = new Map()
/**
* @param {String} key
* @param {Function} builderItemSupplier 返回BuilderItem
* @param {Array} [updateDepends] 组件更新依赖 在任意项变化后更新组件dom
*/
function createItem(key, builderItemSupplier, updateDepends) {
if (BuilderItemCache.has(key)) {
const {builderItem, updateDepends: oldUpdateDepends} = BuilderItemCache.get(key);
const needUpdate = updateDepends ? oldUpdateDepends.some((val, index) => val !== updateDepends[index]) : false
if (needUpdate === false) {
return builderItem
}
}
const builderItem = builderItemSupplier();
builderItem.key = key
BuilderItemCache.set(key, {builderItem, updateDepends})
return builderItem
}
function getFormValue() {
return _.cloneDeep(form.value)
}
return {formRef, Component, refsMap, assignForm, setFormProp, createItem, getFormValue}
}
export class BuilderItem {
/**
*
* @param {Array | String} [label]
* @param {String} [prop]
* @param {Function | Object} [component]
* @param {Number | String} [spanOrLabelWidth]
* @param {String} [labelWidth]
*/
constructor(label, prop, component, spanOrLabelWidth, labelWidth) {
if (label) {
if (Array.isArray(label)) {
this.label = zhEn(label[0], label[1])
} else {
this.label = label
}
}
if (prop) {
this.prop = prop
}
if (component) {
this.component = component
}
if (spanOrLabelWidth) {
if (typeof spanOrLabelWidth === 'string') {
this.labelWidth = spanOrLabelWidth
} else {
this.span = spanOrLabelWidth
}
}
if (labelWidth) {
this.labelWidth = labelWidth
}
}
/**
* 该项开始 另起一行 row
* @param {Boolean} [newRow]
* @param {Object} [rowProps]
*/
NewRow(newRow = true, rowProps) {
this.newRow = newRow
this.rowProps = rowProps
return this
}
/**
* @param {String} labelWidth
*/
LabelWidth(labelWidth) {
this.labelWidth = labelWidth
return this
}
/**
* @param {Boolean} formItem
*/
FormItem(formItem) {
this.formItem = formItem
return this
}
/**
* @param {Boolean} vIf
*/
VIf(vIf) {
this.vIf = vIf
return this
}
/**
* @param {Boolean} vShow
*/
VShow(vShow) {
this.vShow = vShow
return this
}
/**
* @param {Boolean} invisible
*/
Invisible(invisible) {
this.invisible = invisible
return this
}
/**
* @param {Function} fn
*/
HiddenCallBack(fn) {
this.hiddenCallBack = fn;
return this
}
/**
* @param {Number} span
*/
Span(span) {
this.span = span
return this
}
/**
* @param {Object} colProps
*/
ColProps(colProps) {
this.props = colProps
return this
}
/**
* @param {Function | Object} component
*/
Component(component) {
this.component = component
return this
}
/**
*
* @param {Array<Function> | Function} slots
* @returns {BuilderItem}
*/
Slots(slots) {
if (Array.isArray(slots)) {
this.slots = slots
} else {
this.slots = [slots]
}
return this
}
/**
*
* @param {Object} scopedSlots
*/
ScopedSlots(scopedSlots) {
this.scopedSlots = scopedSlots
return this
}
/**
*
* @param {Boolean} showMessage
*/
ShowMessage(showMessage) {
this.showMessage = showMessage
return this
}
/**
* @param {Object} props
*/
Props(props) {
this.props = props
return this
}
/**
* @param {Object} on
*/
On(on) {
this.on = on
return this
}
/**
* @param {Object} style
*/
Style(style) {
this.style = style
return this
}
Clazz(clazz) {
this.clazz = clazz
return this
}
/**
* @param {Array | Object} rules
*/
Rules(rules) {
if (Array.isArray(rules)) {
this.rules = rules
} else {
this.rules = [rules]
}
return this
}
/**
* @param {Object} watch
*/
Watch(watch) {
this.watch = watch
return this
}
/**
* @param {Object} eventSub
*/
EventSub(eventSub) {
this.eventSub = eventSub
return this
}
/**
* @param {Array<Function> | Function} modifiers
*/
Modifiers(modifiers) {
if (Array.isArray(modifiers)) {
this.modifiers = modifiers
} else {
this.modifiers = [modifiers]
}
return this
}
}使用示例
vue