vue封装表单组件
悟空和大王 人气:0封装组件注意点
- 不要为了封装而封装
- 只封装不变的部分,将变化的部分通过slot或其他方式,暴露出去,交给调用者实现
- 为了提供封装组件的复用率,优先封装为UI组件,而不是封装为业务组件(即:封装组件内部使用到的数据,都通过prop获取,而不是直接通过ajax请求或vuex中获取)
动态表单动态控制的是什么?
已知的需要动态控制的场景:
- 在A界面显示:A, B, C三个表单项,B界面显示:A,B,E,F三个表单项
- 在A界面默认显示:A,B两个表单项,当A项的值为x时,C表单项才显示出来
- 在A界面默认显示:A,B,C三个表单项,A,B默认可用,C默认禁用,当A项的值为x时,C表单项才可用
- 在A界面B表单项可选择的值,依赖于A表单项的选择/填写结果
- 根据不同的分辨率每行可显示的表单项数量不同
动态表单封装
请优先考虑风格二的封装方式
风格一
特点简介
将el-form, el-form-item, el-input等等完全封装到动态表单组件内,通过for循环来刷表单项,外部通过传json配置对象的方式,完成字段的动态显示/隐藏控制。
优点:
- 减少了
<el-form-item><el-input></el-input></el-form-item>
这类代码的重复编写 - 可以轻易的改变表单项的先后顺序
缺点:
- 如果所有用到表单的地方,都用这一个表单组件实现,那么这个组件后期一定会变得非常庞大,充斥大量的逻辑控制代码,导致后期难以维护
- 要想最大程度的保留原始表单的配置和事件,那就需要使用
v-bind=attrs
和v-on=evts
方式进行配置,这相对于template语法来说,json对象的配置方式,就没那么让vue开发者习惯了。 - 通过
v-bind=attrs
方式设置原组件的属性,那么如果想设置一些默认值就变得麻烦起来,因为vue2中,设置了v-bind=attrs
之后,没法再在模板中设置默认属性,必须在这个封装的组件中,在获取到配置对象时,进行一些比较繁琐的初始值设置。 - 如果表单model对象是通过prop传入,那么表单内部修改这个表单对象需要做特殊处理,来规避eslint对单项数据流的检查报错
代码实现示例
动态表单组件
<template> <el-form :model="formModel" v-bind="elFormAttrs"> <el-form-item v-for="(formItemConfig, index) in formItemConfigArr" :key="`${formItemConfig.prop}-${index}`" :prop="formItemConfig.prop" :rules="formItemConfig.rules" > <template #label> <div class="o-custom-label">{{ formItemConfig.label }}</div> </template> <el-input v-if="formItemConfig.itemType === 'input'" v-model="formModel[formItemConfig.prop]" ></el-input> <el-select v-else-if="formItemConfig.itemType === 'select'" v-model="formModel[formItemConfig.prop]" > <template v-if="formItemConfig.optionArr"> <el-option v-for="(option, pos) in formItemConfig.optionArr" :key="`${option.value}-${pos}`" :value="option.value" :label="option.label" ></el-option> </template> </el-select> </el-form-item> </el-form> </template> <script> export default { name: "DynForm", model: { event: "change", prop: "formData", }, props: { formData: { type: Object, }, /** * prop: 唯一标识 * itemType: 表单项类型 * rules: 表单验证规则 * label: 显示标签 * optionArr: 下拉值 */ formItemConfigArr: { type: Array, default: () => [], }, // el-form支持的所有属性 elFormAttrs: { type: Object, }, }, data() { return { formModel: this.formData ? this.formData : {}, }; }, watch: { // 监听formData改变,将formData的值设置给组件内部的formModel,规避eslint对单项数据流的规则检测报错 formData: { handler(newVal) { this.formModel = newVal; }, }, }, methods: {}, }; </script> <style scoped></style>
调用动态表单组件
<template> <div class="demo-form"> <DynForm v-model="formData" :form-item-config-arr="formItemConfigArr" :el-form-attrs="elFormAttrs" ></DynForm> </div> </template> <script> import DynForm from "@/components/form/dyn/DynForm"; export default { name: "FormTemplate3", components: { DynForm, }, props: { // input文本框数量 count: { type: Number, default: 50, }, }, data() { // 表单项配置 const formItemConfigArr = [ { prop: "sex", label: "成语故", itemType: "select", optionArr: [], }, { prop: "name", label: "姓名", itemType: "input", }, { prop: "three", label: "成语故事", itemType: "select", optionArr: [], }, { prop: "four", label: "一二三四五", itemType: "select", optionArr: [], }, { prop: "five", label: "一二三四五六", itemType: "select", optionArr: [], }, { prop: "six", label: "一二三四五六七", itemType: "select", optionArr: [], }, { prop: "seven", label: "一二三四五六七八", itemType: "select", optionArr: [], }, { prop: "eight", label: "一二三四五六七八九", itemType: "select", optionArr: [], }, { prop: "ten", label: "ten", itemType: "select", optionArr: [], }, { prop: "zero", label: "zero", itemType: "select", optionArr: [], }, { prop: "a", label: "hello", itemType: "select", optionArr: [], }, { prop: "b", label: "hello world", itemType: "select", optionArr: [], }, { prop: "c", label: "good idea thank", itemType: "select", optionArr: [], }, { prop: "d", label: "good configuration", itemType: "select", optionArr: [], }, { prop: "d", label: "good idea thank configuration", itemType: "select", optionArr: [], }, ]; return { elFormAttrs: { inline: true, labelWidth: "81px", }, formData: null, formItemConfigArr, }; }, created() { this.loadFormData(); this.loadSexOptions(); }, methods: { loadFormData() { setTimeout(() => { this.formData = { name: "张三", sex: 1, }; }, 1000); }, loadSexOptions() { setTimeout(() => { const item = this.formItemConfigArr.find((item) => item.prop === "sex"); if (item) { const sexOptionArr = [ { value: 1, label: "选项1" }, { value: 2, label: "选项2" }, { value: 3, label: "选项3" }, ]; item.optionArr = sexOptionArr; } }, 1500); }, }, }; </script> <style scoped lang="scss"> .js-validate-form ::v-deep(.is-error .o-show-data) { color: red; } .demo-form ::v-deep(.el-form-item__label) { line-height: 1.6; display: inline-flex; height: 40px; justify-items: right; justify-content: flex-end; align-items: center; } .demo-form ::v-deep(.el-form-item__label .o-custom-label) { word-break: break-word; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; /* 这里是超出几行省略 */ overflow: hidden; } </style>
风格二 (推荐优先采用此风格)
特点介绍
通过函数组件,以jsx语法对el-form-item进行封装,仅封装模板代码部分,动态部分全部由调用者自行实现
优点:
- 能够针对界面特点,仅封装不变的模板代码部分,能够最大程度保留template编程风格
- 因为表单项都是通过slot实现,不会导致这个表单组件随着应用的场景增多和变得越来越复杂,复杂度会基本保持不变
缺点:
- 需要掌握jsx语法(jsx仅用于封装组件,调用这个组件使用template语法即可)
代码实现示例
表单项组件封装
<script> export default { functional: true, name: "DynFormItemsFunction", props: { formItemConfigArr: { type: Array, required: true, }, }, render(h, context) { // console.log(context); const { props, scopedSlots } = context; const { formItemConfigArr } = props; // 获得插槽里的 vNodes const vNodes = scopedSlots.default(); return vNodes.map((node, idx) => { const formItemConfig = formItemConfigArr[idx]; return ( <el-form-item prop={formItemConfig.prop} rules={formItemConfig.rules}> <span slot="label" className="o-custom-label"> {formItemConfig.label} </span> {node} </el-form-item> ); }); }, }; </script> <style scoped> .o-custom-label { color: blue; } </style>
代码调用示例
<template> <div> <el-form :model="formData" inline :validate-on-rule-change="false"> <!-- DynFormItemsFunction 这个组件,仅封装了el-form-item的逻辑 --> <DynFormItemsFunction :form-item-config-arr="formItemConfigArrComp"> <!-- 注意组件的循环是在slot中进行的,el-form-item的包装逻辑, 交给了DynFormItemsFunction组件实现 --> <template v-for="(formItemConfig, idx) in formItemConfigArrComp"> <el-input v-if="formItemConfig.itemType === 'input'" v-model="formData[formItemConfig.prop]" :key="idx" ></el-input> <el-select v-else-if="formItemConfig.itemType === 'select'" v-model="formData.sex" :key="idx" > <template v-if="formItemConfig.optionArr"> <el-option v-for="option in formItemConfig.optionArr" :key="option.value" :value="option.value" :label="option.label" ></el-option> </template> </el-select> </template> </DynFormItemsFunction> </el-form> </div> </template> <script> import DynFormItemsFunction from "@/components/form/dyn/DynFormItemsFunction"; export default { name: "FormTemplate", components: { DynFormItemsFunction, }, props: { count: { type: Number, default: 50, }, }, data() { /* 完整的表单配置放在data中,数据中的元素个数不会变, 动态值也填充到formItemConfigArr对应元素中(如性别的下拉选项值), formItemConfigArr只需要关注完整表单需要哪些表单项字段, 以及下拉选项数据的填充,无需考虑表单项字段的显示/隐藏逻辑控制 */ const formItemConfigArr = [ { prop: "sex", label: "性别", itemType: "select", optionArr: [], }, { prop: "name", label: "姓名", itemType: "input", rules: [{ required: true, message: "该项必填", trigger: "change" }], }, { prop: "school", label: "学校", itemType: "input", rules: [], }, ]; // for (let i = 0; i < this.count; i++) { // formItemConfigArr.push({ // prop: "name" + i, // label: "姓名" + i, // itemType: "input", // }); // } return { formData: { name: null, sex: null, school: null, }, formItemConfigArr, }; }, computed: { /* 表单项的显示/隐藏通过计算属性实现,可以认为计算属性就只关注控制哪些表单项需要显示, 哪些需要隐藏,就可以了。算是一种职责分离 */ formItemConfigArrComp() { return this.formItemConfigArr.filter((item) => { if (this.formData.sex === 3 && item.prop === "name") { return null; } return item; }); }, }, created() { this.loadOptions(1500); }, methods: { loadOptions(timeout) { setTimeout(() => { const item = this.formItemConfigArr.find((item) => item.prop === "sex"); if (item) { const sexOptionArr = new Array(10) .fill(true) .map((item, idx) => ({ value: idx, label: "选项:" + idx })); item.optionArr = sexOptionArr; } }, timeout); }, }, }; </script> <style scoped lang="scss"> .js-validate-form ::v-deep(.is-error .o-show-data) { color: red; } </style>
加载全部内容