import { VNode, CreateElement } from 'vue' import { Component, Prop, Model, Watch, Mixins } from 'vue-property-decorator' import omit from 'lodash/omit' import { Col } from 'element-ui' import MethodsMixins from './mixins/methods' import './custom/fragment' import { Fragment } from 'vue-frag' import { cloneDeep, isFunction, isString, isArray } from 'lodash' import objectPath from './utils/object-path' import { deepQuery, isDefined } from './utils' // 样式 import './styles/index.scss' // 取出vnode匹配表 import { Vnodes, DetailVnodes, SuperCustom } from './vnode' interface IVnodes { [key: string]: any [index: number]: any } interface IModel { [key: string]: any [index: number]: any } @Component({ name: 'ElFormPlus', components: { Fragment }, }) export default class ElFormPlus extends Mixins(MethodsMixins) { // 双向绑定的formData @Model('change', { type: Object }) readonly modelData!: any // 表单布局配置项 https://element.eleme.cn/#/zh-CN/component/layout#row-attributes @Prop({ type: Object, default: () => ({}) }) readonly layout?: any // 表单整体配置 https://element.eleme.cn/#/zh-CN/component/form#form-attributes @Prop({ type: Object, default: () => ({}) }) readonly config!: any // 单个表单配置项组成的表单项渲染规则 @Prop({ type: Array, default: () => [] }) readonly options!: any[] // 各个表单项相同的配置项 @Prop({ type: Object, default: () => ({}) }) readonly unifyOptions!: any public model: IModel = {} public data: any[] = [] public listeners: any = null created() { // 绑定初值 // Watch immediate为true时的执行时机会先于created // 利用这个就做到了modelData(晚渲染)比options(早渲染)优先级更高 this.bindData(this.modelData) // 防止事件注入时再次触发render this.listeners = this.$listeners } // this.model.x = xx 这样的写法只会触发一次 // this.model = { x: xx } 这样的写法只会触发两次 因为这个写法改变了model的原有引用值 @Watch('modelData', { deep: true }) modelDataChange() { this.bindData(this.modelData) this.$emit('change', this.model) } get cacheModelData() { return cloneDeep(this.modelData) } objDiff(a: IModel, b: IModel) { const c: string[] = [] Object.keys(b).map(key => { // 在a中不存在 // 与a中相同的key 但value不同 if (!isDefined(a[key]) || a[key] !== b[key]) { // c[key] = b[key] c.push(key) } }) return c } @Watch('cacheModelData', { deep: true }) cacheModelDataChange(value: IModel, oldValue: IModel) { const changedModelKey = this.objDiff(oldValue, value) changedModelKey.forEach(o => { const option = deepQuery(this.options, o, 'field') if (option) { const { on = {} } = option as any const { modelChange } = on modelChange && isFunction(modelChange) && modelChange.call(this, value[o], this.options, this.model, this) } }) } // 这一步主要是为了方便内部操作options // 深拷贝保存为内部状态 @Watch('options', { immediate: true, deep: true }) public setData() { const options = this.options this.data = options } // 监听options // 先从option中取出所有的field字段 组成model @Watch('data', { immediate: true, deep: true }) public dataChange(val: any, oldVal: any) { const options = val this.buildModel(options) // 将组装好的model对外暴露出去 this.$emit('change', this.model) // this.exportInstance() } // 深度绑定数据 public bindData(data: any) { for (const o in data) { if (Object.prototype.toString.call(data[o]) === '[object Object]') { this.bindData(data[o]) } this.$set(this.model, o, data[o]) } } // 构建model public buildModel(data: any) { for (const o of data) { const result = this.isFieldExist(o) if (!result) { continue } const { field, value, more } = o if (field) { const modelValue = this.model[field] const finalValue = isDefined(modelValue) ? modelValue : value this.$set(this.model, field, finalValue) } if (more && isArray(more)) { this.buildModel(more) } } } // 校验必须参数 // 目前必须的参数为 attrs中的 field字段 public isFieldExist(attrs: any): boolean { // 不需要field时不校验 if (attrs.noField) return true const isExist = objectPath.has(attrs, 'field') if (!isExist) { console.error('field字段不能为空,请检查配置项') return false } return true } // 根据type 判断需要渲染的组件 public renderWhatComponent(type: any) { const vnodes: IVnodes = Vnodes return vnodes[type] } public renderWhatDetailComponent(type: any) { const vnodes: IVnodes = DetailVnodes return vnodes[type] } // 判断需要渲染的containerEl public renderContainerEl(c: any) { if (c) { if (isFunction(c)) { // eslint-disable-next-line no-useless-call return c.call(null, this) } if (isString(c)) { return c } } return 'FFragment' } // eslint-disable-next-line @typescript-eslint/no-unused-vars render(h: CreateElement): VNode { const model = this.model const { row: globalRowConfig = { gutter: 20 }, col: globalColConfig = { span: 12 }, } = this.layout || {} const { container: globalContainer, full = false, detailPattern = 'form', } = this.config // 渲染表单项 const renderSingleForm = (singleFormAttrs: any) => { const { attrs: unifyAttrs = {} } = this.unifyOptions const { placeholder, disabled } = singleFormAttrs const { type = '', attrs = {}, layout, col, field = '', detail = {}, container, on = {}, scopedSlots = {}, } = singleFormAttrs // 将attrs中一些常用的配置提取出来, const shortcutAttrs = { placeholder, disabled } // 作用域插槽本身也是函数,在这里做一次转换 const customScopedSlots: { [key: string]: any } = {} for (const key in scopedSlots) { if (isString(scopedSlots[key])) { customScopedSlots[key] = this.$scopedSlots[scopedSlots[key]] } else { customScopedSlots[key] = scopedSlots[key] } } // 拦截原生input事件,以便触发数据更新 const customInputEvent = (val: any) => { if (field) this.$set(model, field, val) // 拦截之后再触发input事件 const { input } = on if (!input) return if (isFunction(input)) { // eslint-disable-next-line no-useless-call input.call(null, val) } else { console.error(`field='${field}'中input必须是函数`) } } // 取出表单初始值 const value = model[field] // 取出除input事件之外的事件 const extraEvents = omit(on, ['input']) // 是否渲染el-col元素 // 一个el-form-item内部某表单项占据的空间 const ColEl: any = layout ? Col : 'FFragment' // 渲染container // 表单项的包裹元素,目前主要用于表单项的拖拽功能 const ContainerEl = this.renderContainerEl(container) // 需要渲染的组件 SuperComponent const TrueComponent: any = this.renderWhatComponent(type) const DetailComponent: any = this.renderWhatDetailComponent(type) return ( {detailPattern === 'desc' ? ( ) : ( )} ) } // 拼装el-form-item插槽 const getElFormItemScopedSlots = ( field: string, config: any, mergeConfig: any ) => { const { scopedSlots = {} } = config const elFormItemScopedSlots: { [key: string]: any } = {} const { label: labelSlots } = scopedSlots if (labelSlots) { if (isFunction(labelSlots)) { elFormItemScopedSlots.label = () => labelSlots({ h, value: model[field], config, mergeConfig }) } const labelSlotsRender = this.$scopedSlots[labelSlots] if (isString(labelSlots) && labelSlotsRender) { elFormItemScopedSlots.label = () => labelSlotsRender({ h, value: model[field], config, mergeConfig, }) } } return elFormItemScopedSlots } // 渲染 el-form-item const renderElFormItem = (o: any) => { const { config: unifyConfig = {} } = this.unifyOptions // 剥离掉表单项不需要的配置项 const singleFormAttrs = omit(o, ['hidden', 'config', 'more']) const { label, field = '', config = {}, more = [], layout } = o const mergeConfig = { ...unifyConfig, ...config } const { col = globalColConfig, container, cancelrule = false, } = mergeConfig // 将config中一些常用的配置提取出来, const shortcutConfig = { label } const elFormItemScopedSlots = getElFormItemScopedSlots( field, o, mergeConfig ) const isHasField = this.isFieldExist(singleFormAttrs) // 一个el-form-item占据的空间 const ColEl = this.layout ? 'el-col' : 'FFragment' // 一个el-form-item内部的布局 const RowEl = layout ? 'el-row' : 'FFragment' // 渲染container const ContainerEl = this.renderContainerEl(container) // 更多表单项 const moreForm = () => { return more.map((o: object) => { // 不接受layout配置,一定会被more同级的layout配置项覆盖 const props: any = o || {} props.layout = layout return renderSingleForm(props) }) } return ( {isHasField && [renderSingleForm(singleFormAttrs)].concat(moreForm())} ) } const renderSuperCustom = (options: any) => { const { scopedSlots, col = { span: 24 } } = options const customScopedSlots = isString(scopedSlots) ? { custom: this.$scopedSlots[scopedSlots] } : { custom: scopedSlots } const ColEl = this.layout ? 'el-col' : 'FFragment' return ( ) } const renderItem = () => { const options = this.data // 分流,SuperCustom是独立,但还是在el-form里面 // 与el-form-item(不启用布局)或el-row(启用布局)平级 return options .filter((o: any) => isFunction(o.hidden) ? !o.hidden(model) : !o.hidden ) .map((o: any) => { if (o.type === 'SuperCustom') { return renderSuperCustom(o) } return renderElFormItem(o) }) } // 是否渲染el-row元素 const RowEl = this.layout ? 'el-row' : 'FFragment' // 其实 我也没想到有什么应用场景,我主要用来做可拖拽系统的 const GlobalContainer = this.renderContainerEl(globalContainer) // 渲染el-form return ( {renderItem()} ) } }