阅读 3420

封装组件的技巧和坑

前置内容

对于Vue的双向绑定大家一定不陌生,入门基础的东西为啥要提?因为这是封装组件的基础!

这里提出一个问题👇 不妨试试你的基础😏

pageIndex.vue

<template>
  <el-card>
 	  <dynamic-form v-model="testModel" />
  </el-card>
</template>
<script>
 export default {
   // ...省去不关键的代码
   data() {
     return {
       testModel: 'Init Value'
     }
   }
 }
</script>
复制代码

dynamic-form.vue

<template>
  <div>
    <el-input v-model="value"></el-input>
  </div>
</template>

<script>
export default {
  name: 'DynamicForm',
  props: {
    value: {}
  },
}
</script>
复制代码

效果图💗

上面的代码问题很明显,有两个问题:

  1. 我们违背了vue的单项数据流,子组件不应该去变动父组件的值
  2. 子组件v-model绑定的value在组件变动值之后,父组件并没有实现双向绑定的效果

这时候我们一般会有两种解决方案

方案一

pageIndex.vue

- <dynamic-form v-model="testModel" />
+ <dynamic-form :value.sync="testModel" />
复制代码

dynamic-form.vue

- <el-input v-model="value"></el-input>
+ <el-input v-model="newValue"></el-input>

+ export default {
+   computed: {
+     newValue: {
+       get({ value }) {
+         return value
+       },
+       set(newVal) {
+         this.$emit('update:value', newVal)
+       }
+     }
+   },
+ }
复制代码

效果图💗

这种方案在我们并时基于开源组件库来封装业务组件的时候用的比较多的,这里解释下为啥这样就行了

我们通过计算属性get来使用父组件的值,set向上抛出新值的变动,让子组件不去直接变动父组件的值,父组件通过.sync父组件来修改子组件的值,子组件中的computed重新计算返回新值

.sync语法糖等同于这样

pageIndex.vue

<dynamic-form :value.sync="testModel" />
<dynamic-form :value="testModel" @update:value="testModel = $event" />
复制代码

这种方式是等价的,.sync这种方式用的也是蛮多的,常出现在我们封装组件「组件嵌套」的情况,但是本质上和v-model这种是干的一样的事,原理也是一样的: 比如, v-model可以自定义model的prop/event

v-model的方式

pageIndex.vue

<dynamic-form
  v-model="testModel"
  @customValue="testModel = $event"
/>
复制代码

dynamic-form.vue

<template>
  <div>
    <el-input v-model="newValue"></el-input>
  </div>
</template>

<script>
export default {
  name: 'DynamicForm',
  model: {
    prop: 'customValue',
    event: 'customEvent'
  },
  props: {
    customValue: {},
  },
  computed: {
    newValue: {
      get({ customValue }) {
        return customValue
      },
      set(newVal) {
        this.$emit('customValue', newVal)
      }
    }
  },
}
</script>
复制代码

.sync的方式

pageIndex.vue

<dynamic-form
  :value="testModel"
  :eventName="eventName"
  v-on:[eventName]="testModel = $event"
/>

<script>
export default {
  data() {
    return {
      eventName: 'update:newValue',
    }
  }
}
复制代码

dynamic-form.vue

<template>
  <div>
    <el-input v-model="newValue"></el-input>
  </div>
</template>

<script>
export default {
  name: 'DynamicForm',
  model: {
    prop: 'customValue',
    event: 'customEvent'
  },
  props: {
    customValue: {},
  },
  computed: {
    newValue: {
      get({ customValue }) {
        return customValue
      },
      set(newVal) {
        this.$emit('customValue', newVal)
      }
    }
  },
}
</script>
复制代码

不难看出来实现的原理都是一样的, 那在Vue2中为什么要去设计一个和v-model干同样事情的API出来呢??

因为v-model不能在同个组件上使用多次!但是.sync这种语法可以啊,我只要在子组件中抛出不同的'update:xxx'就行了!

在Vue3中v-model传递arguments达到使用多次的目的,并且还可以自定义修饰符,所以所以在Vue3种这个语法被干掉了,Vue3相关内容你直接看文档吧😶

方案二

pageIndex.vue

<dynamic-form
  v-model="testModel"
/>
复制代码

dynamic-form.vue

<el-input
  v-bind="$attrs"
  v-on="$listeners"
>
</el-input>
复制代码

效果图💗

双向绑定原理:通过v-bind="$attrs"将父组件的值透传到el-input上实现值的绑定;v-on="$listeners"v-model内部实现的@input事件透传到el-input上实现值响应。

这种方式最为灵活,也是一般封装组件用的最多的一种方式,因为在我们日常工作v-model绑定在自定义组件的值一般不会是基础类型的值,我们绑定的最多的是对象,v-model绑定对象会出现什么问题?

教你优雅的封装动态表单

基本功能

indexPage.vue

<template>
  <el-card>
    <dynamic-form
      v-model="formModel"
      v-bind="formConfig"
    />
  </el-card>
</template>

<script>
import DynamicForm from './common/DynamicForm'
export default {
  name: 'Model',
  components: {
    DynamicForm
  },
  data() {
    return {
      formModel: {},
      formConfig: {
        labelWidth: '100px',
        formItemList: [
          {
            label: '用户名',
            type: 'input',
            prop: 'userName',
          },
          {
            label: '密码',
            type: 'password',
            prop: 'passWord',
            'show-password': true
          },
          {
            label: '备注',
            type: 'textarea',
            prop: 'remark',
            maxlength: 400,
            'show-word-limit': true,
            'auto-size': { minRows: 3, maxRows: 4 }
          }
        ]
      }
    }
  },
}
</script>
复制代码

dynamic-form.vue

<template>
  <el-form v-bind="$attrs">
    <template v-for="formItem of formItemList">
      <dynamic-form-item
        v-model="value[formItem.prop]"
        :key="formItem.prop"
        v-bind="formItem"
      />
    </template>
  </el-form>
</template>

<script>
import DynamicFormItem from './DynamicFormItem'
export default {
  name: 'DynamicForm',
  props: {
    value: {
      type: Object,
      required: true
    },
    formItemList: {
      type: Array,
      default: () => ([])
    }
  },
  components: {
    DynamicFormItem,
  },
  created() {
    this.initFormItemValue()
  },
  methods: {
    initFormItemValue() {
      const formModel = { ...this.value }
      this.formItemList.forEach((item) => {
        // 设置默认值
        const { prop, value } = item
        if (formModel[prop] === undefined || formModel[prop] === null) {
          formModel[prop] = value
        }
      })
      this.$emit('input', { ...formModel })
    },
  }
}
</script>
复制代码

dynamic-form-item.vue

<template>
  <el-form-item :label="label">
    <el-input
      v-if="inputType.includes(type)"
      :type="type"
      v-bind="$attrs"
      v-on="$listeners"
    >
    </el-input>
  </el-form-item>
</template>

<script>

/* 这些配置项实际工作中可以单独提取成配置文件 */
const inputType = [/* input支持的类型 */]
const selectType = [/* select支持的类型 */]
const customType = [/* 自定义的表单项组件 */]

export default {
  name: 'DynamicFormItem',
  props: {
    label: {
      type: String,
      required: true
    },
    type: {
      type: String,
      require: true,
      validator: (type) => {
        return [...inputType, ...selectType, ...customType]
          .includes(type)
      }

    }
  },
  data() {
    return {
      inputType: Object.freeze(inputType),
    }
  },
}
</script>
复制代码

效果图💗

可以Get到的技巧:

  • 通过v-fortemplate减少创建不必要的dom节点,也可以通过这种方式实现v-for/v-if一起使用的效果
  • 不需要响应式的数据通过Object.freeze冻结,Vue在初始化的时候就不会给这个属性加上get/set, 也算是性能优化的点

支持深度属性

在实际开发中,表单对象的属性还有可能是对象,拿常见的CRM/ERP系统来讲,一般像**[租赁合同,定金协议,项目设置]**等等,这些引用主数据的基本都是级联引用,这个时候表单组件就需要支持这种功能

indexPage.vue

<script>
export default {
  data() {
    return {
      formModel: {
        depositAgreement: {
          depositAmount: 100,
          businessType: 'traditionWork',
          signDate: '2021-7-18'
        }
      },
      formConfig: {
        labelWidth: '100px',
        formItemList: [
          {
            label: '金额',
            type: 'number',
            prop: 'depositAgreement.depositAmount',
          },
          {
            label: '协议类型',
            type: 'select',
            prop: 'depositAgreement.businessType',
            options: [
              {
                dictKey: 'traditionWork',
                dictValue: '专属办公',
              }
            ],
          },
          {
            label: '签订日期',
            type: 'depositAgreement.date',
            prop: 'signDate',
          }
        ]
      }
    }
  },
}
</script>
复制代码

效果图💗

支持深度属性需要用到两个方法

  • _getDeepAttr(model, deepPath)
  • _setDeepAttr(model, deepPath, val)
_getDeepAttr实现
let data = {
  obj: {
    v1: 'v1-val'
  }
}
_getDeepAttr(data, 'obj.v1') // => v1-val
复制代码
/**
 * @description: 深度获取属性
 * @param {Object} model 表单对象
 * @param {String} deepPath 深度属性
 * @return {any} 
 */
function _getDeepAttr(model, deepPath) {

  if (!deepPath) return
  if (deepPath.indexOf('.') !== -1) {
    const paths = deepPath.split('.')
    let current = model
    let result = null
    for (let i = 0, j = paths.length; i < j; i++) {
      const path = paths[i]
      if (!current) break
      if (i === j - 1) {
        result = current[path]
        break
      }
      current = current[path]
    }
    return result
  } else {
    return model[deepPath]
  }
}
复制代码
_setDeepAttr实现
let data = {
  obj: {
    dep1: {
      v1: 'v1',
      v2: 'v2'
    },
    name: '北歌'
  }
}
_setDeepAttr(data, 'obj.dep1.v1', 'v1-newVal') 
// data.obj.dep1.v1 => v1-newVal
// data.obj.dep1.v2 => v2
复制代码
/**
 * @description: 设置深度属性
 * @param {Object} model 表单对象
 * @param {String} deepPath 深度属性
 * @param {any} val 要设置的值
 */
function _setDeepAttr(model, deepPath, val) {
  // 路径
  let paths = deepPath.split('.')
  // 目标值,后面这个值会存放符合路径下的所有属性
  let targetVal = {}
  // 陆续查找每个对象的prop
  let pathsNew = [...paths]
  let prop
  for (let i = paths.length - 1, j = i; i >= 0; i--) {
    prop = paths[i]
    // 最后一层要设定的值
    if (i === j) {
      targetVal[prop] = val
    } else if (i === 0) {
      // 先获取根属性的值
      const originalVal = model[prop]
      // 第一层需要直接替换的根属性
      model[prop] = Object.assign(originalVal, targetVal)
    } else {
      // 更新每一个层级的值(去除存起来的值)
      let curDeppObj = _getDeepAttr(model, pathsNew.join('.'))
      // 将当前层级的值存储起来
      targetVal[prop] = Object.assign({}, curDeppObj, targetVal)
      // 删除上个路径存储的值
      delete targetVal[paths[i + 1]]
    }

    // 将处理过的路径去除
    pathsNew.pop()
  }
}
复制代码

_getDeepAttr啥好讲的,这里主要讲下_setDeepAttr的实现思路

实现思路

  • 拆分路径:拿到每一层级下的属性名,因为后面需要一层一层往下找,拆分出来的层级数组做备份
  • 倒序循环:我们最终的目的是给最后一层的属性赋值,需要j来确定当前循环是否是最后一层
  • 循环体内的个判断条件
    1. i === j:最后一层,也就是第一次循环,我们直接将临时对象设置为指定值
    2. i === 0: 第一层,也就是最后一层循环,这个时我们直接读model[第一层属性]拿到根属性,值很显然是对象,接着将根属性直接替换成重新设置临时对象
    3. 中间层级的处理:需要将获取到当前层级的属性对象, 将当前层级的对象和临时对象进行合并,只影响目标属性,同层级的其他属性不能丢失,最后将上个层级的属性去除

调试图💗

强烈建议跟着调试一遍,一学就废~😃

dynamic-form.vue

<template>
  <el-form v-bind="$attrs">
    <template v-for="formItem of formItemList">
      <dynamic-form-item
-       v-model="value[formItem.prop]"
+       :value="value[formItem.prop] | _formatterItemVal(value, formItem, _getDeepAttr)"
+       @input="bindItemValue(value, formItem, $event)"
        :key="formItem.prop"
        v-bind="formItem"
      />
    </template>
  </el-form>
</template>

<script>
import DynamicFormItem from './DynamicFormItem'
export default {
  name: 'DynamicForm',
  props: {
    value: {
      type: Object,
      required: true
    },
    formItemList: {
      type: Array,
      default: () => ([])
    }
  },
  components: {
    DynamicFormItem,
  },
-  created() {
-    this.initFormItemValue()
-  },
+  filters: {
+    /**
+     * @description: 
+     * @param {any} curVal 当前表单的值(如果是深度属性就为空)
+     * @param {Object} value form的值
+     * @param {Object} item 当前表单配置项
+     * @param {Function} _getDeepAttr 格式化item值的方法
+     * @return {*}
+     */
+    _formatterItemVal: (curVal, value, item, _getDeepAttr) => {
+      if (curVal) {
+        return curVal
+      }
+
+      // 往下走就是深度属性的情况,需要格式化获取值
+
+      // 提供给用户格式化value的的方法: 如trim、number等作用
+      const formater = item.formatter
+
+      return typeof formater === 'function'
+        ? formater(_getDeepAttr(value, item.prop))
+        : _getDeepAttr(value, item.prop)
+    }
  },
  methods: {
-    	initFormItemValue() {
-      const formModel = { ...this.value }
-      this.formItemList.forEach((item) => {
-        // 设置默认值
-        const { prop, value } = item
-        if (formModel[prop] === undefined || formModel[prop] === null) {
-          formModel[prop] = value
-        }
-      })
-      this.$emit('input', { ...formModel })
-    	},

+     /**
+     * @description: 实现双向绑定
+     * @param {Object} model 表单对象
+     * @param {String} deepPath 深度属性
+     * @param {any} val 要设置的值
+     */
+    bindItemValue(model, item, val) {
+      // 深度属性需要格式化
+      if (~item.prop.indexOf('.')) {
+        this._setDeepAttr(model, item.prop, val)
+      } else {
+        model[item.prop] = val
+      }
+
+      const _model = { ...model }
+      this.$emit('input', _model)
+    }

+    _getDeepAttr(){ /*... */ }
+    _setDeepAttr(){/*... */}
  }
}
</script>
复制代码

dynamic-form-item.vue

<template>
  <el-input
    v-if="inputType.includes(type)"
    :type="type"
    v-bind="$attrs"
    v-on="$listeners"
  />
  <dynamic-select
    v-else-if="selectType.includes(type)"
    v-bind="$attrs"
    v-on="$listeners"
  />
  <el-date-picker
    v-else-if="dateType.includes(type)"
    v-bind="$attrs"
    v-on="$listeners"
  />
</template>
复制代码

表单项组件只是多加了两个对表单项类型的支持,并没有其他逻辑,dynamic-select组件是封装的支持select/treeSelect通过url动态获取options的扩展组件,感兴趣的可以看专栏里的这篇文章

自定义内容(slots)

前置知识👉 精讲插槽

对于el-form进行包装之后封装的组件也需要支持form-item插槽,就这三个:

工作中我们自定义最多的就是表单项。比如这种:使用业务封装的组件作为表单项

不管是什么样的组件,总之这个表单项内容必须得可自定义,这个时候就得用到插槽了

  • 支持两种渲染方式
    1. template模板的方式
    2. 在配置项中写render函数的方式

优先级:我们遵循和vue一样的规则,render函数的方式是高于template模板的

{
  label: '自定义',
  type: 'slot',
  prop: 'custom'
}
复制代码

template写插槽的方式

<dynamic-form
  v-model="formModel"
  v-bind="formConfig"
>
  <template #custom="{value}">
    <el-button>{{value}}</el-button>
  </template>
</dynamic-form>
复制代码

配置项写render函数的方式

{
  label: '自定义',
  type: 'slot',
  prop: 'custom',
  render: ({ value, $createElement: h }) => {
    return h('el-button', value)
  }
}
复制代码

dynamic-form.vue

dynamic-form这个组件改动不大,就加了这一项将当前实例往下注入

export default {
  provide() {
    return {
      formThis: this
    }
  },
}
复制代码

dynamic-form-item.vue

<template>
  <!-- 添加对type: slot的支持 -->
  <slot-content
    v-else-if="isRenderSlot({type, prop})"
    v-bind="$attrs"
    :render="generateSlotRender()"
  />
</template>

<script>
export default {
  /**
  * @description: 是否渲染自定义内容
  * @param {String} type
  */
  isRenderSlot({ type, prop }) {
    if (type !== 'slot') {
      return false
    }
    /* 
      支持两种渲染方式
        1. template模板的方式
        2. 在配置项中写render函数的方式
    */
    return [
      typeof this.formThis.$scopedSlots[prop],
      typeof this.$attrs.render
    ].includes('function')
  },
  // 渲染自定内容的render函数 优先级:在配置项中写render函数的方式 > template的方式
  generateSlotRender() {
    // normalizeScopedSlot 
    return ({ value, $createElement }) => {
      // 给插槽传递参数
      const slotScope = { ...this.$attrs, value, $createElement }
      const renderSlot = this.$attrs.render || this.formThis.$scopedSlots[this.prop]
      return renderSlot(slotScope)
    }
  },
}
</script>
复制代码

可以Get到的技巧:

  • 这种方式读起来比写老长的||读起来更清晰一点
[
	typeof xxx,
  typeof xxxx,
].includes('function')

复制代码
  • $scopeSlots作用域插槽中的方法是用来返回VNode, 我们可以传递参数来给插槽传递数据

效果图💗

label/error内置插槽的支持

在实现这两个插槽之前需要改下el-form-item,让用户使用深度属性配置插槽的时候好写一点。

比如用户设置了
{
  prop: 'depositAgreement.depositAmount',
  ...
}
复制代码

这个时候用户想要在使用dynamic-form个组件自定义表单项内容的时候需要怎么去用

<!-- depistiAmountSlot是在data中定义的变量 depistiAmountSlot: 'depositAgreement.depositAmount' -->
<template #[depistiAmountSlot]></template> 

<!-- 这种方式是不行的 -->
<template #depositAgreement.depositAmount></template>
复制代码

在使用template去写组件的时候,我们写的所有东西都要经过compile,如果模板上有特殊字符肯定也需要做更多的额外出理,所以在享受template模板语法带来的快捷同时也丢失了一些灵活度。

dynamic-form-item.vue

<template>
  <el-form-item
    :rules="rules"
    :label="label"
    :prop="prop"
  >
    <template
      v-if="formThis.$scopedSlots[realProp + 'Label']"
      #label
    >
      <slot-content
        v-bind="_attrs"
        :render="formThis.$scopedSlots[realProp + 'Label']"
      />
    </template>
    <template
      v-if="formThis.$scopedSlots[realProp + 'Error']"
      #error="{error}"
    >
      <slot-content
        v-bind="{..._attrs, error}"
        :render="formThis.$scopedSlots[realProp + 'Error']"
      />
    </template>
  </el-form-item>
</template>


<script>
export default {
  computed: {
    // 传递给插槽的数据
    _attrs({ value, label, rules, realProp, $attrs }) {
      return {
        value,
        label,
        rules,
        realProp,
        ...$attrs
      }
    },
    // 对于深度属性做特殊处理 data.val => dataVal
    realProp({ prop }) {
      return prop.replace(/\.([^.]+)+?/g, (...arg) => {
        const [, execProp] = arg
        return execProp[0].toUpperCase() + execProp.substr(1)
      })
    }
  }
}
</script>
复制代码

效果图💗

表单校验

自动校验的支持

如果是非深度属性的值校验其实只要在e-form上加model属性,在el-form-item上加prop/rules就已经实现了,但如果是深度属性,这个时候就需要做一些额外的处理。

{
  label: '金额',
  type: 'text',
  prop: 'depositAgreement.depositAmount',
  rules: [
    {
      required: true,
      message: '金额不能为空',
      trigger: 'blur'
    },
    {
      validator: (rule, val, callback) => {
        if (val < 100) {
          return callback(new Error('金额不能小于100'))
        }
        return callback()
      },
      trigger: ['change', 'blur']
    }
  ]
},
复制代码

dynamic-form.vue

{
  computed: {
    // 需要校验的item
    validateItem({ formItemList }) {
      return formItemList.filter(i => i.rules)
    },
    // 传递的form的数据源,用于处理深度属性校验问题
    model({ validateItem, value, isDeepPath, _getDeepAttr }) {
      const _model = { ...value }
      if (!validateItem.length) return _model;

      validateItem.forEach(({ prop }) => {
        if (isDeepPath(prop)) {
          _model[prop] = _getDeepAttr(_model, prop, _getDeepAttr)
        }
      })
      return _model
    }
  },
}
复制代码

效果图💗

我们计算出来的model只给el-form的model属性用,且只有当用户设置了深度属性且设置了校验model才会计算出来用于校验的属性

手动校验的的支持

只要给el-form设置ref属性就可以了,为了方便用户自定义,ref就不直接写死

{
  props: {
    // 自定义elForm的ref属性
    elFormRef: {
      type: String,
      default: 'elForm'
    }
  },
  methods: {
   /**
     * @description: 表单整体校验
     */
    validate(callback) {
      return this.$refs[this.elFormRef].validate(callback)
    },
    /**
     * @description: 单个字段校验
     */
    validateField(props, callback) {
      return this.$refs[this.elFormRef].validateField(props, callback)
    },
    /**
     * @description: 清除校验
     */
    clearValidate(props) {
      return this.$refs[this.elFormRef].clearValidate(props)
    },
    /**
     * @description: 表单重置,清除校验
     */
    resetFields() {
      return this.$refs[this.elFormRef].resetFields()
    },
  }
}
复制代码

上面这些其实可以不写,为了方便用户操作,不写老长的$refs一层的引用,让用户自己通过reference的方式去操作

v-on="$linsters"的坑,千万小心!

接下来就是最后一环,我们需要对向外界支持所有表单项的自定义事件,这不挺容易的嘛,v-on="$listeners"透传事件下去不就完事?

使用

<dynamic-form
  ref="dynamicForm"
  v-model="formModel"
  v-bind="formConfig"
  @input="handleInput"
>
复制代码

dynamic-form.vue

<dynamic-form-item
  :key="formItem.prop"
  :value="value[formItem.prop] | _formatterItemVal(value, formItem, _getDeepAttr)"
  v-bind="formItem"
+  v-on="$listeners"
  @input="bindItemValue(value, formItem, $event)"
/>
复制代码

仔细看看这段代码,真的没有问题嘛???

我们v-on="$listeners"是不是一个不小心把v-model的内置input事件也透传下去了?会有问题嘛?

故事的开始是这样的:

el-input和dynamic-form-item交互的过程:

  1. 我们改变输入框值
  2. el-input抛出input事件并携带当前表单项的值Sting类型的基本值)
  3. dynamic-form-item组件在使用el-input写了v-on="$listeners"将所有用在dynamic-form-item上事件全都透传到el-input上
  4. 我们在使用dynamic-form-item也确实自己写了input事件的处理程序

到这我们实现了 dynamic-item和el-input的双向绑定

dynamic-form-item和dynamic-form的交互

  1. dynamic-form中我们通过监听@input,监听当前变动的哪个表单项,将表单对象的值重新更新抛出input事件
  2. 用户通过v-model内部实现的input监听事件及事件处理方法帮我们实现了双向绑定

故事到这就非常的圆满。

可是我外部又自己写了一个@input事件,并且这个事件还透传到el-input上了! 那表单项变动我就得监听, v-model内部实现的input事件处理不得重新赋值?

结果内部就给v-model="formModel"中的formModel重新赋值了,从一个对象变成了基本值(el-input抛出的值)。

故事到这也就结束了。页面报错了

所以dynamic-form是有两个Input事件算是程序的,怎么去看?在dynamic-form打印下this,来看看

dynamic-form.vue

稍微改造下

export default {
  // 解决$listeners透传事件导致双绑问题
  model: {
    // 自定义v-model的监听事件名
    event: 'dyInput' 
  },
}
复制代码

抛事件的话我们这样去抛就行

this.$emit('dyInput', {...value})
复制代码

效果图💗

到这功能算是完成了,但是外界监听input事件没法具体到哪个表单项,这事件就基本没用!我们需要给外界加个标识告诉他是那个表单项抛出的事件。

还有一个问题,表单项随着项目增多肯定类型也越来越多, 事件类型也就越来越多,很多表单项抛出的事件名都是一样的,外界并不太好去管控这些事件,而且所有事件都写在这个组件上,想要区分事件到底是哪个组件抛出来的还得去input事件里面输出一下嘛?这不太好。。。

示例代码

<dynamic-form
  ref="dynamicForm"
  v-model="formModel"
  v-bind="formConfig"
  @input="handleInput"
  @change="handleChange"
  @visible-change="handleVisibleChange"
  @...
/>
复制代码

我们希望dynamic-form上只有input/change事件,其他事件全都写在各自配置项里面

{
  label: '协议类型',
  type: 'select',
  // ...省略options,props配置
  prop: 'depositAgreement.businessType',
  listeners: {
    'visible-change': (isShow) => {
      console.log(isShow, 'select');
    }
  }
},
复制代码

dynamic-form.vue

<dynamic-form-item
  v-on="_listeners"
/>

<script>
export default {
  computed: {
    // 能在dynamic-form监听的事件
    _listeners({ $listeners }) {
      // 支持往下透传的事件
      let supportEvent = ['input', 'change']
      return supportEvent.reduce((_listeners, eventName) => {
        _listeners[eventName] = $listeners[eventName] || (() => { })
        return _listeners
      }, {})
    }
  }
}
</script>
复制代码

dynamic-form-item.vue

<dynamic-form-item
  v-on="_listeners"
/>

<script>
export default {
  computed: {
    // 整合配置项中的listeners,最终向下透传的事件 
    onEvent({ $listeners, listeners }) {
      // 配置项中的事件优先级大于在dynamic-form中监听的事件
      return { ...$listeners, ...listeners }
    }
  }
}
</script>
复制代码

扩展:v-model源码浅析

transform component v-model data into props & events

src\core\vdom\create-component.js

// transform component v-model data into props & events
// v-model特殊处理
if (isDef(data.model)) {
  transformModel(Ctor.options, data)
}
复制代码

transformModel

// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
  /* 
    model: {
      prop: 'xx',
      event: 'xxx,
    }
  */
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'
  // 给attrs的指定model的prop赋值
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value
  const on = data.on || (data.on = {})
  const existing = on[event]
  const callback = data.model.callback
  if (isDef(existing)) {
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1
        : existing !== callback
    ) {
      on[event] = [callback].concat(existing)
    }
  } else {
    // 给事件里面添加v-model解析出来的内置事件
    on[event] = callback
  }
}
复制代码

至此这篇文章就分享结束了,表单组件的大部分都实现了,还有表单布局没有去实现,其实就是需要用el-row/el-col包下表单项,并没有特别的逻辑就不贴代码了,感兴趣的可以点我查看代码

写在最后

如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下

我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。

业精于勤,荒于嬉

系列文章

手把手教你玩转render函数「组件封装-dynamic-select」

手把手教你玩转render函数「组件封装-dynamic-input」

手把手教你玩转render函数「组件封装-dynamic-checkbox」

手把手教你玩转render函数「组件封装-dynamic-cascader」

往期文章

【建议追更】以模块化的思想来搭建中后台项目

【以模块化的思想开发中后台项目】第一章

【前端体系】从一道面试题谈谈对EventLoop的理解 (更新了四道进阶题的解析)

【前端体系】从地基开始打造一座万丈高楼

【前端体系】正则在开发中的应用场景可不只是规则校验

「函数式编程的实用场景 | 掘金技术征文-双节特别篇」

【建议收藏】css晦涩难懂的点都在这啦

文章分类
前端
文章标签